@chipmobilesdk/rn-payment 0.5.0 → 0.6.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/CHANGELOG.md +61 -0
- package/README.md +46 -15
- package/package.json +1 -1
- package/skills/sdk-rn-payment/SKILL.md +4 -4
- package/src/adapter/iap-adapter.ts +12 -216
- package/src/adapter/iapShapes.ts +232 -0
- package/src/config/types.ts +8 -0
- package/src/config/validate.ts +19 -4
- package/src/core/retry.ts +1 -58
- package/src/diagnostics/events.ts +1 -1
- package/src/engine/connection.ts +194 -0
- package/src/engine/engine.ts +140 -700
- package/src/engine/internals.ts +40 -0
- package/src/engine/products.ts +39 -17
- package/src/engine/purchase.ts +22 -130
- package/src/engine/purchaseFailure.ts +48 -0
- package/src/engine/receiptValidation.ts +101 -0
- package/src/engine/state.ts +253 -0
- package/src/engine/sync.ts +161 -0
- package/src/entitlements/aggregate.ts +3 -5
- package/src/entitlements/mapping.ts +0 -22
- package/src/persistence/mmkv-store.ts +10 -3
- package/src/persistence/snapshot.ts +102 -27
- package/src/react/PaymentProvider.tsx +73 -24
- package/src/react/hooks.ts +13 -3
|
@@ -10,8 +10,8 @@ import React, {
|
|
|
10
10
|
createContext,
|
|
11
11
|
useContext,
|
|
12
12
|
useEffect,
|
|
13
|
-
useMemo,
|
|
14
13
|
useRef,
|
|
14
|
+
useState,
|
|
15
15
|
type PropsWithChildren,
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { createEngine, type EngineDeps, type PaymentEngine } from '../engine/engine';
|
|
@@ -20,17 +20,35 @@ import type { PaymentConfig } from '../config/types';
|
|
|
20
20
|
|
|
21
21
|
const PaymentContext = createContext<PaymentEngine | null>(null);
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
interface OwnedEngineProps {
|
|
24
24
|
config: PaymentConfig;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Required until the real adapter ships; also how a test or the demo app
|
|
29
|
-
* supplies the fake store. Keeping it explicit rather than defaulting to a
|
|
30
|
-
* lazily-required binding means a missing native module is a clear
|
|
31
|
-
* configuration error instead of a mysterious runtime failure.
|
|
26
|
+
* The store provider and storage. Usually `{ provider: createStoreProvider() }`;
|
|
27
|
+
* a test or the demo app supplies the fake store here.
|
|
32
28
|
*/
|
|
33
29
|
deps: EngineDeps;
|
|
30
|
+
/**
|
|
31
|
+
* When this changes, the provider builds a new engine. Defaults to
|
|
32
|
+
* `config.scopeToken`, so switching accounts gets a fresh one. `config` and
|
|
33
|
+
* `deps` themselves are read when an engine is built and may be inline objects:
|
|
34
|
+
* a new object on every render no longer reconnects to the store (PAY-07).
|
|
35
|
+
*/
|
|
36
|
+
engineKey?: string;
|
|
37
|
+
engine?: undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ExternalEngineProps {
|
|
41
|
+
/**
|
|
42
|
+
* An engine the app created and owns. The provider shares it with the tree and
|
|
43
|
+
* neither initializes nor cleans it up.
|
|
44
|
+
*/
|
|
45
|
+
engine: PaymentEngine;
|
|
46
|
+
config?: undefined;
|
|
47
|
+
deps?: undefined;
|
|
48
|
+
engineKey?: undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type PaymentProviderProps = (OwnedEngineProps | ExternalEngineProps) & {
|
|
34
52
|
/** Skip automatic initialization when the app wants to control the moment. */
|
|
35
53
|
autoInitialize?: boolean;
|
|
36
54
|
/**
|
|
@@ -39,29 +57,60 @@ export interface PaymentProviderProps {
|
|
|
39
57
|
* return is the app's decision (FR-059).
|
|
40
58
|
*/
|
|
41
59
|
syncOnForeground?: boolean;
|
|
42
|
-
}
|
|
60
|
+
};
|
|
43
61
|
|
|
44
|
-
export function PaymentProvider(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
autoInitialize = true,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
62
|
+
export function PaymentProvider(
|
|
63
|
+
props: PropsWithChildren<PaymentProviderProps>,
|
|
64
|
+
): React.ReactElement {
|
|
65
|
+
const { autoInitialize = true, syncOnForeground = false, children } = props;
|
|
66
|
+
const external = props.engine;
|
|
67
|
+
const key = props.engine ? undefined : (props.engineKey ?? props.config.scopeToken);
|
|
68
|
+
|
|
69
|
+
// The latest config and deps, read only when an engine is built.
|
|
70
|
+
const latest = useRef(props);
|
|
71
|
+
latest.current = props;
|
|
72
|
+
const build = (): PaymentEngine => {
|
|
73
|
+
const current = latest.current;
|
|
74
|
+
if (current.engine) {
|
|
75
|
+
return current.engine;
|
|
76
|
+
}
|
|
77
|
+
return createEngine(current.config, current.deps);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Built once, not once per render: validating the catalog and opening storage
|
|
81
|
+
// are not render work, and a new engine per render reconnected to the billing
|
|
82
|
+
// service every time the app's root re-rendered (FR-018, PAY-07).
|
|
83
|
+
const [owned, setOwned] = useState<PaymentEngine | null>(() => (external ? null : build()));
|
|
84
|
+
const builtFor = useRef(key);
|
|
85
|
+
const cleanedUp = useRef(new WeakSet<PaymentEngine>());
|
|
86
|
+
const engine = external ?? owned ?? build();
|
|
56
87
|
|
|
57
88
|
useEffect(() => {
|
|
89
|
+
if (external) {
|
|
90
|
+
// The app's engine, so the app's lifecycle.
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const cleaned = cleanedUp.current;
|
|
94
|
+
// An engine cleaned up by an earlier run of this effect — StrictMode's
|
|
95
|
+
// mount-unmount-mount in development, or a changed `engineKey` — is never
|
|
96
|
+
// reused: it would answer every call with "cleaned up". A fresh one replaces
|
|
97
|
+
// it and this effect runs again for that one.
|
|
98
|
+
if (!owned || cleaned.has(owned) || builtFor.current !== key) {
|
|
99
|
+
builtFor.current = key;
|
|
100
|
+
setOwned(build());
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
58
103
|
if (autoInitialize) {
|
|
59
|
-
|
|
104
|
+
owned.initialize().catch(() => undefined);
|
|
60
105
|
}
|
|
61
106
|
// Cleanup on unmount is the whole reason the provider owns the engine:
|
|
62
107
|
// after it, no event fires and no timer survives (FR-082).
|
|
63
|
-
return () =>
|
|
64
|
-
|
|
108
|
+
return () => {
|
|
109
|
+
cleaned.add(owned);
|
|
110
|
+
owned.cleanup();
|
|
111
|
+
};
|
|
112
|
+
// `build` reads the latest props through a ref, so it is not a dependency.
|
|
113
|
+
}, [external, owned, key, autoInitialize]);
|
|
65
114
|
|
|
66
115
|
useEffect(
|
|
67
116
|
() => (syncOnForeground ? watchForeground(engine) : undefined),
|
package/src/react/hooks.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { useExternalStore } from '@chipmobilesdk/rn-core/react';
|
|
12
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13
13
|
import { usePaymentEngine } from './PaymentProvider';
|
|
14
14
|
import type {
|
|
15
15
|
EntitlementKey,
|
|
16
16
|
FinalizeResult,
|
|
17
17
|
PaymentStatus,
|
|
18
18
|
PendingPurchasesState,
|
|
19
|
+
ProductKey,
|
|
19
20
|
PurchaseOptions,
|
|
20
21
|
PurchaseResult,
|
|
21
22
|
TransactionRef,
|
|
@@ -89,7 +90,7 @@ export function usePaymentStatus(): PaymentStatus {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export interface UsePurchaseResult {
|
|
92
|
-
purchase: (productKey:
|
|
93
|
+
purchase: (productKey: ProductKey, options?: PurchaseOptions) => Promise<PurchaseResult>;
|
|
93
94
|
consume: (ref: TransactionRef) => Promise<FinalizeResult>;
|
|
94
95
|
isPurchasing: boolean;
|
|
95
96
|
/** The last outcome, so a screen can render cancellation differently from failure. */
|
|
@@ -157,9 +158,13 @@ export function useProducts(productKeys: readonly string[]): UseProductsResult {
|
|
|
157
158
|
// Joined so the effect depends on the contents rather than the array identity:
|
|
158
159
|
// a caller passing a literal would otherwise re-query on every render.
|
|
159
160
|
const keyToken = productKeys.join('|');
|
|
161
|
+
// Each load's number. Only the latest may write: when the keys change while a
|
|
162
|
+
// query is out, the older answer must not overwrite the newer one (PAY-20).
|
|
163
|
+
const latestRequest = useRef(0);
|
|
160
164
|
|
|
161
165
|
const load = useCallback(
|
|
162
166
|
async (fresh: boolean): Promise<void> => {
|
|
167
|
+
const request = ++latestRequest.current;
|
|
163
168
|
const keys = keyToken === '' ? [] : keyToken.split('|');
|
|
164
169
|
if (keys.length === 0) {
|
|
165
170
|
setProducts([]);
|
|
@@ -169,6 +174,9 @@ export function useProducts(productKeys: readonly string[]): UseProductsResult {
|
|
|
169
174
|
setIsLoading(true);
|
|
170
175
|
try {
|
|
171
176
|
const result = fresh ? await engine.refreshProducts(keys) : await engine.getProducts(keys);
|
|
177
|
+
if (request !== latestRequest.current) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
172
180
|
if (result.ok) {
|
|
173
181
|
setProducts(result.products);
|
|
174
182
|
setMissing(result.missing);
|
|
@@ -177,7 +185,9 @@ export function useProducts(productKeys: readonly string[]): UseProductsResult {
|
|
|
177
185
|
setError(result.error);
|
|
178
186
|
}
|
|
179
187
|
} finally {
|
|
180
|
-
|
|
188
|
+
if (request === latestRequest.current) {
|
|
189
|
+
setIsLoading(false);
|
|
190
|
+
}
|
|
181
191
|
}
|
|
182
192
|
},
|
|
183
193
|
[engine, keyToken],
|