[DO NOT MERGE] test(billing): FE-991 combined integration preview of Billing Rework V1 - #12792
Closed
dante01yoon wants to merge 226 commits into
Closed
Conversation
…tatus fields Add resubscribe(), topup(amountCents) and billingStatus/subscriptionStatus/ tier/renewalDate to the shared BillingContext so components stop bypassing useBillingContext with raw workspaceApi calls (B2 of the billing-divergence convergence plan, FE-904). - types.ts: extend BillingActions + BillingState; export BillingStatus, BillingSubscriptionStatus, CreateTopupResponse from workspaceApi - useWorkspaceBilling: real wiring (workspaceApi.resubscribe/createTopup, surface statusData fields) - useLegacyBilling: legacy equivalents (resubscribe = fresh checkout; topup converts cents -> dollars via purchaseCredits; billing_status = null) - useBillingContext: proxy the new members - migrate orphaned callers (SubscriptionPanelContentWorkspace, TopUpCreditsDialogContentWorkspace, useSubscriptionCheckout) onto the facade Facade standardizes topup on cents; the legacy adapter converts.
…ance fetch The completed-topup balance refresh is already owned by the caller (TopUpCreditsDialogContentWorkspace); refreshing inside the facade too caused a redundant second fetch. Keep topup a thin pass-through that returns the CreateTopupResponse so the caller orchestrates completed/pending.
…-resubscribe-topup-status
Standalone presentational slider for the team-plan credit subscription. It
snaps to the 5 fixed DES-197 stops (200/400/700/1400/2500 USD) by driving the
shared reka-ui Slider in index space (min=0, max=4, step=1) — the user can
never land on a value in between. v-model carries the selected USD value; a
`change` event also emits { index, usd, credits }.
Thresholds live in a typed constant (teamPlanCreditStops.ts) hardcoded per
Figma DES-197 until the backend slider contract lands; a test guards that the
credit figures stay equal to usdToCredits(usd).
B4 standalone slice (FE-935). Not yet wired into PricingTableWorkspace
(deferred to FE-934, blocked on the BE slider contract).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lint-and-format failed on @intlify/vue-i18n/no-raw-text for the hardcoded '/ month' and 'credits' template strings. Route both through vue-i18n (subscription.perMonth, credits.credits — same key UserCredit.vue uses) and inject an i18n instance in the component test. Also normalize tailwind class order flagged by better-tailwindcss.
…s, yearly total) Bring the team-plan slider up to the settled DES-197 / Slack design: - discounted monthly price + struck pre-discount price + Save% badge - '$N billed yearly' annual total - stop labels now show CREDITS (42.2K..527.5K) with a credit icon and a gold highlight on the selected stop (was raw USD amounts) - add threshold-based yearly discounts to TEAM_PLAN_CREDIT_STOPS (0/5/10/15/20%, $700=10% per design; others per the agreed sequence, TBD) - USD / mo unit, drop the separate credits readout (credits live on the ticks)
The billing facade refactor moved resubscribe() onto useBillingContext, but useSubscriptionCheckout's test still stubbed it on workspaceApi, which the composable no longer calls. resubscribe was undefined at runtime, so handleResubscribe threw "resubscribe is not a function" -- failing the "emits close on success" and "shows error toast on failure" cases. Wire mockResubscribe into the useBillingContext mock where the composable reads it. The workspaceApi mock stays to shield the test from the real @/scripts/api -> app.ts import chain.
…esEnabled B4 (FE-934): a single pricing table for the Jun-5 model — one workspace with a personal/team PLAN toggle (Gamma-style), per DES-197. New, flag-gated component that will replace PricingTable.vue + PricingTableWorkspace.vue at cutover (strangler). - UnifiedPricingTable.vue: plan toggle, personal tier cards (facade plans with TIER_PRICING fallback), team column hosting CreditSlider, Enterprise card. Reuses useBillingContext; emits subscribe/resubscribe (personal) + subscribeTeam (team). - SubscriptionRequiredDialogContentUnified.vue: host wiring the table to useSubscriptionCheckout for personal checkout; team checkout stubbed (toast) pending the BE slider contract (doc Open Q#2). - showPricingTable: render the unified host when teamWorkspacesEnabled; the legacy PricingTable stays for the flag-off build. - i18n: subscription.planScope / teamPlan / enterprise keys. Stacked on the FE-935 CreditSlider branch. Personal checkout fully wired; team checkout + live /api/billing/plans discount data deferred to the BE contract. vue-tsc + oxlint clean; dispatcher tests pass (11).
Adds Personal / TeamPlan stories so the new table renders on the deployed Storybook preview. initialPlanMode (default personal) lets the story show the team view without the server flag (the toggle itself is flag-gated).
The real facade pulls in Firebase auth via the legacy adapter and crashes in Storybook (TypeError: setPersistence). Alias it to a static stub so the new table renders against TIER_PRICING / DES-197 fallbacks.
…imate count - Save badge -> outlined primary pill, right-aligned (border-2 border-primary-background, text-primary-background, rounded-full, text-sm bold); bump price to 32px, per Figma 2983:29834 - Count the price up/down between stops via @vueuse useTransition (easeOutCubic, 350ms); honor prefers-reduced-motion - Derive billed-yearly from the displayed monthly so it stays exactly 12x the shown price even mid-count
… when narrow - The price (amount + struck + "USD / mo") is now an unbreakable unit (shrink-0 + whitespace-nowrap); the Save badge wraps to its own line on a narrow card instead of breaking "USD / mo" mid-word at the widest stop - Story previews at the real DES-197 width: 512px card column with 32px padding -> 448px content
…opIndex) - Accept `stops` + `defaultStopIndex` props (default to the hardcoded DES-197 set) so the slider can be fed from GET /api/billing/plans (BE-1254) without code changes; selectedIndex falls back to defaultStopIndex - Add a prop-driven unit test and a "Backend-driven stops" story that maps the team_credit_stops API shape into the props
Guard selectedIndex so a shorter-than-expected backend `stops` array (or an out-of-bounds `defaultStopIndex`) can't make `current` undefined and crash the price computeds; floor `lastIndex` at 0 so the slider max is never negative. `stops` is documented as required non-empty. Add a unit test asserting no update:modelValue / change events fire when the slider is disabled. Addresses CodeRabbit review on #12644 (undefined-`current` runtime path; disabled-state test coverage).
…34-unified-pricing-table
- personal cards: replace uniform attribute matrix with progressive
"What's included:" / "Everything in {prev}, plus:" bullets + credit block
(monthly credits + "Generates ~N 5s videos*"); drop dead per-card popover
- unify monthly-credits / video-estimate / "Everything in {plan}" into shared
subscription.* i18n keys reused by both personal and team
- team Details checks use foreground color, "Invite team members" copy
- billing-cycle badge "-20%" -> "Save 20%"
- Creator CTA to inverted (white) to match DES-197 and legacy PricingTable
- pricing dialog shell uses secondary-background (elevated) so the base-background cover panel + cards read as the darker well per DES-197; drop the translucent base/60 + backdrop-blur - cover panel gets solid bg-base-background - personal tier price unit "USD / mo / member" -> "USD / mo" (matches DES-197 and the team slider; fixes awkward wrap on mobile)
The workspace adapter feeds subscription.renewalDate/endDate straight from the API (ISO 8601), but the legacy adapter passed pre-formatted display strings, so facade consumers like CreditsTile got different shapes per adapter. Source both from subscriptionStatus and leave formatting to the display site.
- useWorkspaceBilling: resubscribe refreshes status+balance and surfaces errors; topup stays a pass-through (no double-fetch) and surfaces errors - useBillingContext: legacy topup converts cents to whole dollars - types: trim convenience-accessor JSDoc, note whole-dollar-cents contract
Consolidate the divergent credits surfaces into a single CreditsTile that always reads the useBillingContext facade, matching DES-247: - New CreditsTile.vue: total + remaining, monthly/additional breakdown with a stacked progress bar, refresh, and permission-gated add-credits. Owns the post-checkout (focus / pending_topup) balance refresh. - Extract the duplicated credits card out of SubscriptionPanelContentWorkspace and SubscriptionPanelContentLegacy onto the shared tile. - Replace LegacyCreditsPanel (authStore.balance) with CreditsPanel.vue routed through the tile; repoint useSettingUI; delete LegacyCreditsPanel. - creditsProgress helper + numeric getters on useSubscriptionCredits. - i18n keys for the unified tile labels.
Drives a raw page with mocked Firebase auth + stubbed boot endpoints so the cloud app initializes without a backend, then asserts the DES-247 tile renders its total/progress/monthly+additional breakdown/add-credits in Settings ▸ Workspace ▸ Plan & Credits. Bypasses the comfyPage fixture (which requires the OSS devtools backend).
Match the finalized DES-247 credits tile: single monthly-usage bar (gold,
fills with the consumed portion) with a Monthly / Refills header and
"{used} used" + "{remaining} left of {total}" labels, a divider, and a single
Additional-credits row with a credit icon and "Used after monthly runs out"
subtitle. Drops the two-segment bar and colored dots; updates i18n + the
unit/e2e assertions accordingly.
Container-query the tile: narrow widths drop the used/remaining labels and breakdown subtitle, stack the additional-credits value, and compact the monthly summary numbers (105K left of 200K). Align the additional credits tooltip copy with the updated design.
…acade The subscribe-clicked telemetry `current_tier` (SubscribeButton) and the PostHog `subscription_tier` person property read the legacy useSubscription store, which is empty/stale for team workspaces — so telemetry and analytics record the wrong tier for team users today. Source the tier from useBillingContext().tier (added in B2) so it is workspace-aware. B3 of the billing convergence plan (FE-933). Stacked on FE-904 (B2).
useSubscriptionActions.handleRefresh and the SettingDialog credits-nav refresh called the legacy authActions.fetchBalance (/customers only), so team-workspace balances were never refreshed. Route both through useBillingContext().fetchBalance, which is workspace-aware. Part of B3 (FE-933).
The next-refresh line read formattedRenewalDate from the legacy useSubscription store, which is empty for team workspaces, so the date silently disappeared for team users (T5). Read the facade's raw ISO renewalDate and format at the display site with the same en-US format. Part of B3 (FE-933).
The personal-workspace avatar popover read tier/balance state from the legacy useSubscription store and authStore directly. Source it all from useBillingContext (tier, subscription, balance, isLoading, fetchStatus, fetchBalance) and derive the tier badge via useWorkspaceTierLabel instead of duplicating the tier-name mapping. Part of B3 (FE-933).
Current-plan highlight and upgrade-vs-subscribe branching read the legacy useSubscription store. Take isActiveSubscription/isFreeTier/tier from useBillingContext and derive isYearlySubscription from the facade subscription duration, matching PricingTableWorkspace. The billing portal flow (accessBillingPortal deep-links + proration) stays as is — facade manageSubscription is not behavior-identical. Part of B3 (FE-933).
The cloud CI project runs a production build, which ignores the dev-only ff: localStorage override — the Workspace settings panel never appeared and the spec timed out on every run. Resolve the flag the way production does: mock /api/features (the remote-config source) with team_workspaces_enabled instead.
Route all same-origin /api, websocket, and bootstrap /api/features calls to https://pr-4393.testenvs.comfy.org so the labeled preview build exercises that ephemeral backend. Throwaway, scoped to this DO NOT MERGE integration PR.
…E-934)
Members upsell 'Upgrade to Team' was re-wired to showSubscriptionDialog()
during the FE-768 members merge, dropping the FE-934 team-tab fix on this
integration branch. Restore showTeamPlans() -> show({ planMode: 'team' }).
…E-991 integration
…total from current stop (FE-964)
…ncelled re-subscribes (FE-934)
…), drop slider stop disable (FE-934)
A cancelled-but-still-active personal subscription renders both "Add credits" and "Resubscribe" in the credits row. With a wide (7-digit) balance the second button overflowed the fixed-width w-80 popover. Make the popover width fluid (w-fit, clamped min-w-80..max-w-96) so it stays 320px in the common single-action case and only grows (to ~370px) to fit both buttons. Adds a @cloud e2e that asserts the Resubscribe button stays within the popover bounds.
…n, not Resubscribe (FE-934)
…be (FE-934) A Stripe-backed subscribe (needs_payment_method / pending_payment) only fired the billing op poll; on completion billingOperationStore.handleSuccess closed the pricing dialog and opened settings, so "You're all set" never showed. Team subscribes were also gated out of the success step by a selectedTierKey-only guard. - await the billing op in the async checkout branches and advance to the success step instead of tearing the dialog down - leave the subscription dialog open in handleSuccess (only top-ups close + surface settings); keep the success toast - render the success step for team checkouts, sourcing the name, price and credits from the selected slider stop - drive the team success invite block off the flat MAX_WORKSPACE_MEMBERS cap
- give the email input a persistent tertiary-background fill so it reads as a field at rest, not only on focus (Figma 3084-6116) - add a showSubmit prop to InviteMembersForm and expose submit/canSubmit/loading so the success screen places "Send invites" in the footer next to Close, not glued under the input (Figma 3698-10316)
Port of the FE-768 Plan & Credits redesign onto the integration base, using canManageSubscriptionLifecycle for the original-owner gate. - ... menu per role: original owner = Edit / Cancel plan / Delete; promoted owner = Edit / Leave; member = Leave; personal = Edit, plus Cancel plan + Delete once on a paid plan. - Cancel/Delete are creator-or-personal-owner only; the creator can't leave; Delete is locked while a team subscription is active. - Render the menu via the design-system DropdownMenu (dividers, danger Delete) to match Figma; cancelled state shows Manage billing + Reactivate plan; personal Free shows Subscribe, both with the menu.
…reate/switch (FE-991) A team plan can't be subscribed in place from a personal workspace, so a personal user choosing a team plan in the unified pricing table is routed to the Team Workspaces create/switch dialog instead of advancing into team checkout. Gate on isInPersonalWorkspace; team workspaces keep the normal flow.
…ace (FE-991) The new container gate test could fail on a cold-cache first run (onChooseTeam observed 0 calls) due to a userEvent/happy-dom event-dispatch warm-up race; the gate source is synchronous and correct. Wrap the positive assertions in vi.waitFor so the emit lands deterministically.
A team plan's credit stop can't be changed, so moving the slider off the active stop now disables the CTA with "Credits currently can't be changed" instead of an enabled "Change plan" — for both active and cancelled subscriptions. The only actionable subscribed state remains re-subscribing on the current stop. Drops the now-unused teamPlan.changePlan key.
…icing table (FE-934) Existing per-member (legacy) team subscribers (hyphenated team- plan slug, no team_credit_stop) keep the old tier-based team pricing table instead of the new unified credit-slider table. Detection lives in the billing facade (isLegacyTeamPlan); routing forks in showPricingTable before the unified branch. Restores modal:false for the legacy dialog's body-teleported popover and adds the missing success step to the resurrected legacy dialog.
…(FE-934) Single classification of the active subscription (none/personal/new-team/ legacy-team) and the transitions it allows, so the pricing table consumes one fact instead of re-deriving plan-vs-plan locks. new-team is keyed on the credit stop (there is no 'team' SubscriptionTier); FREE and no-subscription collapse to 'none'; a cancelled-but-still-active plan keeps its type until billing ends.
…le (FE-934) A new-team subscriber can't switch to a personal tier, and a team workspace on a personal plan can't switch to team in place — both now disable the CTA with a context-specific message driven by the facade subscriptionLock/planType. A personal workspace keeps routing to create/switch a team workspace.
…934) Yearly/monthly at the same credit stop is rejected by the BE (TRANSITION_NOT_ALLOWED), so the off-cycle CTA now disables with "Team plan currently can't be changed" instead of reading "Current plan".
…te (FE-934) Carry the unified pricing-table rollback (no credit-stop / billing-cycle / cross-plan locks, facade planType/subscriptionLock removed) onto the team-subscribe stack, and let a personal workspace subscribe to a team plan directly instead of rerouting to create/switch a workspace. The legacy team table keeps its existing "choose team" flow.
Brings the single strip-before-await URL-cleanup refactor into the FE-991 integration branch so the deep link matches the FE-1104 PR for preview testing.
13 tasks
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
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Why
Billing Rework V1 is split across two axes that converge at runtime — the UI (run button, credits tile, pricing, settings) consumes the API facade (billing status / balance / plans). They cannot be validated in isolation. This branch is the single point where both axes are combined behind the
team_workspaces_enabledflag, so the full user flow can be exercised and missing/broken delegation boundaries surface before BE delivery.How the two axes combine
Base is FE-904 (not
main) because the UI axis (notably the credits tile) consumes the facade additions in #12622; the facade must be the substrate.Test-target PRs included in this build
API axis (data substrate)
hasFunds)UI axis (screens / flows)
All 12 composed; 0 skipped. One merge resolution (FE-932 vs FE-934 destructure in
TopUpCreditsDialogContentWorkspace.vue, unioned) + the documentedvoid startOperation('topup')fix once FE-932 + FE-970 both land.Status
pnpm typecheck(vue-tsc): green, 0 errors. 113 files, +8574 / −2243 vsmain.mockCloudBilling(page)helper + the new@cloudspecs covering the FE-991 scenario matrix. Thepreview/preview-gpubuild lets the combined flows be walked manually in the meantime (toggleteam_workspaces_enabled).Known limitation
Personal checkout confirm / success is not walkable even here — gated by the B1 code fork (
useBillingContext.tsisInPersonalWorkspace→useLegacyBillingplans = []), removed only in FE-966. Those two screens stay covered by Storybook (done in FE-934); this build covers the team path + render / gating / settings / members / switcher.Relates FE-991. Per-feature review happens on the individual PRs above — do not review or merge this branch.
Refresh — 2026-06-12 (rebuilt
02ab2c126b..4b622ec998)Re-synced this preview to the latest feature tips so the combined walk reflects current code:
main(squashafd42525fe) — the facade substrate is inmain; this branch keeps its proven base and stacks the rest on top.853c1164ba, DES-394): inline "Invite your team" block on the team-upgrade success step (sharedInviteMembersForm.vue, seat cap fromgetMaxSeats(tier),trackWorkspaceInviteSenttelemetry,TeamSuccessWithInviteStorybook story). Not yet a standalone PR; carried here for the combined walk. Live team success is BE-gated (BE-1254) -> verify primarily via Storybook.One merge conflict resolved:
src/storybook/mocks/useBillingContext.ts— the integration'sBillingContext(extended by FE-964) requiresbillingStatus/subscriptionStatus/tier/renewalDate/resubscribe/topup+getMaxSeats; the mock now satisfies the full interface. Project type-check is green at HEAD.Refresh — 2026-06-19 (B1 personal routing + billing history + remaining merges)
Added the remaining existing Billing Rework V1 PRs and re-synced the convergence levers so the combined walk reflects current code.
Newly included
/api/billing/*(same as team) behind the newpersonal_workspace_billing_readyserver flag.useBillingContext.tstype:isInPersonalWorkspace && !personalWorkspaceBillingReady → legacy, elseworkspace.useLegacyBilling → workspaceApidelegation was dropped (redundant once FE-966 flips personal touseWorkspaceBilling).GET /api/billing/eventsbehindteam_workspaces_enabled(retires thecustomerEventsServicepath).useWorkspaceBilling.subscribeusesPromise.allSettledso a successful subscribe survives a failed post-write status/balance refresh (soft error, not reject).?pricing=1/?pricing=team/?pricing=personalopens the unified pricing table on app load (optionally on a preselected plan tab), original-owner gated viacanManageSubscriptionLifecycle; member/promoted-owner is a silent no-op + param-strip. Stacked on FE-934 (#12666) + FE-770 (#12829) — both already present here. Post-Billing-V1 follow-up.Re-synced tips: FE-934 #12666 (team Yearly/Monthly toggle + pricing-tab behavior), FE-878 #12785 (route the 402 queue paywall to the modal +
subscriptionPaywallError.spec.ts).FE-768 plan-credits tip (#12761) — now synced (was blocked on the cancel-permission model). The Billing V1 decision is confirmed (Slack thread + Notion "Slack Decisions & Open Questions" billing-permissions matrix): cancel and resubscribe are original-owner-only; other owners and members can leave the workspace. The merge reconciles the panel to that union — cancel/resubscribe gated by
canManageSubscriptionLifecycle(FE-770), Leave-workspace menu item from #12761 (creator sees it disabled, others enabled), free-tier "10 min" runtime copy. Leave-disabled now reads the canonical storeisCurrentUserOriginalOwnerinstead of the stale joinDate heuristic, so it agrees with the cancel gate.WorkspacePanelContent.vuekept at HEAD (FE-768 members/FE-769 superseded #12761's in-header invite/menu layout). Panel test reconciled to the creator-only model (14 pass). Note: #12761's own branch still carries the any-owner model + joinDate heuristic — fold the same reconciliation in when it rebases onto FE-770.Still pending
pnpm typecheck(vue-tsc): green, 0 errors at HEAD.Refresh — 2026-06-19 (re-synced feature tips: FE-768 · FE-969 · FE-934 · FE-1104)
Re-synced to the latest feature tips after the constituent PRs were updated. Each PR's own delta only was cherry-picked (not branch-merged), so
mainand other PRs' versions are not dragged onto this FE-904 base:4b4c3ccf92lapsed-plan reactivate-vs-upgrade copy.af2e9bbc7bnamespace type import forvi.mockimportOriginal.d61ab982c5), tertiary CTA surface + success-check color (5b113faa20); the dialog-size-constant fix was already present. One merge-defect fixed: the FE-965TeamSuccessWithInvitestory was repointed to the renamedcreatorAnnualfixture.usePricingTableUrlLoader+@cloudpricingTableDeepLink.spec.ts), with its members-preload prerequisite (ensureMembersLoaded). Supersedes the earlier "already present here" note above — that was stale; the deep link is present as of this refresh.Also removed the in-app billing mock harness (
billingMockHarness.ts) now that the real backend serves team-workspaces / billing data.pnpm typecheck(vue-tsc): green, 0 errors at HEAD; affected unit tests green.Refresh — 2026-06-20 (FE-965 #12954 inline team-invite block integrated)
jaewon/fe-934-team-subscribe-wire.Cherry-picked the single own commit (
af8a7a1f41) on top of the FE-934 base already present here. It supersedes the earlier ad-hoc FE-965 carry (853c1164ba):normalizeEmail(trim + lowercase), seat cap =getMaxSeats - 1(the buyer already holds one seat), theinviteMemberDialog.seatLimitReachedi18n key, and thecreatorMonthlyteam-success Storybook fixture.Conflicts resolved (expected — not from FE-965's own code): the telemetry files conflicted on
trackRunButtonbecause af8a7a1's base carries theRunButtonPropertiesrefactor (#12925, amaincommit) that this FE-904 base intentionally lacks — kept HEAD's shape; the shared InviteMembersForm + invite i18n conflicted against FE-768's email-chips redesign — kept the chips UI, dropped the obsolete invite-link strings, addedseatLimitReached.pnpm typecheck(vue-tsc): green, 0 errors at HEAD; FE-965 + telemetry unit tests green (279).