mirror of
https://github.com/moeru-ai/airi.git
synced 2026-10-29 18:48:55 +08:00
fix(capacitor): accept partial pending subscription changes
This commit is contained in:
@@ -351,7 +351,7 @@ export function useSubscription(options: {
|
||||
throw new Error(t('settings.pages.capacitor.checkout.error'))
|
||||
|
||||
// NOTICE:
|
||||
// `productChangeInfo` makes the checkout replace the subscription instead of adding a second one.
|
||||
// `productChangeInfo` requests a change. Without a configured path, the SDK creates a new subscription.
|
||||
// The SDK marks the field internal while product changes are in beta, so its type omits it.
|
||||
// https://www.revenuecat.com/docs/web/product-changes
|
||||
// Remove this local type when `PurchaseParams` declares `productChangeInfo`.
|
||||
|
||||
Generated
+1
-1
@@ -409,7 +409,7 @@ catalogs:
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.7
|
||||
'@revenuecat/purchases-js':
|
||||
specifier: ^1.70.0
|
||||
specifier: 1.70.0
|
||||
version: 1.70.0
|
||||
'@rive-app/canvas-lite':
|
||||
specifier: ^2.40.1
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ catalog:
|
||||
'@proj-airi/unocss-preset-chromatic': ^1.1.4
|
||||
'@proj-airi/unplugin-fetch': ^0.2.3
|
||||
'@proj-airi/unplugin-live2d-sdk': ^0.1.7
|
||||
'@revenuecat/purchases-js': ^1.70.0
|
||||
'@revenuecat/purchases-js': 1.70.0
|
||||
'@rive-app/canvas-lite': ^2.40.1
|
||||
'@sherpaw/asr': 0.3.0
|
||||
'@sherpaw/kws': 0.3.0
|
||||
|
||||
@@ -162,6 +162,39 @@ describe('revenuecat subscriber client', () => {
|
||||
expect((await customer(renewed))[0]!.pendingChange).toBeNull()
|
||||
})
|
||||
|
||||
// https://github.com/moeru-ai/airi/pull/3003#discussion_r4302328417
|
||||
// ROOT CAUSE:
|
||||
// RevenueCat omits unchanged products from pending_changes.
|
||||
// Requiring product rejected early and failed renewals with 502.
|
||||
// The nested product is now optional and still accepts null.
|
||||
it.each([
|
||||
{
|
||||
scenario: 'early renewal',
|
||||
autoRenewalStatus: 'has_already_renewed',
|
||||
pendingChanges: { current_period_starts_at: periodEnd, current_period_ends_at: Date.parse('2026-12-01T00:00:00Z') },
|
||||
},
|
||||
{
|
||||
scenario: 'failed upcoming renewal',
|
||||
autoRenewalStatus: 'will_renew',
|
||||
pendingChanges: { status: 'in_billing_retry', pending_payment: true, gives_access: false },
|
||||
},
|
||||
])('accepts $scenario without a pending product (Issue #3003)', async ({ autoRenewalStatus, pendingChanges }) => {
|
||||
const { client } = revenuecat({
|
||||
subscriptions: [{ ...goSubscription, auto_renewal_status: autoRenewalStatus, pending_changes: pendingChanges }],
|
||||
activeEntitlements: [{ entitlement_id: 'entl_go', expires_at: periodEnd }],
|
||||
})
|
||||
|
||||
expect(await client.fetchEntitlements('user-1')).toEqual([{
|
||||
entitlementId: 'airi_go',
|
||||
productId: 'rc_go_monthly',
|
||||
purchasedAt: new Date(periodStart),
|
||||
accessUntil: new Date(periodEnd),
|
||||
pendingChange: null,
|
||||
}])
|
||||
expect(await client.hasRenewingSubscription('user-1')).toBe(true)
|
||||
expect(await client.grantProductChange('user-1')).toEqual({ subscriptionId: 'sub_go', subscriberToken: 'subscriber-token' })
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The server granted subscriptions of both environments.
|
||||
|
||||
@@ -33,10 +33,10 @@ const subscriptionSchema = object({
|
||||
current_period_ends_at: nullable(number()),
|
||||
/**
|
||||
* The state at the next renewal. `product` differs from `product_id` when a downgrade is scheduled.
|
||||
* It also appears with the same product after an early renewal, or before a failed renewal.
|
||||
* Early renewals and failed upcoming renewals can omit `product` when it stays the same.
|
||||
*/
|
||||
pending_changes: optional(nullable(object({
|
||||
product: nullable(object({ id: string() })),
|
||||
product: optional(nullable(object({ id: string() }))),
|
||||
}))),
|
||||
entitlements: object({
|
||||
items: array(object({ id: string(), lookup_key: string() })),
|
||||
|
||||
@@ -141,9 +141,10 @@ Purchased Flux capacity and `GET /flux/history` read only the wallet pool.
|
||||
|
||||
### Product change
|
||||
|
||||
The Web SDK replaces a web subscription with another Capacitor through `purchase({ rcPackage, productChangeInfo })`.
|
||||
The Web SDK requests a subscription change through `purchase({ rcPackage, productChangeInfo })`.
|
||||
See the [RevenueCat Web SDK product changes](https://www.revenuecat.com/docs/web/product-changes).
|
||||
The SDK marks `productChangeInfo` internal during the beta, so the client declares the field locally with a `NOTICE`.
|
||||
The workspace pins the SDK to `1.70.0` because internal fields have no semver guarantee.
|
||||
|
||||
`POST /api/v1/revenuecat/product-change` gives the signed-in user a `subscriptionId` and a `subscriberToken`.
|
||||
The server picks the latest subscription with access that `rc_billing` or `stripe` bills.
|
||||
@@ -155,6 +156,8 @@ The route allows 10 calls for each user in one minute.
|
||||
|
||||
RevenueCat starts an upgrade at once and charges the prorated difference. It starts a downgrade at the next renewal.
|
||||
The dashboard defines the allowed changes under **Product Catalog > Products > Subscription changes**.
|
||||
Without a configured path, the SDK opens a regular purchase and can create a second subscription.
|
||||
The client does not check these paths before checkout. PR #3003 remains a draft until this duplicate-charge risk is resolved.
|
||||
An upgrade sends `PRODUCT_CHANGE` and `RENEWAL`. The webhook then reconciles the new period and quota.
|
||||
The client reports a downgrade as scheduled with the renewal date.
|
||||
`POST /api/v1/revenuecat/sync` returns `pendingChange` from `pending_changes.product` of the subscription.
|
||||
|
||||
Reference in New Issue
Block a user