@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 CHANGED
@@ -1,334 +1,482 @@
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.
1
+ # @chipmobilesdk/rn-payment
2
+
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - One diagnostic stream: install a sink with `installSdkSink` from
8
+ `@chipmobilesdk/rn-core` and it receives what every package observes (events,
9
+ failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
10
+ rn-core, so a listener that throws no longer stops the others nor fails the call that
11
+ notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
12
+
13
+ - rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
14
+ events are no longer deduplicated away; the dedupe memory is bounded. A throwing
15
+ status, consent or placement listener is reported as an `internal_error` event.
16
+ - rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
17
+ with a stable `code`); without it, warnings go to `console.warn` in development
18
+ builds only. The warn-once memory is bounded.
19
+ - rn-chart: now depends on rn-core for its diagnostics.
20
+ - rn-payment: status, entitlement and pending listeners report a throw to the logger
21
+ instead of swallowing it.
22
+
23
+ - The package's errors extend `SdkError` from the new `@chipmobilesdk/rn-core`
24
+ dependency: `isSdkError` recognises them alongside every other package's, and each
25
+ carries `pkg`, `code` and `retryable`. Names and codes are unchanged. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
26
+
27
+ - rn-ai and rn-camera: `retryable` is the new name of `recoverable`, which stays as a
28
+ deprecated alias.
29
+ - rn-auth: `retryable` is true for `network-unavailable`. `redact` also removes URLs,
30
+ e-mail addresses and UUIDs.
31
+ - rn-ads: `redact` also removes JWTs and e-mail addresses, and reduces an `AdsError`
32
+ to its code.
33
+ - rn-ai: an error message quoted from the gateway is pattern-redacted.
34
+ - rn-camera: `redact` also removes JWTs, e-mail addresses and UUIDs.
35
+ - rn-theme: `ThemeConfigError` has `code: 'invalid_config'` and `ThemeContrastError`
36
+ `code: 'contrast'` (new `ThemeErrorCode` type).
37
+ - rn-i18n: `I18nConfigError` has `code: 'invalid_config'` and `UnitConversionError`
38
+ `code: 'unit_conversion'` (new `I18nErrorCode` type).
39
+
40
+ - Timers, retries and bounds now come from `@chipmobilesdk/rn-core`, and the drift
41
+ between the copies is fixed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
42
+
43
+ - rn-ads: a retry policy out of bounds throws `retry_out_of_bounds`, as documented,
44
+ instead of `timeout_out_of_bounds`; a placement's `minRequestIntervalMs` out of
45
+ bounds throws `invalid_config`.
46
+ - rn-payment: backoff validates `initialDelayMs` (100 ms to 60 s, not above
47
+ `maxDelayMs`) and `factor` (1 to 10); `factor: 0` used to retry at once, forever.
48
+ Every timeout, backoff or interval out of bounds now throws the new
49
+ `PAYMENT_INVALID_CONFIG` instead of `PAYMENT_INVALID_CATALOG`, which is about
50
+ products.
51
+ - rn-ai: concurrent calls share one behavior-config request, so a cold start makes
52
+ one instead of one per call. A silent retry after a config reload waits
53
+ `recovery.retryBackoffMs`, which was parsed but never applied.
54
+
55
+ - Native peers load through `lazyPeer` from `@chipmobilesdk/rn-core`, on first use,
56
+ and a missing one is each package's own typed error. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
57
+
58
+ - rn-auth: creating `auth` no longer loads react-native-app-auth or
59
+ react-native-keychain; each loads on first use. New entrypoints
60
+ `@chipmobilesdk/rn-auth/app-auth` and `/keychain`. The default platform comes from
61
+ rn-core.
62
+ - rn-camera: a preview whose VisionCamera feed cannot load fails the session with
63
+ `cameraUnavailable` (new `controller.reportSurfaceUnavailable()`), instead of
64
+ showing black with no error. A missing VisionCamera module is `cameraUnavailable`.
65
+ - rn-notification: `createMmkvStore` reports a fallback to memory as
66
+ `STATE_STORE_DEGRADED` (to the SDK sinks and a new `onDegraded` option) and
67
+ exposes `durable`.
68
+ - rn-payment: an iOS app validates its catalog against the iOS product ids; the
69
+ default platform used to be Android everywhere. A missing react-native-iap is
70
+ `PAYMENT_UNAVAILABLE`.
71
+ - rn-theme: `buildThemes` runs without React Native (new `hairlineWidth` option) and
72
+ importing the package no longer loads Unistyles; `applyThemeMode` loads it on
73
+ first use.
74
+ - rn-ads, rn-local-db: the same loading, with the same error codes as before.
75
+
76
+ - Hooks read through `useSyncExternalStore` (rn-core's `useExternalStore`) and
77
+ re-render only when what they return changes. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
78
+
79
+ - rn-auth: `useProfile` and `useAccessTier` no longer re-render for a status change,
80
+ and the provider's context no longer changes on every transition. New
81
+ `useAuthSelector`. `auth.getState()` returns the same frozen object until the state
82
+ changes, instead of a copy per call.
83
+ - rn-payment: `useEntitlement` re-renders only when its own entitlement changes; the
84
+ engine keeps unchanged entitlement objects across syncs and does not notify for a
85
+ sync that changed nothing.
86
+ - rn-ads: `usePlacementState` and `useAdsStatus` read tear-free; `useAdsEvents` keeps
87
+ one subscription when given an inline listener.
88
+ - rn-i18n: `useTranslation` returns the same `t` until the locale changes, and
89
+ `useLocale`'s `setLocale` is stable.
90
+ - rn-ai: new `useAiBehaviorState`, with `client.subscribe` and a stable
91
+ `client.getBehaviorState()`.
92
+
93
+ - Every package's own copy is an rn-core message catalog that an app can translate. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
94
+
95
+ - rn-i18n: new `runtimeEntriesFromCatalog(catalog)` registers any SDK catalog in one
96
+ line, converting `{name}` variables to `{{name}}`.
97
+ - rn-ai: new `messageKeyForError(error)` gives the copy for every error code, and
98
+ `aiMessageSet`. The input placeholder default is neutral ("Type a message…"), and
99
+ two keys were added for errors that had none (`ai.error.signIn`, `ai.error.generic`).
100
+ - rn-auth: new `authMessages`, the catalog split into user and developer copy.
101
+ - rn-payment: new `paymentMessages`.
102
+ - rn-chart: summaries, announcements, adjustable-action labels, built-in state text
103
+ and legend hints come from `chartStrings`; pass `strings` to translate them. The
104
+ `accessibility.summaryFormatter` prop, declared before, now replaces the summary.
105
+
106
+ - Port conformance suites: one list of checks that a package's fake and its real adapter
107
+ both pass, so a test written against the fake proves something about the device. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
108
+
109
+ - rn-core: `@chipmobilesdk/rn-core/testing` adds `runChecks`, `expectConformance`,
110
+ `ensure` and `createFaultRegistry` (deterministic fault injection by named point).
111
+ - rn-camera: a `./testing` entrypoint with `runPermissionPortConformance`,
112
+ `runCameraEngineConformance` and the test adapter, which stays on the main
113
+ entrypoint too until the next major. `createTestCamera(...).prompts()` counts system
114
+ prompts.
115
+ - rn-payment: `runProviderPortConformance` in `./testing`, passed by the fake store and
116
+ the Play adapter.
117
+ - rn-ads: `runAdsEngineConformance` in `./testing`, passed by the fake engine and the
118
+ AdMob adapter.
119
+ - rn-auth: a `./testing` entrypoint with `createMemorySecureStore`,
120
+ `runSecureTokenStoreConformance` and `runOidcAuthClientConformance`, passed by the
121
+ in-memory doubles and the keychain and app-auth adapters.
122
+
123
+ - One app-lifecycle source, and a startup that needs no extra frame. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
124
+
125
+ - rn-core: `appLifecycle()`, a shared, reference-counted `AppState` subscription that
126
+ reports changes only (`active`, `inactive`, `background`), with `onForeground` for a
127
+ return from the background and `createManualLifecycle` in `./testing`.
128
+ - rn-camera, rn-ads and rn-notification hear the lifecycle through it instead of
129
+ subscribing to `AppState` each, so an app holds one native listener. The camera
130
+ controller's core layer no longer imports `react-native`.
131
+ - rn-payment: `syncOnForeground(engine)` and `<PaymentProvider syncOnForeground>` call
132
+ `handleForeground()` on each return from the background. Off by default; calling
133
+ `handleForeground()` yourself works as before.
134
+ - rn-i18n: `i18n.initSync()` resolves the locale before the first render when the
135
+ storage adapter has the new optional `getItemSync`, and returns false otherwise.
136
+ `localeStorageFromKeyValue(port)` builds such an adapter from rn-core's
137
+ `KeyValuePort` (MMKV).
138
+
139
+ ### Patch Changes
140
+
141
+ - Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
142
+ - @chipmobilesdk/rn-core@0.1.0
143
+
144
+ ## 0.4.1
145
+
146
+ ### Patch Changes
147
+
148
+ - Packaging fixes; no code change. ([#14](https://github.com/operationd8979/chip-mobile-sdk/pull/14))
149
+
150
+ - `react-native-nitro-modules` is no longer a peer dependency. The package never
151
+ imports it; `react-native-iap` declares it as its own peer, and the README still
152
+ tells apps to install it.
153
+ - `react-native-mmkv` is now `>=4.0.0 <5`. The store is written against v4's
154
+ `createMMKV`; there is no v5 yet, so no app is affected.
155
+ - `publishConfig` declares public access on npmjs, like every other package. It
156
+ had none, so it only published publicly because publishing was done by hand.
157
+
158
+ ## [0.4.0] — 2026-11-01
159
+
160
+ Minor release hardening the settlement path. **Upgrading requires no code change**;
161
+ the API additions are optional, and the error-code union gains a member
162
+ (`PAYMENT_NOT_CONSUMABLE`), so an exhaustive `switch` over it needs one more case.
163
+
164
+ ### Fixed
165
+
166
+ - **A purchase on Android is settled once.** The Play binding delivers a launched
167
+ purchase both to the launch and to the session-long update listener, and both paths
168
+ settled it: the validation hook ran twice and `purchase_pending` fired twice.
169
+ Settlement is now single-flight per transaction, and the pending signal fires once
170
+ per transaction.
171
+ - **Nothing that never answers can wedge the engine.** `timeouts.finalizeAttemptMs`
172
+ was validated but never applied; acknowledge and consume now honour it, and a
173
+ timed-out attempt is retried. The validation hook is bounded by the new
174
+ `timeouts.validateMs` and treated as unavailable when it runs out. Finalize drain and
175
+ re-validation run after the sync flight rather than inside it, so a stuck call no
176
+ longer makes every later sync join it. A store-timed purchase flow gets a
177
+ ten-minute ceiling so the purchase lock is always released.
178
+ - **`consume` refuses what it must not consume.** A subscription or permanent unlock
179
+ returns `{ kind: 'failed', code: 'PAYMENT_NOT_CONSUMABLE' }`, a pending purchase
180
+ `PAYMENT_PENDING`, and no store call is made. A failed consume is now retried by
181
+ consuming; it was retried through the acknowledge path, which skips consumables.
182
+ - **A second `initialize()` after success is a no-op.** It reconnected and left a
183
+ second set of native listeners behind, so every update was ingested twice. The Play
184
+ adapter also removes its previous native listeners on every connect.
185
+ - **An exhausted reconnect budget is recoverable.** The engine stayed at `connecting`
186
+ forever. It now reports `{ kind: 'failed', retryable: true }`, and
187
+ `handleForeground()` starts a fresh budget with one immediate attempt.
188
+ - **Re-validation identifies the transaction.** It sent an empty payload and no
189
+ identifier, so a backend could not tell which purchase it was asked about.
190
+ - **The logger no longer receives raw error text.** `redact` passed `Error` messages
191
+ and strings through verbatim; it now keeps only a `PaymentError` code or an error's
192
+ class name.
193
+
194
+ ### Added
195
+
196
+ - `ValidationCandidate.transactionId` and `ValidationCandidate.platform`.
197
+ - `TimeoutConfig.validateMs` (3–120 s, default 15 s).
198
+ - Error code `PAYMENT_NOT_CONSUMABLE`.
199
+ - `./testing`: `createFakeStore({ mode: 'android-16.7' })`, which delivers a launched
200
+ purchase through `observePurchases` as the Play binding does and reports Android's
201
+ capabilities.
202
+
203
+ ## [0.3.1] — 2026-10-25
204
+
205
+ Patch release. **Upgrade before shipping on Android**: with `0.3.0` no purchase could
206
+ complete on a device. Found on a Galaxy S25 Ultra with a licence-tester account, in the
207
+ first app to buy through this package (Task Manager, internal testing track).
208
+
209
+ ### Fixed
210
+
211
+ - **Every purchase failed before Play opened.** The adapter sent
212
+ `request: { android: { skus, obfuscatedAccountIdAndroid } }`, the request shape from
213
+ `react-native-iap`'s documentation before 16.7. The 16.7 binding reads
214
+ `request.google` and rejected the call with `empty-sku-list`; the app saw a store
215
+ error. The request now uses `google` and `obfuscatedAccountId`, and is typed with the
216
+ binding's own `RequestPurchaseProps`, so a renamed key fails to typecheck.
217
+ - **A completed purchase would have been reported as a store error.** On Android
218
+ `requestPurchase` resolves with an empty list when the flow ends, whatever happened;
219
+ the outcome arrives only through `purchaseUpdatedListener` / `purchaseErrorListener`.
220
+ The adapter read the resolved value, so it would have reported `store_error` for
221
+ every purchase, while Play delivered the purchase separately. `launchPurchase` now
222
+ settles on the first outcome event for the product (bought, pending, or an error).
223
+ - **"Already owned" was a generic store error.** The adapter matched
224
+ `item-already-owned`; 16.7 sends `already-owned`. A reinstall that taps Buy again
225
+ now gets `already_owned` and the self-repairing sync (FR-033). `sku-not-found` now
226
+ maps to `unknown_product`.
227
+ - **The launch timeout cut into the user's time in Play (FR-035).** Because the
228
+ binding reports nothing until the flow ends, the 30-second "opening the store
229
+ surface" timeout covered the time the user spends choosing a payment method. The
230
+ engine no longer applies it to a provider whose outcome includes that time (the Play
231
+ adapter); the binding always ends the flow with an event, so the call cannot hang.
232
+ - **Plan changes sent the removed 16.6 fields.** A plan change now sends the replaced
233
+ purchase's token as `purchaseToken` and, with a proration mode,
234
+ `subscriptionProductReplacementParams` (old product id + replacement mode).
235
+
236
+ ### Changed
237
+
238
+ - Peer `react-native-iap` is now `>=16.7.3 <17`, the version this adapter was verified
239
+ against (was `>=16.6.1 <17`, never verified).
240
+ - Internal: `ProviderPurchaseRequest.replaceStoreProductId` now holds the replaced
241
+ product's id and the token moved to `replacePurchaseToken` (it held the token before);
242
+ `ProviderCapabilities` gained `purchaseOutcomeIncludesStoreTime`. Neither type is
243
+ public API. The fake store keeps `false`, so launch-timeout tests behave as before.
244
+
245
+ ### Added
246
+
247
+ - `__tests__/iap-adapter.test.ts`: the adapter against a fake binding that validates
248
+ and resolves as 16.7.3 does — request shape, event-based outcomes, error mapping, the
249
+ no-event fallback.
250
+
251
+ ## [0.3.0] — 2026-10-13
252
+
253
+ Documentation release: the README and the agent skill now match the code `0.2.1` shipped.
254
+ **No code, API, storage or behaviour change** — upgrading from `0.2.1` changes nothing at
255
+ runtime. Released as its own version so the skill a consuming app syncs (`packageVersion`)
256
+ always matches the package it came from.
257
+
258
+ ### Changed
259
+
260
+ - README `### Migrating 0.1.0 to 0.2.0`: no longer claims `0.1.0` persisted pending records.
261
+ On a device nothing persisted before `0.2.1`; the section now says to use `0.2.1` or
262
+ later and what a device first running it does (empty record, `unknown` until the first
263
+ sync, never a false `not_entitled`).
264
+ - README intro: "Android only for now" instead of a version-pinned phrase.
265
+ - Skill: "Require `0.2.1` or later" under the upgrade notes, with the reason; the public
266
+ API heading names `v0.3.0` (the surface is unchanged since `0.2.0`).
267
+
268
+ ## [0.2.1] — 2026-10-12
269
+
270
+ Patch release. **Upgrade before shipping**: on a device, `0.2.0` and earlier persisted
271
+ nothing.
272
+
273
+ ### Fixed
274
+
275
+ - **The purchase snapshot never reached the device's storage.** The store opened MMKV
276
+ with `new MMKV()`, the react-native-mmkv **v3** constructor. In v4 — the only major the
277
+ peer range (`>=4.0.0`) allows — `MMKV` is a type, not a value, so the call threw and the
278
+ store fell back to memory on every device. Consequences: entitlements read `unknown`
279
+ after every cold start until a sync, an owner with no connection stayed `unknown`, and a
280
+ pending purchase was forgotten after an app kill. The store now uses v4's `createMMKV`
281
+ and `remove`. Found on an emulator: the app's `files/mmkv` held no
282
+ `chipmobilesdk.payment` instance.
283
+ - The engine did report the fallback, as a `config_warning` ("Local storage
284
+ unavailable"), but nothing in the tests could trigger it: the repository's test double
285
+ exported a v3 `MMKV` class. The double now mirrors v4 exactly, and the persistence suite
286
+ fails against the old code.
287
+
288
+ No API, storage format or behaviour change otherwise. Records written by a working
289
+ install are read as before; there were none from `0.2.0` on a device to migrate.
290
+
291
+ ## [0.2.0] — 2026-10-12
292
+
293
+ Additive minor release closing one gap that surfaced when `0.1.0` met an app that
294
+ must show a pending payment after a restart. It is a **capability** gap: `0.1.0`'s
295
+ engine refused to grant a pending purchase, but an app could only *see* one in the
296
+ session that started it. Reconstructing the pending set meant importing the billing
297
+ library, which this package exists to prevent.
298
+
299
+ It also fixes a **serious defect on Android devices**: a pending purchase was
300
+ granted. See [Fixed](#fixed). **Upgrade from `0.1.0` before shipping.**
301
+
302
+ **Upgrading requires one change, compile-time only:** `PaymentEventType` gained a
303
+ member. See [Migration](#migration).
304
+
305
+ ### Added
306
+
307
+ **Pending purchases, visible across sessions**
308
+
309
+ - `getPendingPurchases()` and `subscribePendingPurchases(listener)` on the engine,
310
+ and `usePendingPurchases(productKeys?)` in React. They return a
311
+ `PendingPurchasesState`: a `status` (`known` | `unknown`), an `isStale` flag, and
312
+ `purchases`, one `{ productKey, since }` per product. No transaction id, token or
313
+ store object.
314
+ - Read from the persisted snapshot, so the set survives an app kill and an offline
315
+ restart. It is judged by the **same staleness rule as entitlements**, now shared
316
+ code rather than a parallel copy: never synced, or older than `staleAfterMs`,
317
+ reports `unknown`, never an empty "nothing pending".
318
+ - Pushed only when the set, its status, or its staleness changes. A sync that finds
319
+ nothing new re-renders nothing.
320
+
321
+ **How a pending purchase ended**
322
+
323
+ - `purchase_pending_resolved` event, `{ productKey, outcome }`, with `outcome`
324
+ `settled` | `cancelled` | `expired`, emitted once per pending transaction.
325
+ `settled` precedes the `entitlement_granted` it leads to.
326
+ - Detected from the snapshot commit itself, so it is exactly-once by construction:
327
+ a store push and a sync racing on one settlement emit one event, a sync that
328
+ changed nothing emits none, and `clearLocalData()` emits none, because discarding
329
+ a record locally is not the store ending it.
330
+ - **On Android every unpaid end is `cancelled`.** Play does not say whether a pending
331
+ purchase was declined or expired; it stops reporting it. The package does not
332
+ guess from elapsed time. `expired` is reported only by a store that says so,
333
+ through a new optional `pendingEndReason` on the internal provider port.
334
+
335
+ **Testing**
336
+
337
+ - Fake store: `settlePending`, `cancelPending` and `expirePending` change store-side
338
+ state the way the real store does, so the engine sees the change at its next sync,
339
+ in the same session or after a simulated app kill.
340
+
341
+ ### Fixed
342
+
343
+ - **On a device, a pending purchase was granted.** The Android adapter read only
344
+ Play's numeric purchase state and treated anything else as purchased. The
345
+ `react-native-iap` 16.x binding hands JavaScript the state as a **string**
346
+ (`'pending' | 'purchased' | 'unknown'`) and does not carry the numeric one across,
347
+ so every pending purchase fell through to "purchased": entitlement granted, and an
348
+ acknowledgement attempted, for money that had not arrived. The engine's own
349
+ "pending never grants" rule was intact; the adapter fed it the wrong state. No
350
+ test could see it, because tests drive the fake store, which bypasses the adapter.
351
+ The adapter now reads either form, and anything it does not recognize is
352
+ `unspecified`, never purchased. Checked against the installed 16.7.3 declarations
353
+ and its JavaScript bridge.
354
+ - **An `unspecified` purchase state is never treated as paid.** `0.1.0` persisted
355
+ Play's unknown state as `purchased`, granting it and queuing an acknowledgement.
356
+ Now a new transaction in that state is left out until the store says what it is,
357
+ a known one keeps what the package already knew (uncertainty is neither a grant nor
358
+ a refund), a store push in that state is ignored, and `purchase()` returning one
359
+ answers `store_error`, retryable: if the money did move, a retry gets
360
+ `already_owned`, which re-syncs and grants.
361
+
362
+ ### Unchanged, deliberately
363
+
364
+ - **Pending never grants**, under any configuration. The new surface is read-only.
365
+ - **No storage migration.** The schema stays at version 1. `0.1.0` already persisted
366
+ pending records; `0.2.0` only reads them.
367
+ - `purchase()` still returns `pending`, and `purchase_pending` still fires, exactly
368
+ as before.
369
+
370
+ ### Migration
371
+
372
+ | Union | New member |
373
+ |---|---|
374
+ | `PaymentEventType` / `PaymentEvent` | `'purchase_pending_resolved'` |
375
+
376
+ An exhaustive `switch` over either needs the new case or a `default`. A
377
+ hand-written `PaymentEngine`, typically a test mock, needs `getPendingPurchases` and
378
+ `subscribePendingPurchases`. Nothing else changes.
379
+
380
+ ## [0.1.0] — 2026-10-06
381
+
382
+ First version. Android only, via Google Play Billing.
383
+
384
+ ### Added
385
+
386
+ **Entitlements**
387
+
388
+ - `getEntitlement`, `getAllEntitlements`, `subscribeEntitlements` — synchronous,
389
+ in-memory reads safe to call on a render path.
390
+ - Three states: `unknown`, `entitled`, `not_entitled`. `unknown` is both the
391
+ initial state and re-entrant, so a long-offline paying user is never reported
392
+ as unpaid.
393
+ - Many-to-many product↔entitlement mapping. An entitlement stays valid while any
394
+ granting product remains.
395
+ - Source attribution: which of *your* product keys currently grants an
396
+ entitlement.
397
+
398
+ **Selling**
399
+
400
+ - `purchase` with a typed outcome covering success, pending, user cancellation,
401
+ already-owned, ineligible, unknown product, region unavailability, disabled,
402
+ not-ready, unsupported, timeout, network error, store error, environment block,
403
+ and validation rejection.
404
+ - One purchase flow at a time. A second request is refused, never queued.
405
+ - `already_owned` triggers a re-sync, which is how a reinstall repairs itself.
406
+ - Consumables with app-controlled consumption via `consume`.
407
+ - Subscriptions with expiry, auto-renew, and cancelled-but-valid reporting.
408
+ - `changePlan` with four proration modes; `openSubscriptionManagement`;
409
+ `confirmPriceChange`.
410
+
411
+ **Finalization**
412
+
413
+ - A persisted queue drained at startup, on foreground, and after each purchase.
414
+ A purchase survives the app being killed between payment and acknowledgement.
415
+ - Idempotent finalization and consumption.
416
+ - Bounded per-session retry that resumes in the next session rather than
417
+ abandoning a paid transaction.
418
+ - `getUnfinalized()` so "paid but not settled" is observable.
419
+
420
+ **Products and pricing**
421
+
422
+ - `getProducts` / `refreshProducts` returning store-formatted prices, currency
423
+ codes, numeric prices for comparison, billing periods, trials, and
424
+ introductory pricing.
425
+ - Offer eligibility as the store determined it.
426
+ - In-memory cache with a configurable TTL, defaulting to 15 minutes.
427
+
428
+ **Sync, restore, and offline**
429
+
430
+ - Automatic sync at initialization and on foreground, rate-limited.
431
+ - `restore` distinguishing "restored" from "nothing found".
432
+ - Revocation on the next sync when the store stops reporting a transaction.
433
+ - Offline reads from the last successful sync, expiring to `unknown` after a
434
+ configurable ceiling (7 days by default).
435
+ - `clearLocalData` and optional cache-scope partitioning.
436
+
437
+ **Safety**
438
+
439
+ - Fail-closed initialization when a non-production build declares `production`.
440
+ - Runtime block on any chargeable purchase outside production, recorded in
441
+ observable state.
442
+ - No exported way to grant an arbitrary entitlement.
443
+
444
+ **Optional server validation**
445
+
446
+ - A hook that decides entitlement for newly completed purchases.
447
+ - Configurable fallback when the backend is unreachable, defaulting to grant and
448
+ queue for re-validation.
449
+
450
+ **React**
451
+
452
+ - `PaymentProvider`, `useEntitlement`, `useEntitlements`, `usePaymentStatus`,
453
+ `usePurchase`, `useProducts`, `useSync`.
454
+
455
+ **Diagnostics**
456
+
457
+ - 19 typed event types with correlation ids, sufficient to reconstruct a
458
+ transaction's lifecycle from logs alone.
459
+ - 22 stable error codes.
460
+ - A localizable message-key map with English defaults and no i18n dependency.
461
+
462
+ **Testing**
463
+
464
+ - A fake store behind `@chipmobilesdk/rn-payment/testing` driving every outcome,
465
+ both callback orderings, connection loss, refunds, and crash-then-recover
466
+ finalization.
467
+
468
+ ### Known limitations
469
+
470
+ - **Grace period and account hold are not separable on Android.** Play returns a
471
+ grace-period subscription as active and stops returning a held one, so
472
+ `renewalState` reports `indeterminate` rather than inventing a distinction. The
473
+ default policy still behaves correctly with no backend; a non-default policy
474
+ needs server data through the validation hook.
475
+ - **iOS is contract-only.** `createStoreProvider()` returns a stub that reports
476
+ `unavailable` on iOS. The host app still starts and runs.
477
+ - **The Android adapter is unverified against an installed binding.** See the
478
+ README's compatibility section.
479
+
480
+ ### Migration
481
+
482
+ None — this is a new package.