← Provider replacement walkthrough

IAPKit · convex/commerce/capabilities.ts

Source snapshot · 2026-09-09

SHA-256 2e2158922e05ed919bb8c5de5e581d180466cbf4f2e803f3684708f92b52c2f8
1// Per-provider capability declarations.2//3// These describe what IAPKit *actually implements today*, not what a store's4// API theoretically allows. Consumers branch on this instead of assuming every5// store behaves like Apple, and support can answer "why is there no renewal6// event for Meta" without reading provider code.7//8// Keep in sync with the provider modules; `contract.test.ts` pins the9// claims that have a concrete implementation behind them.10 11import type { CommerceStore } from "./contract";12 13export type ProviderCapabilities = {14  /** Server-side receipt/token validation on demand. */15  supportsInitialValidation: boolean;16  /** Store pushes lifecycle notifications to IAPKit. */17  supportsServerNotifications: boolean;18  /** IAPKit keeps a canonical subscription record for this store. */19  supportsSubscriptions: boolean;20  /** Renewal is observable as a distinct lifecycle event. */21  supportsRenewalEvents: boolean;22  /** Refund or revocation is observable as a distinct lifecycle event. */23  supportsRefundEvents: boolean;24  /** Expiration is observable rather than only inferred from a timestamp. */25  supportsExpiration: boolean;26  /** A scheduled pass re-reads authoritative store state. */27  supportsReconciliation: boolean;28  /** Entitlement state is derivable for gating. */29  supportsEntitlements: boolean;30  /** The store asserts an amount IAPKit can attribute to revenue. */31  supportsRevenueAmount: boolean;32  /** Human-readable reason for every false above. */33  notes: string;34};35 36export const PROVIDER_CAPABILITIES: Record<37  CommerceStore,38  ProviderCapabilities39> = {40  apple: {41    supportsInitialValidation: true,42    supportsServerNotifications: true,43    supportsSubscriptions: true,44    supportsRenewalEvents: true,45    supportsRefundEvents: true,46    supportsExpiration: true,47    supportsReconciliation: false,48    supportsEntitlements: true,49    supportsRevenueAmount: true,50    notes:51      "App Store Server Notifications V2 drive the lifecycle. No scheduled " +52      "reconciliation pass exists yet, so a notification lost past Apple's " +53      "retry window is not self-healing. Receipt verification bootstraps a " +54      "token only before its first store event and cannot recreate a missed " +55      "commerce event.",56  },57  google: {58    supportsInitialValidation: true,59    supportsServerNotifications: true,60    supportsSubscriptions: true,61    supportsRenewalEvents: true,62    supportsRefundEvents: true,63    supportsExpiration: true,64    supportsReconciliation: false,65    supportsEntitlements: true,66    supportsRevenueAmount: true,67    notes:68      "RTDN drives the lifecycle. Google reissues purchaseToken across " +69      "upgrade/downgrade. The receiver requires subscriptionsv2 enrichment " +70      "for non-terminal lifecycle events and uses linkedPurchaseToken to move " +71      "the canonical row onto the replacement token. No scheduled " +72      "reconciliation pass exists, and receipt verification cannot recreate " +73      "a missed commerce event.",74  },75  horizon: {76    supportsInitialValidation: true,77    supportsServerNotifications: false,78    supportsSubscriptions: false,79    supportsRenewalEvents: false,80    supportsRefundEvents: false,81    supportsExpiration: false,82    supportsReconciliation: false,83    supportsEntitlements: true,84    supportsRevenueAmount: false,85    notes:86      "Only the Graph verify_entitlement endpoint is integrated: a one-shot " +87      "check that the viewer owns the SKU. Meta exposes no server " +88      "notifications to IAPKit, so there is no renewal, expiration or refund " +89      "signal and no canonical subscription record. Entitlement is answerable " +90      "only at the moment it is asked.",91  },92  amazon: {93    supportsInitialValidation: true,94    supportsServerNotifications: false,95    supportsSubscriptions: false,96    supportsRenewalEvents: false,97    supportsRefundEvents: false,98    supportsExpiration: false,99    supportsReconciliation: true,100    supportsEntitlements: true,101    supportsRevenueAmount: false,102    notes:103      "RVS validates receipts. Rows become due every 48 hours and a bounded " +104      "worker checks due work every five minutes, so backlog and retries can " +105      "extend that interval. RVS alone does not carry " +106      "enough lifecycle detail for a canonical subscription record, so no " +107      "subscription rows or lifecycle events are produced. A verification " +108      "still answers point-in-time entitlement.",109  },110};111 112/** Stores that can produce normalized subscription lifecycle events today. */113export function storesWithLifecycleEvents(): CommerceStore[] {114  return (Object.keys(PROVIDER_CAPABILITIES) as CommerceStore[]).filter(115    (store) => PROVIDER_CAPABILITIES[store].supportsSubscriptions,116  );117}118