-
Notifications
You must be signed in to change notification settings - Fork 673
feat(billing): disclose that changing plans on a cancelled subscription resumes it #14222
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f7ec3dd
feat: add reactivation disclosure banner for plan changes from cancel…
wei-hai 1331c71
fix: derive confirm_reactivation from the disclosure banner, not subs…
wei-hai b3e4e65
test(billing): drop tracker id from regression-guard comments
wei-hai c2df311
fix: prevent data loss and thread reactivation consent through downgr…
wei-hai ed8543c
refactor(billing): keep the reactivation-consent error module-private
wei-hai 801284e
fix(billing): refuse to bill an amount the user did not confirm
wei-hai b11127f
test(storybook): add reactivation-disclosure states for SubscriptionT…
wei-hai 644036e
fix(billing): align reactivation banner neutrals with sibling banner …
wei-hai 1b6d16c
fix(billing): refresh subscription status before the reactivation guard
wei-hai 91af8f8
fix(billing): bind reactivation consent to the amount actually shown
wei-hai 8a55d23
fix(billing): correct reactivation-preview date rollover and readines…
wei-hai 86f45c4
fix(billing): refresh the downgrade dialog after reactivation validat…
wei-hai 6c37dc6
fix(billing): close two reactivation gaps in the subscription checkou…
wei-hai 28d4d9b
fix(billing): close five retry-loop and consent-drift regressions in …
wei-hai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
224 changes: 224 additions & 0 deletions
224
src/platform/workspace/components/SubscriptionTransitionPreviewWorkspace.stories.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| import type { Meta, StoryObj } from '@storybook/vue3-vite' | ||
|
|
||
| import type { SubscriptionInfo } from '@/composables/billing/types' | ||
| import { i18n } from '@/i18n' | ||
| import type { PreviewSubscribeResponse } from '@/platform/workspace/api/workspaceApi' | ||
| import { setBillingContextMock } from '@/storybook/mocks/useBillingContext' | ||
|
|
||
| import SubscriptionTransitionPreviewWorkspace from './SubscriptionTransitionPreviewWorkspace.vue' | ||
|
|
||
| type PreviewPlanInfo = PreviewSubscribeResponse['new_plan'] | ||
|
|
||
| /** | ||
| * The reactivation-disclosure banner on the single-plan change preview: a | ||
| * cancelled-but-not-lapsed subscription resumes silently on any plan change, | ||
| * so the banner discloses that plus the exact charge (and, above the current | ||
| * plan's monthly total, a consent checkbox gating the confirm button). The | ||
| * banner reads `subscription`/`isInitialized` from `useBillingContext`, not | ||
| * from `previewData`, so each story drives it through the Storybook stub | ||
| * (`setBillingContextMock`) rather than props. | ||
| */ | ||
| const meta: Meta<typeof SubscriptionTransitionPreviewWorkspace> = { | ||
| title: 'Components/SubscriptionTransitionPreviewWorkspace', | ||
| component: SubscriptionTransitionPreviewWorkspace, | ||
| tags: ['autodocs'], | ||
| parameters: { layout: 'centered' }, | ||
| decorators: [ | ||
| (story) => ({ | ||
| components: { story }, | ||
| template: | ||
| '<div class="mx-auto flex h-[680px] w-[460px] flex-col rounded-2xl border border-border-default bg-secondary-background p-12"><story /></div>' | ||
| }) | ||
| ] | ||
| } | ||
|
|
||
| export default meta | ||
| type Story = StoryObj<typeof meta> | ||
|
|
||
| const TODAY = '2026-08-01T00:00:00Z' | ||
| // The date the pre-existing cancellation was set to lapse; still active until | ||
| // then, which is what makes a plan change on this subscription a reactivation. | ||
| const CANCEL_DATE = '2026-08-25T00:00:00Z' | ||
| const NEXT_MONTHLY_RENEWAL = '2026-09-01T00:00:00Z' | ||
| const NEXT_ANNUAL_RENEWAL = '2027-08-01T00:00:00Z' | ||
| const NEXT_MONTHLY_AFTER_CANCEL = '2026-09-25T00:00:00Z' | ||
|
|
||
| function plan( | ||
| tier: PreviewPlanInfo['tier'], | ||
| duration: PreviewPlanInfo['duration'], | ||
| priceCents: number, | ||
| periodEnd: string | ||
| ): PreviewPlanInfo { | ||
| return { | ||
| slug: `${tier.toLowerCase()}-${duration.toLowerCase()}`, | ||
| tier, | ||
| duration, | ||
| price_cents: priceCents, | ||
| credits_cents: 0, | ||
| seat_summary: { | ||
| seat_count: 1, | ||
| total_cost_cents: priceCents, | ||
| total_credits_cents: 0 | ||
| }, | ||
| period_end: periodEnd | ||
| } | ||
| } | ||
|
|
||
| function cancelledSubscription( | ||
| tier: NonNullable<SubscriptionInfo['tier']>, | ||
| duration: NonNullable<SubscriptionInfo['duration']> = 'MONTHLY' | ||
| ): SubscriptionInfo { | ||
| return { | ||
| isActive: true, | ||
| tier, | ||
| duration, | ||
| planSlug: `${tier.toLowerCase()}-${duration.toLowerCase()}`, | ||
| renewalDate: null, | ||
| endDate: CANCEL_DATE, | ||
| isCancelled: true, | ||
| hasFunds: true | ||
| } | ||
| } | ||
|
|
||
| const notCancelledSubscription: SubscriptionInfo = { | ||
| isActive: true, | ||
| tier: 'STANDARD', | ||
| duration: 'MONTHLY', | ||
| planSlug: 'standard-monthly', | ||
| renewalDate: NEXT_MONTHLY_RENEWAL, | ||
| endDate: null, | ||
| isCancelled: false, | ||
| hasFunds: true | ||
| } | ||
|
|
||
| function story( | ||
| previewData: PreviewSubscribeResponse, | ||
| subscription: SubscriptionInfo | ||
| ): Story { | ||
| return { | ||
| args: { previewData }, | ||
| beforeEach() { | ||
| // Dates render through both a local Intl formatter and vue-i18n's n(), | ||
| // so pin the locale rather than inherit the developer's. | ||
| i18n.global.locale.value = 'en' | ||
| setBillingContextMock({ subscription }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Ordinary upgrade, subscription not cancelled — the reactivation banner | ||
| * must not leak into a normal plan change. | ||
| */ | ||
| export const NotCancelled: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'upgrade', | ||
| effective_at: TODAY, | ||
| is_immediate: true, | ||
| cost_today_cents: 1500, | ||
| cost_next_period_cents: 3500, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('STANDARD', 'MONTHLY', 2000, NEXT_MONTHLY_RENEWAL), | ||
| new_plan: plan('CREATOR', 'MONTHLY', 3500, NEXT_MONTHLY_RENEWAL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| notCancelledSubscription | ||
| ) | ||
|
|
||
| /** | ||
| * Cancelled, immediate upgrade — exact-cents charge ($54.54), guarding the | ||
| * regression where this path rounded the charge shown to the user. | ||
| */ | ||
| export const ReactivatingUpgrade: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'upgrade', | ||
| effective_at: TODAY, | ||
| is_immediate: true, | ||
| cost_today_cents: 5454, | ||
| cost_next_period_cents: 10_000, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('CREATOR', 'MONTHLY', 3500, CANCEL_DATE), | ||
| new_plan: plan('PRO', 'MONTHLY', 10_000, NEXT_MONTHLY_RENEWAL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| cancelledSubscription('CREATOR') | ||
| ) | ||
|
|
||
| /** | ||
| * Cancelled, scheduled downgrade — $0 today. The most important state: no | ||
| * money moves, so the copy alone must make the reactivation unmissable. | ||
| */ | ||
| export const ReactivatingDowngrade: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'downgrade', | ||
| effective_at: CANCEL_DATE, | ||
| is_immediate: false, | ||
| cost_today_cents: 0, | ||
| cost_next_period_cents: 3500, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('PRO', 'MONTHLY', 10_000, CANCEL_DATE), | ||
| new_plan: plan('CREATOR', 'MONTHLY', 3500, NEXT_MONTHLY_AFTER_CANCEL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| cancelledSubscription('PRO') | ||
| ) | ||
|
|
||
| /** Cancelled, monthly-to-annual — the full year is billed today. */ | ||
| export const ReactivatingMonthlyToAnnual: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'duration_change', | ||
| effective_at: TODAY, | ||
| is_immediate: true, | ||
| cost_today_cents: 33_600, | ||
| cost_next_period_cents: 33_600, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('CREATOR', 'MONTHLY', 3500, CANCEL_DATE), | ||
| new_plan: plan('CREATOR', 'ANNUAL', 33_600, NEXT_ANNUAL_RENEWAL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| cancelledSubscription('CREATOR') | ||
| ) | ||
|
|
||
| /** | ||
| * Cancelled, annual-to-monthly — the other cadence direction, which used to | ||
| * wrongly show the annual copy and now has its own wording. | ||
| */ | ||
| export const ReactivatingAnnualToMonthly: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'duration_change', | ||
| effective_at: TODAY, | ||
| is_immediate: true, | ||
| cost_today_cents: 1234, | ||
| cost_next_period_cents: 3500, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('CREATOR', 'ANNUAL', 33_600, CANCEL_DATE), | ||
| new_plan: plan('CREATOR', 'MONTHLY', 3500, NEXT_MONTHLY_RENEWAL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| cancelledSubscription('CREATOR', 'ANNUAL') | ||
| ) | ||
|
|
||
| /** | ||
| * Cancelled, charge above the current plan's monthly total — the consent | ||
| * checkbox renders and the confirm button stays disabled until it's ticked. | ||
| */ | ||
| export const ReactivatingAboveThreshold: Story = story( | ||
| { | ||
| allowed: true, | ||
| transition_type: 'upgrade', | ||
| effective_at: TODAY, | ||
| is_immediate: true, | ||
| cost_today_cents: 8000, | ||
| cost_next_period_cents: 10_000, | ||
| credits_today_cents: 0, | ||
| credits_next_period_cents: 0, | ||
| current_plan: plan('STANDARD', 'MONTHLY', 2000, CANCEL_DATE), | ||
| new_plan: plan('PRO', 'MONTHLY', 10_000, NEXT_MONTHLY_RENEWAL) | ||
| } satisfies PreviewSubscribeResponse, | ||
| cancelledSubscription('STANDARD') | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.