@chipmobilesdk/rn-payment 0.3.0 → 0.4.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 +334 -241
- package/README.md +20 -11
- package/package.json +54 -54
- package/skills/sdk-rn-payment/SKILL.md +174 -168
- package/src/adapter/createStoreProvider.ts +1 -0
- package/src/adapter/iap-adapter.ts +170 -57
- package/src/adapter/port.ts +18 -1
- package/src/config/types.ts +15 -0
- package/src/core/timers.ts +18 -0
- package/src/diagnostics/errors.ts +11 -4
- package/src/engine/engine.ts +62 -14
- package/src/engine/finalize.ts +98 -50
- package/src/engine/purchase.ts +127 -39
- package/src/testing/fakeStore.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,241 +1,334 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@chipmobilesdk/rn-payment` are recorded here.
|
|
4
|
-
|
|
5
|
-
This package follows semantic versioning. **Entitlement keys are yours**, but
|
|
6
|
-
product keys, error codes, event names, state values, message keys, and the
|
|
7
|
-
public API shape are this package's contract: renaming any of them is a breaking
|
|
8
|
-
change.
|
|
9
|
-
|
|
10
|
-
## [0.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
- The
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
and
|
|
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
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@chipmobilesdk/rn-payment` are recorded here.
|
|
4
|
+
|
|
5
|
+
This package follows semantic versioning. **Entitlement keys are yours**, but
|
|
6
|
+
product keys, error codes, event names, state values, message keys, and the
|
|
7
|
+
public API shape are this package's contract: renaming any of them is a breaking
|
|
8
|
+
change.
|
|
9
|
+
|
|
10
|
+
## [0.4.0] — 2026-11-01
|
|
11
|
+
|
|
12
|
+
Minor release hardening the settlement path. **Upgrading requires no code change**;
|
|
13
|
+
the API additions are optional, and the error-code union gains a member
|
|
14
|
+
(`PAYMENT_NOT_CONSUMABLE`), so an exhaustive `switch` over it needs one more case.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A purchase on Android is settled once.** The Play binding delivers a launched
|
|
19
|
+
purchase both to the launch and to the session-long update listener, and both paths
|
|
20
|
+
settled it: the validation hook ran twice and `purchase_pending` fired twice.
|
|
21
|
+
Settlement is now single-flight per transaction, and the pending signal fires once
|
|
22
|
+
per transaction.
|
|
23
|
+
- **Nothing that never answers can wedge the engine.** `timeouts.finalizeAttemptMs`
|
|
24
|
+
was validated but never applied; acknowledge and consume now honour it, and a
|
|
25
|
+
timed-out attempt is retried. The validation hook is bounded by the new
|
|
26
|
+
`timeouts.validateMs` and treated as unavailable when it runs out. Finalize drain and
|
|
27
|
+
re-validation run after the sync flight rather than inside it, so a stuck call no
|
|
28
|
+
longer makes every later sync join it. A store-timed purchase flow gets a
|
|
29
|
+
ten-minute ceiling so the purchase lock is always released.
|
|
30
|
+
- **`consume` refuses what it must not consume.** A subscription or permanent unlock
|
|
31
|
+
returns `{ kind: 'failed', code: 'PAYMENT_NOT_CONSUMABLE' }`, a pending purchase
|
|
32
|
+
`PAYMENT_PENDING`, and no store call is made. A failed consume is now retried by
|
|
33
|
+
consuming; it was retried through the acknowledge path, which skips consumables.
|
|
34
|
+
- **A second `initialize()` after success is a no-op.** It reconnected and left a
|
|
35
|
+
second set of native listeners behind, so every update was ingested twice. The Play
|
|
36
|
+
adapter also removes its previous native listeners on every connect.
|
|
37
|
+
- **An exhausted reconnect budget is recoverable.** The engine stayed at `connecting`
|
|
38
|
+
forever. It now reports `{ kind: 'failed', retryable: true }`, and
|
|
39
|
+
`handleForeground()` starts a fresh budget with one immediate attempt.
|
|
40
|
+
- **Re-validation identifies the transaction.** It sent an empty payload and no
|
|
41
|
+
identifier, so a backend could not tell which purchase it was asked about.
|
|
42
|
+
- **The logger no longer receives raw error text.** `redact` passed `Error` messages
|
|
43
|
+
and strings through verbatim; it now keeps only a `PaymentError` code or an error's
|
|
44
|
+
class name.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `ValidationCandidate.transactionId` and `ValidationCandidate.platform`.
|
|
49
|
+
- `TimeoutConfig.validateMs` (3–120 s, default 15 s).
|
|
50
|
+
- Error code `PAYMENT_NOT_CONSUMABLE`.
|
|
51
|
+
- `./testing`: `createFakeStore({ mode: 'android-16.7' })`, which delivers a launched
|
|
52
|
+
purchase through `observePurchases` as the Play binding does and reports Android's
|
|
53
|
+
capabilities.
|
|
54
|
+
|
|
55
|
+
## [0.3.1] — 2026-10-25
|
|
56
|
+
|
|
57
|
+
Patch release. **Upgrade before shipping on Android**: with `0.3.0` no purchase could
|
|
58
|
+
complete on a device. Found on a Galaxy S25 Ultra with a licence-tester account, in the
|
|
59
|
+
first app to buy through this package (Task Manager, internal testing track).
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **Every purchase failed before Play opened.** The adapter sent
|
|
64
|
+
`request: { android: { skus, obfuscatedAccountIdAndroid } }`, the request shape from
|
|
65
|
+
`react-native-iap`'s documentation before 16.7. The 16.7 binding reads
|
|
66
|
+
`request.google` and rejected the call with `empty-sku-list`; the app saw a store
|
|
67
|
+
error. The request now uses `google` and `obfuscatedAccountId`, and is typed with the
|
|
68
|
+
binding's own `RequestPurchaseProps`, so a renamed key fails to typecheck.
|
|
69
|
+
- **A completed purchase would have been reported as a store error.** On Android
|
|
70
|
+
`requestPurchase` resolves with an empty list when the flow ends, whatever happened;
|
|
71
|
+
the outcome arrives only through `purchaseUpdatedListener` / `purchaseErrorListener`.
|
|
72
|
+
The adapter read the resolved value, so it would have reported `store_error` for
|
|
73
|
+
every purchase, while Play delivered the purchase separately. `launchPurchase` now
|
|
74
|
+
settles on the first outcome event for the product (bought, pending, or an error).
|
|
75
|
+
- **"Already owned" was a generic store error.** The adapter matched
|
|
76
|
+
`item-already-owned`; 16.7 sends `already-owned`. A reinstall that taps Buy again
|
|
77
|
+
now gets `already_owned` and the self-repairing sync (FR-033). `sku-not-found` now
|
|
78
|
+
maps to `unknown_product`.
|
|
79
|
+
- **The launch timeout cut into the user's time in Play (FR-035).** Because the
|
|
80
|
+
binding reports nothing until the flow ends, the 30-second "opening the store
|
|
81
|
+
surface" timeout covered the time the user spends choosing a payment method. The
|
|
82
|
+
engine no longer applies it to a provider whose outcome includes that time (the Play
|
|
83
|
+
adapter); the binding always ends the flow with an event, so the call cannot hang.
|
|
84
|
+
- **Plan changes sent the removed 16.6 fields.** A plan change now sends the replaced
|
|
85
|
+
purchase's token as `purchaseToken` and, with a proration mode,
|
|
86
|
+
`subscriptionProductReplacementParams` (old product id + replacement mode).
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- Peer `react-native-iap` is now `>=16.7.3 <17`, the version this adapter was verified
|
|
91
|
+
against (was `>=16.6.1 <17`, never verified).
|
|
92
|
+
- Internal: `ProviderPurchaseRequest.replaceStoreProductId` now holds the replaced
|
|
93
|
+
product's id and the token moved to `replacePurchaseToken` (it held the token before);
|
|
94
|
+
`ProviderCapabilities` gained `purchaseOutcomeIncludesStoreTime`. Neither type is
|
|
95
|
+
public API. The fake store keeps `false`, so launch-timeout tests behave as before.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- `__tests__/iap-adapter.test.ts`: the adapter against a fake binding that validates
|
|
100
|
+
and resolves as 16.7.3 does — request shape, event-based outcomes, error mapping, the
|
|
101
|
+
no-event fallback.
|
|
102
|
+
|
|
103
|
+
## [0.3.0] — 2026-10-13
|
|
104
|
+
|
|
105
|
+
Documentation release: the README and the agent skill now match the code `0.2.1` shipped.
|
|
106
|
+
**No code, API, storage or behaviour change** — upgrading from `0.2.1` changes nothing at
|
|
107
|
+
runtime. Released as its own version so the skill a consuming app syncs (`packageVersion`)
|
|
108
|
+
always matches the package it came from.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- README `### Migrating 0.1.0 to 0.2.0`: no longer claims `0.1.0` persisted pending records.
|
|
113
|
+
On a device nothing persisted before `0.2.1`; the section now says to use `0.2.1` or
|
|
114
|
+
later and what a device first running it does (empty record, `unknown` until the first
|
|
115
|
+
sync, never a false `not_entitled`).
|
|
116
|
+
- README intro: "Android only for now" instead of a version-pinned phrase.
|
|
117
|
+
- Skill: "Require `0.2.1` or later" under the upgrade notes, with the reason; the public
|
|
118
|
+
API heading names `v0.3.0` (the surface is unchanged since `0.2.0`).
|
|
119
|
+
|
|
120
|
+
## [0.2.1] — 2026-10-12
|
|
121
|
+
|
|
122
|
+
Patch release. **Upgrade before shipping**: on a device, `0.2.0` and earlier persisted
|
|
123
|
+
nothing.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
|
|
127
|
+
- **The purchase snapshot never reached the device's storage.** The store opened MMKV
|
|
128
|
+
with `new MMKV()`, the react-native-mmkv **v3** constructor. In v4 — the only major the
|
|
129
|
+
peer range (`>=4.0.0`) allows — `MMKV` is a type, not a value, so the call threw and the
|
|
130
|
+
store fell back to memory on every device. Consequences: entitlements read `unknown`
|
|
131
|
+
after every cold start until a sync, an owner with no connection stayed `unknown`, and a
|
|
132
|
+
pending purchase was forgotten after an app kill. The store now uses v4's `createMMKV`
|
|
133
|
+
and `remove`. Found on an emulator: the app's `files/mmkv` held no
|
|
134
|
+
`chipmobilesdk.payment` instance.
|
|
135
|
+
- The engine did report the fallback, as a `config_warning` ("Local storage
|
|
136
|
+
unavailable"), but nothing in the tests could trigger it: the repository's test double
|
|
137
|
+
exported a v3 `MMKV` class. The double now mirrors v4 exactly, and the persistence suite
|
|
138
|
+
fails against the old code.
|
|
139
|
+
|
|
140
|
+
No API, storage format or behaviour change otherwise. Records written by a working
|
|
141
|
+
install are read as before; there were none from `0.2.0` on a device to migrate.
|
|
142
|
+
|
|
143
|
+
## [0.2.0] — 2026-10-12
|
|
144
|
+
|
|
145
|
+
Additive minor release closing one gap that surfaced when `0.1.0` met an app that
|
|
146
|
+
must show a pending payment after a restart. It is a **capability** gap: `0.1.0`'s
|
|
147
|
+
engine refused to grant a pending purchase, but an app could only *see* one in the
|
|
148
|
+
session that started it. Reconstructing the pending set meant importing the billing
|
|
149
|
+
library, which this package exists to prevent.
|
|
150
|
+
|
|
151
|
+
It also fixes a **serious defect on Android devices**: a pending purchase was
|
|
152
|
+
granted. See [Fixed](#fixed). **Upgrade from `0.1.0` before shipping.**
|
|
153
|
+
|
|
154
|
+
**Upgrading requires one change, compile-time only:** `PaymentEventType` gained a
|
|
155
|
+
member. See [Migration](#migration).
|
|
156
|
+
|
|
157
|
+
### Added
|
|
158
|
+
|
|
159
|
+
**Pending purchases, visible across sessions**
|
|
160
|
+
|
|
161
|
+
- `getPendingPurchases()` and `subscribePendingPurchases(listener)` on the engine,
|
|
162
|
+
and `usePendingPurchases(productKeys?)` in React. They return a
|
|
163
|
+
`PendingPurchasesState`: a `status` (`known` | `unknown`), an `isStale` flag, and
|
|
164
|
+
`purchases`, one `{ productKey, since }` per product. No transaction id, token or
|
|
165
|
+
store object.
|
|
166
|
+
- Read from the persisted snapshot, so the set survives an app kill and an offline
|
|
167
|
+
restart. It is judged by the **same staleness rule as entitlements**, now shared
|
|
168
|
+
code rather than a parallel copy: never synced, or older than `staleAfterMs`,
|
|
169
|
+
reports `unknown`, never an empty "nothing pending".
|
|
170
|
+
- Pushed only when the set, its status, or its staleness changes. A sync that finds
|
|
171
|
+
nothing new re-renders nothing.
|
|
172
|
+
|
|
173
|
+
**How a pending purchase ended**
|
|
174
|
+
|
|
175
|
+
- `purchase_pending_resolved` event, `{ productKey, outcome }`, with `outcome`
|
|
176
|
+
`settled` | `cancelled` | `expired`, emitted once per pending transaction.
|
|
177
|
+
`settled` precedes the `entitlement_granted` it leads to.
|
|
178
|
+
- Detected from the snapshot commit itself, so it is exactly-once by construction:
|
|
179
|
+
a store push and a sync racing on one settlement emit one event, a sync that
|
|
180
|
+
changed nothing emits none, and `clearLocalData()` emits none, because discarding
|
|
181
|
+
a record locally is not the store ending it.
|
|
182
|
+
- **On Android every unpaid end is `cancelled`.** Play does not say whether a pending
|
|
183
|
+
purchase was declined or expired; it stops reporting it. The package does not
|
|
184
|
+
guess from elapsed time. `expired` is reported only by a store that says so,
|
|
185
|
+
through a new optional `pendingEndReason` on the internal provider port.
|
|
186
|
+
|
|
187
|
+
**Testing**
|
|
188
|
+
|
|
189
|
+
- Fake store: `settlePending`, `cancelPending` and `expirePending` change store-side
|
|
190
|
+
state the way the real store does, so the engine sees the change at its next sync,
|
|
191
|
+
in the same session or after a simulated app kill.
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
|
|
195
|
+
- **On a device, a pending purchase was granted.** The Android adapter read only
|
|
196
|
+
Play's numeric purchase state and treated anything else as purchased. The
|
|
197
|
+
`react-native-iap` 16.x binding hands JavaScript the state as a **string**
|
|
198
|
+
(`'pending' | 'purchased' | 'unknown'`) and does not carry the numeric one across,
|
|
199
|
+
so every pending purchase fell through to "purchased": entitlement granted, and an
|
|
200
|
+
acknowledgement attempted, for money that had not arrived. The engine's own
|
|
201
|
+
"pending never grants" rule was intact; the adapter fed it the wrong state. No
|
|
202
|
+
test could see it, because tests drive the fake store, which bypasses the adapter.
|
|
203
|
+
The adapter now reads either form, and anything it does not recognize is
|
|
204
|
+
`unspecified`, never purchased. Checked against the installed 16.7.3 declarations
|
|
205
|
+
and its JavaScript bridge.
|
|
206
|
+
- **An `unspecified` purchase state is never treated as paid.** `0.1.0` persisted
|
|
207
|
+
Play's unknown state as `purchased`, granting it and queuing an acknowledgement.
|
|
208
|
+
Now a new transaction in that state is left out until the store says what it is,
|
|
209
|
+
a known one keeps what the package already knew (uncertainty is neither a grant nor
|
|
210
|
+
a refund), a store push in that state is ignored, and `purchase()` returning one
|
|
211
|
+
answers `store_error`, retryable: if the money did move, a retry gets
|
|
212
|
+
`already_owned`, which re-syncs and grants.
|
|
213
|
+
|
|
214
|
+
### Unchanged, deliberately
|
|
215
|
+
|
|
216
|
+
- **Pending never grants**, under any configuration. The new surface is read-only.
|
|
217
|
+
- **No storage migration.** The schema stays at version 1. `0.1.0` already persisted
|
|
218
|
+
pending records; `0.2.0` only reads them.
|
|
219
|
+
- `purchase()` still returns `pending`, and `purchase_pending` still fires, exactly
|
|
220
|
+
as before.
|
|
221
|
+
|
|
222
|
+
### Migration
|
|
223
|
+
|
|
224
|
+
| Union | New member |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `PaymentEventType` / `PaymentEvent` | `'purchase_pending_resolved'` |
|
|
227
|
+
|
|
228
|
+
An exhaustive `switch` over either needs the new case or a `default`. A
|
|
229
|
+
hand-written `PaymentEngine`, typically a test mock, needs `getPendingPurchases` and
|
|
230
|
+
`subscribePendingPurchases`. Nothing else changes.
|
|
231
|
+
|
|
232
|
+
## [0.1.0] — 2026-10-06
|
|
233
|
+
|
|
234
|
+
First version. Android only, via Google Play Billing.
|
|
235
|
+
|
|
236
|
+
### Added
|
|
237
|
+
|
|
238
|
+
**Entitlements**
|
|
239
|
+
|
|
240
|
+
- `getEntitlement`, `getAllEntitlements`, `subscribeEntitlements` — synchronous,
|
|
241
|
+
in-memory reads safe to call on a render path.
|
|
242
|
+
- Three states: `unknown`, `entitled`, `not_entitled`. `unknown` is both the
|
|
243
|
+
initial state and re-entrant, so a long-offline paying user is never reported
|
|
244
|
+
as unpaid.
|
|
245
|
+
- Many-to-many product↔entitlement mapping. An entitlement stays valid while any
|
|
246
|
+
granting product remains.
|
|
247
|
+
- Source attribution: which of *your* product keys currently grants an
|
|
248
|
+
entitlement.
|
|
249
|
+
|
|
250
|
+
**Selling**
|
|
251
|
+
|
|
252
|
+
- `purchase` with a typed outcome covering success, pending, user cancellation,
|
|
253
|
+
already-owned, ineligible, unknown product, region unavailability, disabled,
|
|
254
|
+
not-ready, unsupported, timeout, network error, store error, environment block,
|
|
255
|
+
and validation rejection.
|
|
256
|
+
- One purchase flow at a time. A second request is refused, never queued.
|
|
257
|
+
- `already_owned` triggers a re-sync, which is how a reinstall repairs itself.
|
|
258
|
+
- Consumables with app-controlled consumption via `consume`.
|
|
259
|
+
- Subscriptions with expiry, auto-renew, and cancelled-but-valid reporting.
|
|
260
|
+
- `changePlan` with four proration modes; `openSubscriptionManagement`;
|
|
261
|
+
`confirmPriceChange`.
|
|
262
|
+
|
|
263
|
+
**Finalization**
|
|
264
|
+
|
|
265
|
+
- A persisted queue drained at startup, on foreground, and after each purchase.
|
|
266
|
+
A purchase survives the app being killed between payment and acknowledgement.
|
|
267
|
+
- Idempotent finalization and consumption.
|
|
268
|
+
- Bounded per-session retry that resumes in the next session rather than
|
|
269
|
+
abandoning a paid transaction.
|
|
270
|
+
- `getUnfinalized()` so "paid but not settled" is observable.
|
|
271
|
+
|
|
272
|
+
**Products and pricing**
|
|
273
|
+
|
|
274
|
+
- `getProducts` / `refreshProducts` returning store-formatted prices, currency
|
|
275
|
+
codes, numeric prices for comparison, billing periods, trials, and
|
|
276
|
+
introductory pricing.
|
|
277
|
+
- Offer eligibility as the store determined it.
|
|
278
|
+
- In-memory cache with a configurable TTL, defaulting to 15 minutes.
|
|
279
|
+
|
|
280
|
+
**Sync, restore, and offline**
|
|
281
|
+
|
|
282
|
+
- Automatic sync at initialization and on foreground, rate-limited.
|
|
283
|
+
- `restore` distinguishing "restored" from "nothing found".
|
|
284
|
+
- Revocation on the next sync when the store stops reporting a transaction.
|
|
285
|
+
- Offline reads from the last successful sync, expiring to `unknown` after a
|
|
286
|
+
configurable ceiling (7 days by default).
|
|
287
|
+
- `clearLocalData` and optional cache-scope partitioning.
|
|
288
|
+
|
|
289
|
+
**Safety**
|
|
290
|
+
|
|
291
|
+
- Fail-closed initialization when a non-production build declares `production`.
|
|
292
|
+
- Runtime block on any chargeable purchase outside production, recorded in
|
|
293
|
+
observable state.
|
|
294
|
+
- No exported way to grant an arbitrary entitlement.
|
|
295
|
+
|
|
296
|
+
**Optional server validation**
|
|
297
|
+
|
|
298
|
+
- A hook that decides entitlement for newly completed purchases.
|
|
299
|
+
- Configurable fallback when the backend is unreachable, defaulting to grant and
|
|
300
|
+
queue for re-validation.
|
|
301
|
+
|
|
302
|
+
**React**
|
|
303
|
+
|
|
304
|
+
- `PaymentProvider`, `useEntitlement`, `useEntitlements`, `usePaymentStatus`,
|
|
305
|
+
`usePurchase`, `useProducts`, `useSync`.
|
|
306
|
+
|
|
307
|
+
**Diagnostics**
|
|
308
|
+
|
|
309
|
+
- 19 typed event types with correlation ids, sufficient to reconstruct a
|
|
310
|
+
transaction's lifecycle from logs alone.
|
|
311
|
+
- 22 stable error codes.
|
|
312
|
+
- A localizable message-key map with English defaults and no i18n dependency.
|
|
313
|
+
|
|
314
|
+
**Testing**
|
|
315
|
+
|
|
316
|
+
- A fake store behind `@chipmobilesdk/rn-payment/testing` driving every outcome,
|
|
317
|
+
both callback orderings, connection loss, refunds, and crash-then-recover
|
|
318
|
+
finalization.
|
|
319
|
+
|
|
320
|
+
### Known limitations
|
|
321
|
+
|
|
322
|
+
- **Grace period and account hold are not separable on Android.** Play returns a
|
|
323
|
+
grace-period subscription as active and stops returning a held one, so
|
|
324
|
+
`renewalState` reports `indeterminate` rather than inventing a distinction. The
|
|
325
|
+
default policy still behaves correctly with no backend; a non-default policy
|
|
326
|
+
needs server data through the validation hook.
|
|
327
|
+
- **iOS is contract-only.** `createStoreProvider()` returns a stub that reports
|
|
328
|
+
`unavailable` on iOS. The host app still starts and runs.
|
|
329
|
+
- **The Android adapter is unverified against an installed binding.** See the
|
|
330
|
+
README's compatibility section.
|
|
331
|
+
|
|
332
|
+
### Migration
|
|
333
|
+
|
|
334
|
+
None — this is a new package.
|