@chipmobilesdk/rn-payment 0.1.0 → 0.2.1

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,54 +1,54 @@
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
- }
1
+ {
2
+ "name": "@chipmobilesdk/rn-payment",
3
+ "version": "0.2.1",
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
+ }
@@ -1,9 +1,9 @@
1
1
  ---
2
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."
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, pending purchases visible across sessions, guaranteed transaction finalization, per-environment production-transaction blocking. Use when selling a feature, gating on an entitlement, showing a payment still pending after a restart, 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
4
  metadata:
5
5
  package: "@chipmobilesdk/rn-payment"
6
- packageVersion: "0.1.0"
6
+ packageVersion: "0.2.1"
7
7
  user-invocable: true
8
8
  disable-model-invocation: false
9
9
  ---
@@ -17,6 +17,7 @@ the package owns the transaction. Consuming code never imports the billing libra
17
17
  ## Use this when
18
18
 
19
19
  - Selling a feature, or gating one on an entitlement
20
+ - Showing "payment pending" — in this session or after a restart — and withholding Buy
20
21
  - Mapping Play product ids to entitlement keys
21
22
  - Debugging a purchase that returns `environment_blocked`, or an entitlement stuck at
22
23
  `unknown`
@@ -49,6 +50,17 @@ feature locked for a second. It is also **re-entrant** — a cached answer retur
49
50
  `unknown` past its staleness ceiling (7 days by default), so it is not merely a startup
50
51
  flicker. See `## The entitlement model`.
51
52
 
53
+ ## Pending is observable across sessions — never keep your own marker
54
+
55
+ New in 0.2.0. `usePendingPurchases(productKeys?)` (or `getPendingPurchases()` /
56
+ `subscribePendingPurchases()`) lists `{ productKey, since }` for payments still in flight,
57
+ read from the persisted snapshot, so it survives an app kill and an offline restart. Same
58
+ rule as entitlements: check `status` first — `unknown` is not "nothing pending". Show the
59
+ existing `payment.state.pending` message key and withhold Buy while listed.
60
+ `purchase_pending_resolved` (`settled` → then `entitlement_granted`, or `cancelled` /
61
+ `expired`) fires once when it ends. On Android an unpaid end is always `cancelled`: Play
62
+ does not say why. See `### A payment that stays pending` under `## Purchase flow`.
63
+
52
64
  ## A debug build cannot buy anything, and that is the guard working
53
65
 
54
66
  `purchase()` returning `environment_blocked` in development is correct, not a bug. The
@@ -81,12 +93,13 @@ Unlike `@chipmobilesdk/rn-ads`, there is **no test-id substitution**: Play has n
81
93
  Call `handleForeground()` yourself on app resume — the package will not guess what
82
94
  "foreground" means for a host backgrounded by its own modal or the store's purchase sheet.
83
95
 
84
- ## Public API (v0.1.0)
96
+ ## Public API (v0.2.0)
85
97
 
86
98
  Runtime: `createEngine` (throws `PaymentConfigError` on a bad catalog or an unsafe
87
99
  environment; everything else returns a typed result) · `createStoreProvider` ·
88
100
  `PaymentProvider`, `usePaymentEngine` · `useEntitlement`, `useEntitlements`,
89
- `usePaymentStatus`, `usePurchase`, `useProducts`, `useSync` · `PaymentError`,
101
+ `usePendingPurchases`, `usePaymentStatus`, `usePurchase`, `useProducts`, `useSync` ·
102
+ `PaymentError`,
90
103
  `PaymentConfigError`, `PAYMENT_ERROR_CODES` · `DEFAULT_MESSAGES`,
91
104
  `DEFAULT_USER_MESSAGES`, `DEFAULT_DEVELOPER_MESSAGES`, `USER_MESSAGE_KEYS`,
92
105
  `DEVELOPER_MESSAGE_KEYS`, `isUserMessageKey` · timeout/retry/cache defaults and bounds
@@ -95,14 +108,16 @@ environment; everything else returns a typed result) · `createStoreProvider` ·
95
108
  doubles only).
96
109
 
97
110
  Engine: `initialize`, `cleanup`, `getStatus`, `subscribeStatus`, `getEntitlement`,
98
- `getAllEntitlements`, `subscribeEntitlements`, `purchase`, `consume`, `getUnfinalized`,
111
+ `getAllEntitlements`, `subscribeEntitlements`, `getPendingPurchases`,
112
+ `subscribePendingPurchases`, `purchase`, `consume`, `getUnfinalized`,
99
113
  `getProducts`, `refreshProducts`, `changePlan`, `openSubscriptionManagement`,
100
114
  `confirmPriceChange`, `sync`, `restore`, `getLastSyncedAt`, `clearLocalData`,
101
115
  `handleForeground`, `subscribeEvents`.
102
116
 
103
117
  Types: `PaymentConfig`, `CatalogEntry`, `ProductType`, `StoreProductIds`,
104
118
  `EntitlementKey`, `ProductKey`, `EntitlementState`, `EntitlementStatus`,
105
- `EntitlementMap`, `SubscriptionDetail`, `RenewalState`, `PurchaseResult`,
119
+ `EntitlementMap`, `SubscriptionDetail`, `RenewalState`, `PendingPurchase`,
120
+ `PendingPurchasesState`, `PendingResolution`, `PurchaseResult`,
106
121
  `PurchaseOptions`, `TransactionRef`, `FinalizeResult`, `UnfinalizedSummary`,
107
122
  `ProductInfo`, `Offer`, `ProductQueryResult`, `MissingProduct`, `ProrationMode`,
108
123
  `PriceChangeResult`, `PaymentStatus`, `PaymentStatusKind`, `UnavailableReason`,
@@ -112,6 +127,17 @@ Types: `PaymentConfig`, `CatalogEntry`, `ProductType`, `StoreProductIds`,
112
127
 
113
128
  `cleanup()` is idempotent, and so is every `Unsubscribe`. After cleanup no event fires.
114
129
 
130
+ ## Upgrading 0.1.0 → 0.2.0
131
+
132
+ One compile-time change: `PaymentEventType` gained `'purchase_pending_resolved'`, so an
133
+ exhaustive `switch` needs the case or a `default`. A hand-written `PaymentEngine` mock
134
+ needs the two pending methods. No storage migration — `0.1.0` already persisted pending
135
+ records. Replace any app-side "pending since" marker with `usePendingPurchases`.
136
+
137
+ **Upgrade before shipping on Android.** In `0.1.0` the adapter misread `react-native-iap`
138
+ 16.x's string purchase state, so on a device a pending purchase was granted. `0.2.0`
139
+ reads it correctly, and an unrecognized (`unspecified`) state never grants or finalizes.
140
+
115
141
  ## Where the detail lives
116
142
 
117
143
  Read one section of `node_modules/@chipmobilesdk/rn-payment/README.md`:
@@ -122,6 +148,7 @@ Read one section of `node_modules/@chipmobilesdk/rn-payment/README.md`:
122
148
  | Catalog and Play Console product setup | `## Catalog and entitlement mapping` |
123
149
  | Entitlement states and staleness | `## The entitlement model` |
124
150
  | Every purchase outcome and how to handle it | `## Purchase flow` |
151
+ | A payment pending across sessions, and how it ends | `### A payment that stays pending` |
125
152
  | Why acknowledgement is not yours | `## Finalization: the invariant you cannot opt out of` |
126
153
  | Consumables | `## Consumables` |
127
154
  | Grace/hold, plan change, cancellation | `## Subscriptions` |
@@ -1,117 +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 };
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 };