-
Notifications
You must be signed in to change notification settings - Fork 3.7k
fix(billing): point self-hosted upgrade CTAs at the hosted app #6003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { redirect } from 'next/navigation' | ||
| import { getSession } from '@/lib/auth' | ||
| import { isUpgradeReason, UPGRADE_REASON_PARAM } from '@/lib/billing/upgrade-reasons' | ||
|
|
||
| /** | ||
| * Public upgrade entry, for callers that cannot know a workspace id — a | ||
| * self-hosted deployment linking its users to the hosted plans, or an email | ||
| * that predates a workspace switch. | ||
| * | ||
| * Workspace resolution is not repeated here: `/workspace` already owns it, | ||
| * including local recency, last-active fallback, stale-session recovery, and | ||
| * the no-workspace creation policy. | ||
| */ | ||
| export default async function UpgradePage({ | ||
| searchParams, | ||
| }: { | ||
| searchParams: Promise<Record<string, string | string[] | undefined>> | ||
| }) { | ||
| const [session, params] = await Promise.all([getSession(), searchParams]) | ||
|
|
||
| const rawReason = params[UPGRADE_REASON_PARAM] | ||
| const reasonValue = Array.isArray(rawReason) ? rawReason[0] : rawReason | ||
| const reason = isUpgradeReason(reasonValue) ? reasonValue : undefined | ||
|
|
||
| const target = reason | ||
| ? `/workspace?redirect=upgrade&${UPGRADE_REASON_PARAM}=${reason}` | ||
| : '/workspace?redirect=upgrade' | ||
|
|
||
| // `/workspace` recovers a signed-out visitor by hard-navigating to `/login` | ||
| // with no callback, which would drop the upgrade intent — carry it here | ||
| // instead, where the destination is still known. | ||
| if (!session?.user) { | ||
| redirect(`/login?callbackUrl=${encodeURIComponent(target)}`) | ||
| } | ||
|
|
||
| redirect(target) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,14 +10,17 @@ import { | |
| ExpandableContent, | ||
| SecretInput, | ||
| SecretReveal, | ||
| SquareArrowUpRight, | ||
| Tooltip, | ||
| toast, | ||
| } from '@sim/emcn' | ||
| import { useParams } from 'next/navigation' | ||
| import { ThinkingLoader } from '@/components/ui' | ||
| import { useSession } from '@/lib/auth/auth-client' | ||
| import { buildHostedUpgradeUrl, HOSTED_BILLING_SETTINGS_URL } from '@/lib/billing/upgrade-reasons' | ||
| import { canManageWorkspaceBilling } from '@/lib/billing/workspace-permissions' | ||
| import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' | ||
| import { isHosted } from '@/lib/core/config/env-flags' | ||
| import { isSafeHttpUrl } from '@/lib/core/utils/urls' | ||
| import { | ||
| resolveOAuthServiceForSlug, | ||
|
|
@@ -1052,9 +1055,16 @@ function UsageUpgradeDisplay({ data }: { data: UsageUpgradeTagData }) { | |
| const { data: session } = useSession() | ||
| const hostContext = useWorkspaceHostContext() | ||
| const { getSettingsHref } = useSettingsNavigation() | ||
| const settingsPath = getSettingsHref({ section: 'billing' }) | ||
| const buttonLabel = data.action === 'upgrade_plan' ? 'Upgrade Plan' : 'Increase Limit' | ||
| const canManageBilling = canManageWorkspaceBilling(hostContext, session?.user?.id) | ||
|
|
||
| // Self-hosted plan and limit both live on the hosted account, so local | ||
| // workspace billing roles say nothing about who may change them. | ||
| const href = isHosted | ||
| ? getSettingsHref({ section: 'billing' }) | ||
| : data.action === 'upgrade_plan' | ||
| ? buildHostedUpgradeUrl() | ||
| : HOSTED_BILLING_SETTINGS_URL | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Billing CTA drops login destinationMedium Severity Self-hosted Additional Locations (1)Reviewed by Cursor Bugbot for commit e1ac4a8. Configure here. |
||
| const canManageBilling = !isHosted || canManageWorkspaceBilling(hostContext, session?.user?.id) | ||
| const unavailableMessage = hostContext.hostOrganizationId | ||
| ? 'Contact an organization admin to manage this workspace’s usage limits.' | ||
| : 'Only the workspace owner can manage this workspace’s usage limits.' | ||
|
|
@@ -1086,11 +1096,14 @@ function UsageUpgradeDisplay({ data }: { data: UsageUpgradeTagData }) { | |
| </p> | ||
| {canManageBilling ? ( | ||
| <a | ||
| href={settingsPath} | ||
| href={href} | ||
| target={isHosted ? undefined : '_blank'} | ||
| rel={isHosted ? undefined : 'noopener noreferrer'} | ||
| aria-label={isHosted ? undefined : `${buttonLabel} (opens in a new tab)`} | ||
| className='mt-2 inline-flex items-center gap-1 font-[500] text-amber-700 text-small underline decoration-dashed underline-offset-2 transition-colors hover-hover:text-amber-900 dark:text-amber-300 dark:hover-hover:text-amber-200' | ||
| > | ||
| {buttonLabel} | ||
| <ArrowRight className='size-3' /> | ||
| {isHosted ? <ArrowRight className='size-3' /> : <SquareArrowUpRight className='size-3' />} | ||
| </a> | ||
| ) : ( | ||
| <p className='mt-2 font-[500] text-amber-700 text-small dark:text-amber-300'> | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On self-hosted, the upgrade_plan CTA calls
buildHostedUpgradeUrl()withoutdata.reason, so the hosted/upgradepage never receives the reason query used for limit-specific header copy; other upgrade paths forward reason throughbuildUpgradeHref/buildHostedUpgradeUrl.