@chipmobilesdk/rn-payment 0.4.0 → 0.5.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 +482 -334
- package/README.md +925 -897
- package/package.json +52 -54
- package/skills/sdk-rn-payment/SKILL.md +175 -174
- package/src/adapter/createStoreProvider.ts +116 -118
- package/src/adapter/iap-adapter.ts +627 -660
- package/src/adapter/port.ts +209 -215
- package/src/config/types.ts +298 -310
- package/src/config/validate.ts +224 -232
- package/src/core/mutex.ts +25 -128
- package/src/core/retry.ts +95 -133
- package/src/core/timers.ts +272 -267
- package/src/diagnostics/errors.ts +124 -117
- package/src/diagnostics/events.ts +282 -266
- package/src/diagnostics/messageKeys.ts +93 -95
- package/src/engine/engine.ts +921 -999
- package/src/engine/finalize.ts +343 -346
- package/src/engine/foreground.ts +29 -0
- package/src/engine/products.ts +259 -259
- package/src/engine/purchase.ts +589 -614
- package/src/engine/subscriptions.ts +146 -157
- package/src/entitlements/aggregate.ts +261 -242
- package/src/entitlements/mapping.ts +67 -67
- package/src/entitlements/policy.ts +121 -129
- package/src/index.ts +5 -9
- package/src/persistence/mmkv-store.ts +158 -158
- package/src/persistence/snapshot.ts +220 -247
- package/src/react/PaymentProvider.tsx +90 -79
- package/src/react/hooks.ts +236 -274
- package/src/testing/conformance.ts +169 -0
- package/src/testing/fakeStore.ts +360 -393
- package/src/testing/index.ts +19 -10
|
@@ -1,266 +1,282 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed events and the emitter.
|
|
3
|
-
*
|
|
4
|
-
* The bar is FR-077: the event stream **alone** must be enough to reconstruct a
|
|
5
|
-
* transaction's full lifecycle, so that "the user says they paid but has no
|
|
6
|
-
* entitlement" can be investigated from logs without a reproduction.
|
|
7
|
-
*
|
|
8
|
-
* Forbidden in every payload, without exception (FR-076, FR-074): store objects,
|
|
9
|
-
* raw receipts, verification payloads, backend tokens, payment details, the
|
|
10
|
-
* obfuscated account identifier, and the cache-scope token.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| '
|
|
36
|
-
| '
|
|
37
|
-
| '
|
|
38
|
-
| '
|
|
39
|
-
| '
|
|
40
|
-
| '
|
|
41
|
-
| '
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
| '
|
|
45
|
-
| '
|
|
46
|
-
| '
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
| {
|
|
121
|
-
type: '
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
| {
|
|
126
|
-
type: '
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
| {
|
|
131
|
-
type: '
|
|
132
|
-
productKey: ProductKey;
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
| {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
type
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Typed events and the emitter.
|
|
3
|
+
*
|
|
4
|
+
* The bar is FR-077: the event stream **alone** must be enough to reconstruct a
|
|
5
|
+
* transaction's full lifecycle, so that "the user says they paid but has no
|
|
6
|
+
* entitlement" can be investigated from logs without a reproduction.
|
|
7
|
+
*
|
|
8
|
+
* Forbidden in every payload, without exception (FR-076, FR-074): store objects,
|
|
9
|
+
* raw receipts, verification payloads, backend tokens, payment details, the
|
|
10
|
+
* obfuscated account identifier, and the cache-scope token.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createDiagnosticChannel,
|
|
15
|
+
createEmitter as createCoreEmitter,
|
|
16
|
+
} from '@chipmobilesdk/rn-core';
|
|
17
|
+
|
|
18
|
+
/** Every delivered event also goes to the app's SDK sinks (`installSdkSink`, CORE-08). */
|
|
19
|
+
const channel = createDiagnosticChannel<PaymentEvent['type'] | 'listener_reporter_failed'>(
|
|
20
|
+
'rn-payment',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
import { redact, type PaymentErrorCode, type PaymentOperation } from './errors';
|
|
24
|
+
import type {
|
|
25
|
+
EntitlementKey,
|
|
26
|
+
Environment,
|
|
27
|
+
PendingResolution,
|
|
28
|
+
ProductKey,
|
|
29
|
+
Unsubscribe,
|
|
30
|
+
UnavailableReason,
|
|
31
|
+
PaymentStatusKind,
|
|
32
|
+
} from '../config/types';
|
|
33
|
+
|
|
34
|
+
export type PaymentEventType =
|
|
35
|
+
| 'initialization'
|
|
36
|
+
| 'connection_changed'
|
|
37
|
+
| 'products_queried'
|
|
38
|
+
| 'purchase_started'
|
|
39
|
+
| 'purchase_succeeded'
|
|
40
|
+
| 'purchase_cancelled'
|
|
41
|
+
| 'purchase_pending'
|
|
42
|
+
// How a pending purchase ended, once per transaction. `settled` precedes the
|
|
43
|
+
// `entitlement_granted` it leads to (FR-102), added in 0.2.0.
|
|
44
|
+
| 'purchase_pending_resolved'
|
|
45
|
+
| 'purchase_failed'
|
|
46
|
+
| 'transaction_finalized'
|
|
47
|
+
| 'transaction_consumed'
|
|
48
|
+
| 'finalize_retry_scheduled'
|
|
49
|
+
| 'sync_completed'
|
|
50
|
+
| 'entitlement_granted'
|
|
51
|
+
| 'entitlement_revoked'
|
|
52
|
+
| 'validation_decided'
|
|
53
|
+
| 'validation_unavailable'
|
|
54
|
+
| 'environment_blocked'
|
|
55
|
+
| 'config_warning'
|
|
56
|
+
| 'internal_error';
|
|
57
|
+
|
|
58
|
+
interface BaseEvent {
|
|
59
|
+
timestamp: number;
|
|
60
|
+
/** Ties one purchase cycle together end to end. Absent for lifecycle events. */
|
|
61
|
+
correlationId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SyncTrigger = 'initialize' | 'foreground' | 'manual' | 'already_owned';
|
|
65
|
+
|
|
66
|
+
export type PaymentEvent = BaseEvent &
|
|
67
|
+
(
|
|
68
|
+
| {
|
|
69
|
+
type: 'initialization';
|
|
70
|
+
status: PaymentStatusKind;
|
|
71
|
+
unavailableReason?: UnavailableReason;
|
|
72
|
+
retryable?: boolean;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
type: 'connection_changed';
|
|
76
|
+
connected: boolean;
|
|
77
|
+
reason?: string;
|
|
78
|
+
attempt?: number;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
type: 'products_queried';
|
|
82
|
+
productKeys: ProductKey[];
|
|
83
|
+
found: number;
|
|
84
|
+
missing: ProductKey[];
|
|
85
|
+
fromCache: boolean;
|
|
86
|
+
}
|
|
87
|
+
| { type: 'purchase_started'; productKey: ProductKey }
|
|
88
|
+
| { type: 'purchase_succeeded'; productKey: ProductKey }
|
|
89
|
+
| { type: 'purchase_cancelled'; productKey: ProductKey }
|
|
90
|
+
| { type: 'purchase_pending'; productKey: ProductKey }
|
|
91
|
+
| {
|
|
92
|
+
type: 'purchase_pending_resolved';
|
|
93
|
+
productKey: ProductKey;
|
|
94
|
+
outcome: PendingResolution;
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
type: 'purchase_failed';
|
|
98
|
+
productKey: ProductKey;
|
|
99
|
+
code: PaymentErrorCode;
|
|
100
|
+
retryable: boolean;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
type: 'transaction_finalized';
|
|
104
|
+
productKey: ProductKey;
|
|
105
|
+
attempts: number;
|
|
106
|
+
}
|
|
107
|
+
| { type: 'transaction_consumed'; productKey: ProductKey }
|
|
108
|
+
| {
|
|
109
|
+
type: 'finalize_retry_scheduled';
|
|
110
|
+
productKey: ProductKey;
|
|
111
|
+
attempt: number;
|
|
112
|
+
delayMs: number;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
type: 'sync_completed';
|
|
116
|
+
restoredCount: number;
|
|
117
|
+
revokedCount: number;
|
|
118
|
+
trigger: SyncTrigger;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: 'entitlement_granted';
|
|
122
|
+
entitlementKey: EntitlementKey;
|
|
123
|
+
sources: ProductKey[];
|
|
124
|
+
}
|
|
125
|
+
| {
|
|
126
|
+
type: 'entitlement_revoked';
|
|
127
|
+
entitlementKey: EntitlementKey;
|
|
128
|
+
reason: 'expired' | 'revoked_by_store' | 'validation_rejected' | 'consumed';
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
type: 'validation_decided';
|
|
132
|
+
productKey: ProductKey;
|
|
133
|
+
decision: 'approved' | 'rejected';
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
type: 'validation_unavailable';
|
|
137
|
+
productKey: ProductKey;
|
|
138
|
+
fallback: 'grant_and_queue' | 'withhold';
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
type: 'environment_blocked';
|
|
142
|
+
productKey: ProductKey;
|
|
143
|
+
environment: Environment;
|
|
144
|
+
}
|
|
145
|
+
| { type: 'config_warning'; detail: string }
|
|
146
|
+
| {
|
|
147
|
+
type: 'internal_error';
|
|
148
|
+
code: PaymentErrorCode;
|
|
149
|
+
operation: PaymentOperation;
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
export type PaymentEventListener = (event: PaymentEvent) => void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A `PaymentEvent` with the timestamp left to the emitter.
|
|
157
|
+
*
|
|
158
|
+
* Distributive on purpose: a plain `Omit<PaymentEvent, 'timestamp'>` collapses the
|
|
159
|
+
* discriminated union into one object type with only the shared keys, which would
|
|
160
|
+
* reject every variant-specific field. `T extends unknown` forces the mapping to
|
|
161
|
+
* apply per member and keeps the union discriminated.
|
|
162
|
+
*/
|
|
163
|
+
type WithOptionalTimestamp<T> = T extends unknown
|
|
164
|
+
? Omit<T, 'timestamp'> & { timestamp?: number }
|
|
165
|
+
: never;
|
|
166
|
+
|
|
167
|
+
export type PaymentEventInput = WithOptionalTimestamp<PaymentEvent>;
|
|
168
|
+
|
|
169
|
+
export interface PaymentEmitter {
|
|
170
|
+
emit(event: PaymentEvent): void;
|
|
171
|
+
subscribe(listener: PaymentEventListener): Unsubscribe;
|
|
172
|
+
/** Disarms permanently. After this nothing is emitted (FR-082). */
|
|
173
|
+
dispose(): void;
|
|
174
|
+
listenerCount(): number;
|
|
175
|
+
disposed(): boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Creates the emitter.
|
|
180
|
+
*
|
|
181
|
+
* Two properties are deliberate:
|
|
182
|
+
*
|
|
183
|
+
* 1. **A throwing listener cannot break anything** (FR-081). Each listener is
|
|
184
|
+
* invoked defensively, the throw is handed to `onListenerError`, and delivery
|
|
185
|
+
* continues to the remaining listeners. One app's broken analytics callback
|
|
186
|
+
* must not stop the package granting an entitlement or stop another listener
|
|
187
|
+
* hearing about it.
|
|
188
|
+
*
|
|
189
|
+
* The failure is **not** re-emitted as an event, deliberately. A consumer's own
|
|
190
|
+
* bug is not a payment-lifecycle event, and reporting it through the channel
|
|
191
|
+
* that just failed would both invite recursion and double event traffic for
|
|
192
|
+
* every event while that listener stays broken. It goes to the logger instead.
|
|
193
|
+
*
|
|
194
|
+
* 2. **After `dispose`, nothing is emitted** (FR-082). A store callback can
|
|
195
|
+
* arrive after cleanup; it must be dropped silently rather than reach a
|
|
196
|
+
* listener whose component is gone.
|
|
197
|
+
*/
|
|
198
|
+
export function createEmitter(
|
|
199
|
+
onListenerError?: (error: unknown, event: PaymentEvent) => void,
|
|
200
|
+
): PaymentEmitter {
|
|
201
|
+
// The event whose listeners are running, so a failure is reported against it.
|
|
202
|
+
let dispatching: PaymentEvent | null = null;
|
|
203
|
+
let isDisposed = false;
|
|
204
|
+
// rn-core's emitter: a snapshot per emit, each listener isolated, idempotent
|
|
205
|
+
// unsubscribe. Even the error reporter is isolated: if it throws, rn-core
|
|
206
|
+
// rethrows that on a fresh task rather than into the emit.
|
|
207
|
+
const emitter = createCoreEmitter<PaymentEvent>({
|
|
208
|
+
onListenerError: error => {
|
|
209
|
+
if (!dispatching) return;
|
|
210
|
+
try {
|
|
211
|
+
onListenerError?.(error, dispatching);
|
|
212
|
+
} catch {
|
|
213
|
+
// The app's own reporter failed: nothing in the app is left to tell, so
|
|
214
|
+
// the failure goes to the SDK sinks rather than becoming an uncaught
|
|
215
|
+
// error on some later tick (FR-080).
|
|
216
|
+
channel.emit('listener_reporter_failed', 'error', {
|
|
217
|
+
eventType: dispatching.type,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
emit(event) {
|
|
225
|
+
if (isDisposed) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const previous = dispatching;
|
|
229
|
+
dispatching = event;
|
|
230
|
+
try {
|
|
231
|
+
if (emitter.emit(event)) {
|
|
232
|
+
// The type and the cycle only: payment events carry product and
|
|
233
|
+
// entitlement keys an app may treat as personal.
|
|
234
|
+
channel.emit(
|
|
235
|
+
event.type,
|
|
236
|
+
/failed|error|blocked|rejected|revoked/.test(event.type) ? 'warn' : 'info',
|
|
237
|
+
undefined,
|
|
238
|
+
event.correlationId,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
dispatching = previous;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
subscribe(listener: PaymentEventListener): Unsubscribe {
|
|
246
|
+
return isDisposed ? () => undefined : emitter.subscribe(listener);
|
|
247
|
+
},
|
|
248
|
+
dispose(): void {
|
|
249
|
+
isDisposed = true;
|
|
250
|
+
emitter.dispose();
|
|
251
|
+
},
|
|
252
|
+
listenerCount: () => emitter.listenerCount(),
|
|
253
|
+
disposed: () => isDisposed,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Fields that must never appear in an event payload.
|
|
259
|
+
*
|
|
260
|
+
* Used by `__tests__/redaction.test.ts` to walk every emitted event across the
|
|
261
|
+
* whole suite. Enumerating the ban here rather than in the test keeps the rule
|
|
262
|
+
* next to the type it constrains.
|
|
263
|
+
*/
|
|
264
|
+
export const FORBIDDEN_EVENT_FIELDS: readonly string[] = [
|
|
265
|
+
'verificationPayload',
|
|
266
|
+
'receipt',
|
|
267
|
+
'purchaseToken',
|
|
268
|
+
'transactionReceipt',
|
|
269
|
+
'obfuscatedAccountId',
|
|
270
|
+
'scopeToken',
|
|
271
|
+
'token',
|
|
272
|
+
'signature',
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
/** Correlation ids tie one purchase cycle together; no user data goes into them. */
|
|
276
|
+
export function createCorrelationId(counter: { next: number }, prefix = 'pc'): string {
|
|
277
|
+
const id = `${prefix}_${counter.next.toString(36)}`;
|
|
278
|
+
counter.next += 1;
|
|
279
|
+
return id;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export { redact };
|