forked from krille-chan/fluffychat
-
Notifications
You must be signed in to change notification settings - Fork 5
feat(subscription): v2 web payments API layer — checkout, discounts, status, cancel, portal, history #7666
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
Closed
Closed
feat(subscription): v2 web payments API layer — checkout, discounts, status, cancel, portal, history #7666
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
9b4ba92
feat(subscription): v2 API data layer — models, repos, status/product…
bbsatvik01 258ca52
feat(subscription): v2 wiring — manager/controller/settings branches …
bbsatvik01 f52d6ee
Merge remote-tracking branch 'origin/main' into satvik/subs-v2-client…
bbsatvik01 a802eb0
feat(subscription): discount checkout + promo preview + history/porta…
bbsatvik01 c0cf6f4
fix(subscription): payment-flow review fixes — portal mint-on-click, …
bbsatvik01 bcaa483
fix(subscription): round-2 review — products failure surfacing, fresh…
bbsatvik01 488740f
fix(subscription): malformed /products body throws; direct controller…
bbsatvik01 9b6cae3
refactor(subscription): make the manager test-seam production-inert v…
bbsatvik01 8a36ecc
Merge remote-tracking branch 'origin/main' into satvik/subs-v2-client…
bbsatvik01 a2fe9ca
style(subscription): dart format + merge main (CI Check formatting gate)
bbsatvik01 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /// Client model for the Subscriptions-v2 `/subscription/cancel` response | ||
| /// (choreo `CancelSubscriptionResponse`, status_v2_schema.py). The subscription | ||
| /// is set to cancel at the end of the current paid period; access continues | ||
| /// until then. The client refetches `/subscription/status` to observe | ||
| /// `cancel_at_period_end` once the webhook reflects it. | ||
| class CancelResponse { | ||
| /// Always "cancel_at_period_end" on success. | ||
| final String status; | ||
| final String entitlementRef; | ||
|
|
||
| const CancelResponse({required this.status, required this.entitlementRef}); | ||
|
|
||
| factory CancelResponse.fromJson(Map<String, dynamic> json) => CancelResponse( | ||
| status: json['status'] as String? ?? "", | ||
| entitlementRef: json['entitlementRef'] as String? ?? "", | ||
| ); | ||
| } |
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,40 @@ | ||
| /// Client model for the Subscriptions-v2 `/subscription/checkout` response | ||
| /// (choreo `CheckoutResponse`, checkout_v2_schema.py). | ||
| /// | ||
| /// - `created` -> a fresh Stripe Checkout session (200 + sessionUrl) | ||
| /// - `reused` -> the caller's already-open session (200 + sessionUrl); the | ||
| /// anti-double-tap path, never a second session | ||
| /// - `creating` -> another worker is mid-creation (202); poll after | ||
| /// [retryAfterSeconds] | ||
| class CheckoutResponse { | ||
| final String status; | ||
| final String? sessionUrl; | ||
| final int? retryAfterSeconds; | ||
|
|
||
| /// The promo code ACTUALLY on the returned session (the STORED code the | ||
| /// server echoes back), or null when no discount is applied. On a `reused` | ||
| /// open session the request's `promoCode` is IGNORED, so this — not the | ||
| /// requested code — is the discount state the UI should reflect. | ||
| final String? appliedPromoCode; | ||
|
|
||
| const CheckoutResponse({ | ||
| required this.status, | ||
| this.sessionUrl, | ||
| this.retryAfterSeconds, | ||
| this.appliedPromoCode, | ||
| }); | ||
|
|
||
| /// A terminal, session-bearing response (`created` or `reused`). | ||
| bool get isResolved => status == "created" || status == "reused"; | ||
|
|
||
| /// The in-progress state that drives the bounded poll (I3). | ||
| bool get isCreating => status == "creating"; | ||
|
|
||
| factory CheckoutResponse.fromJson(Map<String, dynamic> json) => | ||
| CheckoutResponse( | ||
| status: json['status'] as String, | ||
| sessionUrl: json['sessionUrl'] as String?, | ||
| retryAfterSeconds: (json['retryAfterSeconds'] as num?)?.toInt(), | ||
| appliedPromoCode: json['appliedPromoCode'] as String?, | ||
| ); | ||
| } |
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.
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.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
_refreshV2State()has no reentrancy guard, unlike every other new v2 mutation path.updateCurrentSubscription()andinitialize()/_initialize()both call_refreshV2State(), which mutates_statusV2,_allSubscriptions/_availableSubscriptions(via_setAvailableSubscriptions()), and_stateacross multipleawaitboundaries. UnlikesubmitSubscriptionChange,cancelSubscription's caller, and the billing-portal flow — all of which got aSingleFlightGuard— this path has no protection against concurrent invocation.SettingsSubscriptionController.initState()(insettings_subscription.dart) triggers exactly this: it callssubscriptionController.initialize(...)via.then(...)without awaiting, then immediately callssubscriptionController.updateCurrentSubscription()synchronously after. Both can run_refreshV2State()concurrently, so the catalog built in one call can be paired with a_statusV2/_statesnapshot from the other interleaved call — the exact "never drift" invariant the docstring on_refreshV2Stateclaims to guarantee.🔒 Suggested fix: collapse concurrent refreshes into one in-flight future (same pattern already used in `BillingPortalRepo._inflight`)
📝 Committable suggestion
🤖 Prompt for AI Agents