@chipmobilesdk/rn-payment 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
- {
2
- "name": "@chipmobilesdk/rn-payment",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@chipmobilesdk/rn-payment",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "license": "UNLICENSED",
6
+ "description": "Reusable in-app payment and entitlement capability for ChipMobileSdk React Native apps",
7
+ "main": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "react-native": "./src/index.ts",
13
+ "default": "./src/index.ts"
14
+ },
15
+ "./testing": {
16
+ "types": "./src/testing/index.ts",
17
+ "react-native": "./src/testing/index.ts",
18
+ "default": "./src/testing/index.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "src/index.ts",
23
+ "src/adapter",
24
+ "src/config",
25
+ "src/core",
26
+ "src/diagnostics",
27
+ "src/engine",
28
+ "src/entitlements",
29
+ "src/persistence",
30
+ "src/react",
31
+ "src/testing",
32
+ "skills",
33
+ "README.md",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "sideEffects": false,
37
+ "scripts": {
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "pack:dry-run": "npm pack --dry-run"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=19",
43
+ "react-native": ">=0.85",
44
+ "react-native-iap": ">=16.6.1 <17",
45
+ "react-native-mmkv": ">=4.0.0",
46
+ "react-native-nitro-modules": ">=0.36.5"
47
+ },
48
+ "keywords": [
49
+ "react-native",
50
+ "in-app-purchase",
51
+ "subscriptions",
52
+ "entitlements"
53
+ ]
54
+ }
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: sdk-rn-payment
3
+ description: "In-app payments via @chipmobilesdk/rn-payment: Play Billing one-time/consumable/subscription behind a store-neutral contract, entitlement keys with a required unknown state, guaranteed transaction finalization, per-environment production-transaction blocking. Use when selling a feature, gating on an entitlement, debugging a purchase that returns environment_blocked or an entitlement stuck at unknown, mapping Play products to entitlement keys, or answering a Data safety question about purchases."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-payment"
6
+ packageVersion: "0.1.0"
7
+ user-invocable: true
8
+ disable-model-invocation: false
9
+ ---
10
+
11
+ # @chipmobilesdk/rn-payment
12
+
13
+ One-time, consumable and subscription purchases on Android via Google Play Billing,
14
+ behind a contract that names no store. The app asks about **entitlement keys** it chose;
15
+ the package owns the transaction. Consuming code never imports the billing library.
16
+
17
+ ## Use this when
18
+
19
+ - Selling a feature, or gating one on an entitlement
20
+ - Mapping Play product ids to entitlement keys
21
+ - Debugging a purchase that returns `environment_blocked`, or an entitlement stuck at
22
+ `unknown`
23
+ - Wiring `no_ads` (or any entitlement) to another system
24
+ - Answering a Data safety / Play policy question about purchases
25
+
26
+ ## Do not use this for
27
+
28
+ - **Paywall UI, pricing strategy, or when to offer** — the package ships no paywall, no
29
+ price table and no purchase button, and chooses no moment.
30
+ - **What an entitlement unlocks** — it reports `entitled`; the meaning is the app's.
31
+ - **Cancellation UI** — platforms forbid a self-built one. Use
32
+ `openSubscriptionManagement()`, which opens the store's.
33
+ - **Receipt verification** — `validateTransaction` is a hook for *your* backend; the
34
+ package calls no endpoint but the billing service.
35
+ - iOS — contract only. `createStoreProvider()` returns an `unavailable` stub.
36
+
37
+ ## `unknown` is a real state, and collapsing it is the defect this package exists to prevent
38
+
39
+ `getEntitlement()` returns `unknown | entitled | not_entitled`. On a cold start the answer
40
+ is genuinely not known yet, and the package says so rather than guessing. Treat `unknown`
41
+ as "wait", never as "unpaid":
42
+
43
+ ```tsx
44
+ if (state.status === 'unknown') return <Loading />; // not the unpaid branch
45
+ ```
46
+
47
+ Collapse it and a paying user watches ads flash before they vanish, or sees a purchased
48
+ feature locked for a second. It is also **re-entrant** — a cached answer returns to
49
+ `unknown` past its staleness ceiling (7 days by default), so it is not merely a startup
50
+ flicker. See `## The entitlement model`.
51
+
52
+ ## A debug build cannot buy anything, and that is the guard working
53
+
54
+ `purchase()` returning `environment_blocked` in development is correct, not a bug. The
55
+ rule: a chargeable purchase is permitted only when `environment: 'production'` **and**
56
+ `isProductionBuild: true`. A non-production build declaring `production` throws
57
+ `PAYMENT_PRODUCTION_CONFIG_IN_NON_PRODUCTION_BUILD` at construction and never connects.
58
+
59
+ Consequence worth knowing before you plan device testing: **licence-tester purchases are
60
+ real Play transactions**, so verifying them needs a release-configuration build
61
+ (`isProductionBuild: !__DEV__`, `environment: 'production'`) installed from a Play track.
62
+ Debug builds are structurally unable to reach the store. Drive every behaviour that is
63
+ not "does Play accept this" through the fake store instead. See
64
+ `## Environments and the production guard`.
65
+
66
+ Unlike `@chipmobilesdk/rn-ads`, there is **no test-id substitution**: Play has no test
67
+ *product* id, so blocking is the only honest option.
68
+
69
+ ## Two more things not to "fix"
70
+
71
+ - **Finalization is not yours to call** for non-consumables. Google auto-refunds an
72
+ unacknowledged purchase after three days, so the package owns a persisted queue drained
73
+ at startup and on foreground. A **consumable** is the exception: call `consume()` after
74
+ *your* data records the item, because only you know it was banked.
75
+ - **`renewalState: 'indeterminate'` on Android is honest.** Play returns a grace-period
76
+ subscription as active and stops returning a held one, so the two are not separable
77
+ client-side. The default policy (`grant` in grace, `withhold` on hold) is nonetheless
78
+ correct with no backend, because Play's own behaviour matches it. A *non-default* policy
79
+ needs server data through the validation hook.
80
+
81
+ Call `handleForeground()` yourself on app resume — the package will not guess what
82
+ "foreground" means for a host backgrounded by its own modal or the store's purchase sheet.
83
+
84
+ ## Public API (v0.1.0)
85
+
86
+ Runtime: `createEngine` (throws `PaymentConfigError` on a bad catalog or an unsafe
87
+ environment; everything else returns a typed result) · `createStoreProvider` ·
88
+ `PaymentProvider`, `usePaymentEngine` · `useEntitlement`, `useEntitlements`,
89
+ `usePaymentStatus`, `usePurchase`, `useProducts`, `useSync` · `PaymentError`,
90
+ `PaymentConfigError`, `PAYMENT_ERROR_CODES` · `DEFAULT_MESSAGES`,
91
+ `DEFAULT_USER_MESSAGES`, `DEFAULT_DEVELOPER_MESSAGES`, `USER_MESSAGE_KEYS`,
92
+ `DEVELOPER_MESSAGE_KEYS`, `isUserMessageKey` · timeout/retry/cache defaults and bounds
93
+ (`DEFAULT_STALE_AFTER_MS`, `DEFAULT_PURCHASE_LAUNCH_TIMEOUT_MS`, `STALE_AFTER_BOUNDS`, …)
94
+ · `createFakeStore`, `makePurchase` from `@chipmobilesdk/rn-payment/testing` (test
95
+ doubles only).
96
+
97
+ Engine: `initialize`, `cleanup`, `getStatus`, `subscribeStatus`, `getEntitlement`,
98
+ `getAllEntitlements`, `subscribeEntitlements`, `purchase`, `consume`, `getUnfinalized`,
99
+ `getProducts`, `refreshProducts`, `changePlan`, `openSubscriptionManagement`,
100
+ `confirmPriceChange`, `sync`, `restore`, `getLastSyncedAt`, `clearLocalData`,
101
+ `handleForeground`, `subscribeEvents`.
102
+
103
+ Types: `PaymentConfig`, `CatalogEntry`, `ProductType`, `StoreProductIds`,
104
+ `EntitlementKey`, `ProductKey`, `EntitlementState`, `EntitlementStatus`,
105
+ `EntitlementMap`, `SubscriptionDetail`, `RenewalState`, `PurchaseResult`,
106
+ `PurchaseOptions`, `TransactionRef`, `FinalizeResult`, `UnfinalizedSummary`,
107
+ `ProductInfo`, `Offer`, `ProductQueryResult`, `MissingProduct`, `ProrationMode`,
108
+ `PriceChangeResult`, `PaymentStatus`, `PaymentStatusKind`, `UnavailableReason`,
109
+ `Environment`, `LapsePolicy`, `ValidationHook`, `ValidationCandidate`,
110
+ `ValidationDecision`, `ValidationUnavailablePolicy`, `SyncOutcome`, `RestoreOutcome`,
111
+ `InitOutcome`, `PaymentEvent`, `PaymentErrorCode`, `LoggerAdapter`, `Unsubscribe`.
112
+
113
+ `cleanup()` is idempotent, and so is every `Unsubscribe`. After cleanup no event fires.
114
+
115
+ ## Where the detail lives
116
+
117
+ Read one section of `node_modules/@chipmobilesdk/rn-payment/README.md`:
118
+
119
+ | Need | Section |
120
+ |---|---|
121
+ | First integration | `## Install`, `## Quick start` |
122
+ | Catalog and Play Console product setup | `## Catalog and entitlement mapping` |
123
+ | Entitlement states and staleness | `## The entitlement model` |
124
+ | Every purchase outcome and how to handle it | `## Purchase flow` |
125
+ | Why acknowledgement is not yours | `## Finalization: the invariant you cannot opt out of` |
126
+ | Consumables | `## Consumables` |
127
+ | Grace/hold, plan change, cancellation | `## Subscriptions` |
128
+ | Prices, offers, trial eligibility | `## Prices and offers` |
129
+ | Restore, offline, logout, account switch | `## Sync, restore, and offline` |
130
+ | `environment_blocked`, release config | `## Environments and the production guard` |
131
+ | Backend verification | `## Server-side validation` |
132
+ | Events, ordering, error codes | `## Events and errors` |
133
+ | Wiring an entitlement to ads | `## Wiring an entitlement to another system` |
134
+ | Tests and licence testers | `## Testing` |
135
+ | Data safety, permissions | `## What is stored on the device`, `## Play policy checklist` |
136
+ | Before release | `## Production release checklist` |
137
+ | What is deferred | `## Known limitations`, `## Adding iOS later` |
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Platform selection for the store adapter.
3
+ *
4
+ * This exists so the public surface never names a store. An app writes
5
+ * `createStoreProvider()`, not `createIapAdapter()` — the same call will pick the
6
+ * StoreKit adapter once iOS exists, and no consumer line changes (FR-002, FR-004,
7
+ * FR-007).
8
+ *
9
+ * On a platform with no adapter the result is a stub that reports
10
+ * `platform_unsupported` rather than throwing. That is what lets a host app start
11
+ * and run normally where selling is impossible, losing only the ability to sell
12
+ * (FR-021, FR-023).
13
+ */
14
+
15
+ import { Platform } from 'react-native';
16
+ import type { ProductType, Unsubscribe } from '../config/types';
17
+ import { createIapAdapter } from './iap-adapter';
18
+ import type {
19
+ ProviderCapabilities,
20
+ ProviderConnectOutcome,
21
+ ProviderPort,
22
+ ProviderPriceChangeOutcome,
23
+ ProviderProduct,
24
+ ProviderPurchase,
25
+ ProviderPurchaseOutcome,
26
+ } from './port';
27
+
28
+ const UNSUPPORTED_CAPABILITIES: ProviderCapabilities = {
29
+ supportsSubscriptions: false,
30
+ supportsPlanChange: false,
31
+ supportsPriceChangeConfirmation: false,
32
+ reportsGraceAndHoldDistinctly: false,
33
+ supportsExternalTransactions: false,
34
+ /** Nothing here can charge anyone, so the environment guard has nothing to block. */
35
+ createsRealTransactions: false,
36
+ };
37
+
38
+ /**
39
+ * The no-adapter stub.
40
+ *
41
+ * Every operation fails safely and predictably. Notably `queryOwned` returns an
42
+ * empty list rather than throwing: the engine then computes `not_entitled` after a
43
+ * successful-but-empty sync, which is honest — on a platform with no store, the
44
+ * user genuinely holds nothing this package can verify.
45
+ */
46
+ function createUnsupportedProvider(): ProviderPort {
47
+ return {
48
+ async connect(): Promise<ProviderConnectOutcome> {
49
+ return { ok: false, reason: 'platform_unsupported', retryable: false };
50
+ },
51
+ async disconnect(): Promise<void> {
52
+ // Nothing was ever connected.
53
+ },
54
+ observeConnection(): Unsubscribe {
55
+ return () => undefined;
56
+ },
57
+ async queryProducts(): Promise<ProviderProduct[]> {
58
+ return [];
59
+ },
60
+ async queryOwned(): Promise<ProviderPurchase[]> {
61
+ return [];
62
+ },
63
+ async launchPurchase(): Promise<ProviderPurchaseOutcome> {
64
+ return { ok: false, reason: 'not_ready', detail: 'platform_unsupported' };
65
+ },
66
+ observePurchases(): Unsubscribe {
67
+ return () => undefined;
68
+ },
69
+ async acknowledge(): Promise<void> {
70
+ // No transaction can exist to acknowledge.
71
+ },
72
+ async consume(): Promise<void> {
73
+ // As above.
74
+ },
75
+ async openSubscriptionManagement(): Promise<void> {
76
+ // No store screen to open.
77
+ },
78
+ async confirmPriceChange(): Promise<ProviderPriceChangeOutcome> {
79
+ return { ok: false, reason: 'not_ready' };
80
+ },
81
+ capabilities: () => UNSUPPORTED_CAPABILITIES,
82
+ };
83
+ }
84
+
85
+ export interface StoreProviderOptions {
86
+ /**
87
+ * Force the unsupported stub regardless of platform.
88
+ *
89
+ * Useful for an app that wants to ship one build with selling switched off at
90
+ * the provider level rather than through the `enabled` flag.
91
+ */
92
+ forceUnsupported?: boolean;
93
+ }
94
+
95
+ /**
96
+ * The platform's store adapter.
97
+ *
98
+ * iOS deliberately returns the stub today rather than throwing: the contract is
99
+ * prepared for StoreKit, the implementation is not, and an app running on iOS
100
+ * should degrade to "cannot sell here" instead of crashing.
101
+ */
102
+ export function createStoreProvider(
103
+ options: StoreProviderOptions = {},
104
+ ): ProviderPort {
105
+ if (options.forceUnsupported) {
106
+ return createUnsupportedProvider();
107
+ }
108
+ if (Platform.OS === 'android') {
109
+ return createIapAdapter();
110
+ }
111
+ return createUnsupportedProvider();
112
+ }
113
+
114
+ /** Exported for a test or a screen that needs the stub explicitly. */
115
+ export { createUnsupportedProvider };
116
+
117
+ export type { ProductType };