← Provider replacement walkthrough

IAPKit · convex/commerce/capabilities.ts

Source snapshot · 2026-10-21

SHA-256 f5509c5b6e648c99478676c22a1874eea6966dd1097d3aa0954d7b91f614fcaf
1// Per-provider capabilities: what IAPKit implements today, not what a store's2// API allows. Consumers branch on this instead of assuming every store behaves3// like Apple, and support can answer "why no renewal event for Meta?" without4// reading provider code.5//6// Keep in sync with the provider modules; `contract.test.ts` pins the claims7// that have a concrete implementation behind them.8 9import type { CommerceStore } from "./contract";10 11export type ProviderCapabilities = {12  /** Server-side receipt/token validation on demand. */13  supportsInitialValidation: boolean;14  /** Store pushes lifecycle notifications to IAPKit. */15  supportsServerNotifications: boolean;16  /** IAPKit keeps a canonical subscription record for this store. */17  supportsSubscriptions: boolean;18  /** Renewal is observable as a distinct lifecycle event. */19  supportsRenewalEvents: boolean;20  /** Refund or revocation is observable as a distinct lifecycle event. */21  supportsRefundEvents: boolean;22  /** Expiration is observable rather than only inferred from a timestamp. */23  supportsExpiration: boolean;24  /** A scheduled pass re-reads authoritative store state. */25  supportsReconciliation: boolean;26  /** Entitlement state is derivable for gating. */27  supportsEntitlements: boolean;28  /** The store asserts an amount IAPKit can attribute to revenue. */29  supportsRevenueAmount: boolean;30  /** Human-readable reason for every false above. */31  notes: string;32};33 34export const PROVIDER_CAPABILITIES: Record<35  CommerceStore,36  ProviderCapabilities37> = {38  apple: {39    supportsInitialValidation: true,40    supportsServerNotifications: true,41    supportsSubscriptions: true,42    supportsRenewalEvents: true,43    supportsRefundEvents: true,44    supportsExpiration: true,45    supportsReconciliation: false,46    supportsEntitlements: true,47    supportsRevenueAmount: true,48    notes:49      "App Store Server Notifications V2 drive the lifecycle. No scheduled " +50      "reconciliation pass exists yet, so a notification lost past Apple's " +51      "retry window is not self-healing. Receipt verification bootstraps a " +52      "token only before its first store event and cannot recreate a missed " +53      "commerce event.",54  },55  google: {56    supportsInitialValidation: true,57    supportsServerNotifications: true,58    supportsSubscriptions: true,59    supportsRenewalEvents: true,60    supportsRefundEvents: true,61    supportsExpiration: true,62    supportsReconciliation: false,63    supportsEntitlements: true,64    supportsRevenueAmount: true,65    notes:66      "RTDN drives the lifecycle. Google reissues purchaseToken across " +67      "upgrade/downgrade. The receiver requires subscriptionsv2 enrichment " +68      "for non-terminal lifecycle events and uses linkedPurchaseToken to move " +69      "the canonical row onto the replacement token. No scheduled " +70      "reconciliation pass exists, and receipt verification cannot recreate " +71      "a missed commerce event.",72  },73  horizon: {74    supportsInitialValidation: true,75    supportsServerNotifications: false,76    supportsSubscriptions: false,77    supportsRenewalEvents: false,78    supportsRefundEvents: false,79    supportsExpiration: false,80    supportsReconciliation: false,81    supportsEntitlements: true,82    supportsRevenueAmount: false,83    notes:84      "Only the Graph verify_entitlement endpoint is integrated: a one-shot " +85      "check that the viewer owns the SKU. Meta publishes subscription and " +86      "order webhook fields and a subscriptions read API; IAPKit uses " +87      "neither, so it sees no renewal or expiration signal and keeps no " +88      "canonical subscription record. Entitlement is answerable only at the " +89      "moment it is asked.",90  },91  amazon: {92    supportsInitialValidation: true,93    supportsServerNotifications: true,94    supportsSubscriptions: true,95    supportsRenewalEvents: true,96    supportsRefundEvents: true,97    supportsExpiration: true,98    supportsReconciliation: true,99    supportsEntitlements: true,100    supportsRevenueAmount: false,101    notes:102      "Amazon subscription lifecycle is in beta. Signed RTN notifications " +103      "trigger fresh RVS verification and " +104      "update canonical subscriptions and commerce events. Rechecks become " +105      "due every 48 hours; a bounded worker runs every five minutes, so " +106      "backlog and retries can extend that interval. renewalDate is not expiry, " +107      "and auto-renew off retains access until RVS removes it. RVS supplies " +108      "no authoritative charge amount. Live SNS delivery and LAT renewal, " +109      "cancellation, elapsed expiry and server entitlement revocation remain " +110      "unverified; App Tester does not certify them. See " +111      "https://kit.openiap.dev/docs/verification/amazon for beta limits.",112  },113};114 115/** Stores that can produce normalized subscription lifecycle events today. */116export function storesWithLifecycleEvents(): CommerceStore[] {117  return (Object.keys(PROVIDER_CAPABILITIES) as CommerceStore[]).filter(118    (store) => PROVIDER_CAPABILITIES[store].supportsSubscriptions,119  );120}121