{"record":{"id":"b079eb86d274cbae","repo":"toeverything/AFFiNE","slug":"managed-by-app-store-or-play-b079eb","errorCode":"managed_by_app_store_or_play","errorMessage":"This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.","messagePattern":"This subscription is managed by App Store or Google Play\\. Please manage it in the corresponding store\\.","errorType":"exception","errorClass":"ManagedByAppStoreOrPlay","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/payment/service.ts","lineNumber":173,"sourceCode":"    );\n  }\n\n  async cancelSubscription(\n    identity: z.infer<typeof SubscriptionIdentity>,\n    idempotencyKey?: string\n  ): Promise<Subscription> {\n    this.assertSubscriptionIdentity(identity);\n\n    const manager = this.select(identity.plan);\n    const subscription = await manager.getActiveSubscription(identity);\n\n    if (!subscription) {\n      throw new SubscriptionNotExists({ plan: identity.plan });\n    }\n\n    // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web\n    if (subscription.provider === 'revenuecat') {\n      throw new ManagedByAppStoreOrPlay();\n    }\n\n    if (!subscription.stripeSubscriptionId) {\n      throw new CantUpdateOnetimePaymentSubscription(\n        'Onetime payment subscription cannot be canceled.'\n      );\n    }\n\n    if (subscription.canceledAt) {\n      throw new SubscriptionHasBeenCanceled();\n    }\n\n    // update the subscription in db optimistically\n    const newSubscription = manager.cancelSubscription(subscription);\n\n    // should release the schedule first\n    if (subscription.stripeScheduleId) {\n      const manager = await this.scheduleManager.fromSchedule(","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/payment/service.ts#L155-L191","documentation":"Thrown by cancelSubscription when the active subscription's provider is 'revenuecat'. Subscriptions purchased via in-app purchase (App Store / Google Play) are managed read-only by RevenueCat; Stripe-side mutations like cancel are refused so the web backend never fights the store.","triggerScenarios":"User bought Pro/AI in the iOS or Android app, then opens the web app and clicks Cancel Subscription; a shared account has a mobile-originated subscription and the web client calls cancelSubscription.","commonSituations":"Cross-platform product where mobile IAP and web Stripe checkout coexist; support macros that always cancel via the web API; webhooks out of sync so web UI does not badge the subscription as store-managed.","solutions":["Cancel from the originating store: iOS App Store subscriptions settings or Google Play subscriptions, which flow back through RevenueCat webhooks.","In the web UI, detect provider === 'revenuecat' on the subscription row and link to the store's manage page instead of the cancel button.","For automated flows, branch on subscription.provider before attempting web-side cancel/resume."],"exampleFix":"// before\nif (sub.status === 'active') await svc.cancelSubscription(identity);\n\n// after\nif (sub.provider === 'revenuecat') {\n  openStoreManagePage(sub);\n} else if (sub.status === 'active') {\n  await svc.cancelSubscription(identity);\n}","handlingStrategy":"validation","validationCode":"if (subscription.provider === 'revenuecat') {\n  openManageInStore(subscription); // App Store / Google Play link\n} else {\n  await svc.cancelSubscription(identity, idempotencyKey);\n}","typeGuard":"function isStoreManaged(s: { provider: string | null }): boolean {\n  return s.provider === 'revenuecat';\n}","tryCatchPattern":"catch (e) { if (gqlErrorCode(e) === 'managed_by_app_store_or_play') { openManageInStore(subscription); return; } throw e; }","preventionTips":["Branch all web subscription actions on provider before calling cancel/resume.","Badge store-managed subscriptions in the UI with a Manage in store link.","Keep RevenueCat webhook sync healthy so provider is never missing on rows."],"tags":["payment","iap","revenuecat","cancel","read-only-provider"],"backgroundTag":"subscription-managed-by-app-store","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}