diff --git a/src/renderer/src/components/orca-cloud/cloud-wake-billing.ts b/src/renderer/src/components/orca-cloud/cloud-wake-billing.ts index a4c27c84761..2738dd7d4df 100644 --- a/src/renderer/src/components/orca-cloud/cloud-wake-billing.ts +++ b/src/renderer/src/components/orca-cloud/cloud-wake-billing.ts @@ -42,7 +42,7 @@ export function cloudWakeBillingCopy( // Running limits and anything unknown: the shared workspace copy (it says what to do). return { body: describeError('entitlement_denied', reason, t), fix: null } } - const copy = blockerCopy(kind, t) + const copy = blockerCopy(kind, t, reason) // The sheet's payment line is about new sandboxes; this one already exists. const body = kind === 'payment' diff --git a/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.test.tsx b/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.test.tsx index 091313ac165..67018051a4a 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.test.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.test.tsx @@ -38,7 +38,17 @@ afterEach(() => { function renderBlocker( kind: BillingBlockKind, - { canManageBilling = true, capCents = 2500, offers = true } = {} + { + canManageBilling = true, + capCents = 2500, + offers = true, + reason + }: { + canManageBilling?: boolean + capCents?: number + offers?: boolean + reason?: string | null + } = {} ): { onContinue: ReturnType onBackToDraft: ReturnType @@ -53,7 +63,8 @@ function renderBlocker( entitlement={{ ...sampleTeam, offers: offers ? sampleTeam.offers : null, - overage: { ...sampleTeam.overage, spendCapCents: capCents } + overage: { ...sampleTeam.overage, spendCapCents: capCents }, + ...(reason !== undefined ? { reason: reason as never } : {}) }} onContinue={onContinue} onBackToDraft={onBackToDraft} @@ -147,6 +158,20 @@ describe('billing blocker on the creating sheet (JA-21)', () => { expect(openPlansPage).toHaveBeenCalledWith('sandbox') }) + it.each([ + ['subscription_inactive', 'Your team’s plan has ended.'], + ['trial_used_up', 'Your team’s trial is used up.'], + ['no_subscription', 'Your team doesn’t have a plan yet.'], + ['trial_unavailable', 'Your team doesn’t have a plan yet.'], + [null, 'Your team needs a plan to use Orca Cloud.'] + ] as const)('no plan (%s): only a plan that existed has "ended" (EP-11)', (reason, body) => { + renderBlocker('plan', { reason }) + expect(screen.getByText(body)).toBeTruthy() + if (reason !== 'subscription_inactive') { + expect(screen.queryByText('Your team’s plan has ended.')).toBeNull() + } + }) + it.each([ ['hoursUsed', 'Ask an owner or admin to add hours.'], ['spendCap', 'Ask an owner or admin to raise the spend cap.'], diff --git a/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.tsx b/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.tsx index b7eb6b6f8e7..69defef12e6 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildBillingBlocker.tsx @@ -22,20 +22,42 @@ export type BillingBlockerPhase = /** A fix in the browser (portal): re-check on return, or with Check again. */ | { kind: 'browser'; checking: boolean; stillBlocked: boolean } +/** Why there is no plan, in billing's words: only a plan that existed has "ended". */ +function planBlockBody(reason: string | null | undefined, t: Translate): string { + switch (reason) { + case 'subscription_inactive': + return t('worktreeCreation.billingBlock.plan.body2', 'Your team’s plan has ended.') + case 'trial_used_up': + return t( + 'worktreeCreation.billingBlock.plan.bodyTrialUsedUp', + 'Your team’s trial is used up.' + ) + case 'no_subscription': + case 'trial_unavailable': + return t( + 'worktreeCreation.billingBlock.plan.bodyNoPlan', + 'Your team doesn’t have a plan yet.' + ) + default: + return t( + 'worktreeCreation.billingBlock.plan.bodyNeedsPlan', + 'Your team needs a plan to use Orca Cloud.' + ) + } +} + /** The blocker's words per refusal; also the waking card's, so a refused wake says the same. */ export function blockerCopy( kind: BillingBlockKind, - t: Translate + t: Translate, + /** Billing's refusal reason: says whether a plan ended, a trial ran out, or there never was one (EP-11). */ + reason?: string | null ): { title: string; body: string; ask: string } { switch (kind) { case 'plan': return { title: t('worktreeCreation.billingBlock.plan.title', 'Choose a plan'), - body: t( - // Only teams that had Orca Cloud reach the sheet; a never-planned team gets setup. - 'worktreeCreation.billingBlock.plan.body2', - 'Your team’s plan has ended.' - ), + body: planBlockBody(reason, t), ask: t('worktreeCreation.billingBlock.plan.ask', 'Ask an owner or admin to choose a plan.') } case 'hoursUsed': @@ -73,6 +95,8 @@ export function blockerCopy( export type VmBuildBillingBlockerViewProps = { kind: BillingBlockKind + /** Billing's refusal reason (plan blocks word "ended" vs "no plan yet" from it). */ + reason?: string | null canManageBilling: boolean phase: BillingBlockerPhase /** Spend cap reached: the field's starting value, in whole dollars. */ @@ -93,6 +117,7 @@ export type VmBuildBillingBlockerViewProps = { */ export function VmBuildBillingBlockerView({ kind, + reason, canManageBilling, phase, suggestedCapDollars, @@ -106,7 +131,7 @@ export function VmBuildBillingBlockerView({ }: VmBuildBillingBlockerViewProps): React.JSX.Element { const t = useTranslate() const locale = useIntlLocale() - const copy = blockerCopy(kind, t) + const copy = blockerCopy(kind, t, reason) const capId = React.useId() const [cap, setCap] = React.useState(String(suggestedCapDollars)) const headingRef = React.useRef(null) @@ -340,6 +365,7 @@ export function VmBuildBillingBlocker({ return (