@nosslabs/iap 7.0.0-next.0 → 7.0.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
@@ -5,6 +5,71 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [7.0.0] — 2026-05-14
9
+
10
+ **GA of the Capacitor 7+ line.** `@latest` on npm moves from `5.0.0`
11
+ (Cap-5 maintenance) to `7.0.0` (Cap-7+). Cap-5 consumers stay on
12
+ `5.x` — `^5` ranges don't auto-resolve to `7.x`. The `5.x` maintenance
13
+ branch continues to receive patches.
14
+
15
+ ### Changed (BREAKING, vs `5.0.0` — bundled with the Cap-5→Cap-7 swap)
16
+
17
+ - **EventMap pruning.** Removed two events from the public
18
+ `EventMap` that were declared but never emitted in any prior
19
+ release: `'price-stale'` and `'error'`. Subscriptions to either
20
+ never fired, so no runtime behavior changes — only consumers who
21
+ had `iap.on('price-stale', …)` or `iap.on('error', …)` in their
22
+ TypeScript code need to remove those calls. The
23
+ `'recovery-dropped-permanent'` event (introduced in 0.4 / `5.0.0`)
24
+ remains.
25
+ - `IAPErrorOptions` is now file-local (was wrongly exported from
26
+ `src/lib/errors.ts` but never re-exported through `src/index.ts`,
27
+ so no consumer had access to it). No package-root-exported symbol
28
+ changes.
29
+
30
+ ### Added
31
+
32
+ - **`AppUserIdFetcherContext`** is now re-exported from the package
33
+ root, so a separately-defined async fetcher can be typed
34
+ explicitly:
35
+
36
+ ```ts
37
+ import type { AppUserIdFetcherContext } from '@nosslabs/iap';
38
+
39
+ const fetchUuid = async ({ authHeaders }: AppUserIdFetcherContext) => {
40
+ const r = await fetch('/api/iap/uuid', { method: 'POST', headers: authHeaders });
41
+ return (await r.json()).uuid;
42
+ };
43
+ ```
44
+
45
+ ### Fixed (since `7.0.0-next.0`)
46
+
47
+ - **iOS `Cannot find product for id <id>` now maps to
48
+ `PRODUCT_NOT_FOUND`** (previously fell through to `STORE_ERROR`).
49
+ The capgo plugin uses two different messages for the same
50
+ semantic on iOS vs Android; the adapter now handles both.
51
+ - **`refresh()` is safe to detach from the IAP instance.** Internal
52
+ callbacks no longer reference `this.refresh()`, so
53
+ `const { refresh } = iap;` works without a strict-mode `this`
54
+ binding error. Regression test added.
55
+
56
+ ### Notes
57
+
58
+ - Adapter JSDoc for `getOwnedTransactions()` now documents an iOS
59
+ quirk worth knowing: `@capgo/native-purchases`'s `getPurchases()`
60
+ bundles `Transaction.currentEntitlements` *plus* `Transaction.all`
61
+ (historical + revoked subscriptions). Android-side PENDING
62
+ purchases are filtered out (`purchaseState !== '1'`); iOS-side
63
+ historical transactions are not filtered and pass through to the
64
+ backend's `/restore` endpoint. Attesto evaluates each receipt and
65
+ returns per-transaction validity, so this is the documented
66
+ contract.
67
+ - Android user-cancellation reminder (carried from `7.0.0-next.0`):
68
+ Google Play Billing collapses user-cancel and other billing
69
+ errors into the same plugin rejection, so an Android cancel
70
+ surfaces as `status: 'failed'` rather than `'cancelled'`. iOS
71
+ still distinguishes reliably.
72
+
8
73
  ## [7.0.0-next.0] — 2026-05-14
9
74
 
10
75
  First release of the **Capacitor 7+** line, published on the `@next`
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  > Thin Capacitor IAP orchestrator. Server-side validation via [Attesto](https://attesto.nossdev.com).
4
4
 
5
- **Status: `7.0.0-next.0` — prerelease on the `@next` dist-tag** (the Capacitor 7+ line, built on `@capgo/native-purchases`). The Capacitor 5 line (`cordova-plugin-purchase`) continues as `5.x` on `@latest` (from the `5.x` branch). API may have breaking changes through the 7.x prerelease line; watch the [CHANGELOG](./CHANGELOG.md).
5
+ **Status: `7.0.0` — GA on `@latest`** (the Capacitor 7+ line, built on `@capgo/native-purchases`). The Capacitor 5 line (`cordova-plugin-purchase`) continues as `5.x` from the `5.x` branch `^5` ranges still resolve to `5.x`. See the [CHANGELOG](./CHANGELOG.md) for the GA delta and [Migration](https://iap.nossdev.com/migration/) for upgrading from `5.x`.
6
6
 
7
7
  ```bash
8
- npm install @nosslabs/iap@next @capgo/native-purchases
8
+ npm install @nosslabs/iap @capgo/native-purchases
9
9
  npx cap sync
10
10
  ```
11
11
 
@@ -77,8 +77,8 @@ It does **not**: talk to Attesto directly, define entitlement business logic, ma
77
77
 
78
78
  | `@nosslabs/iap` | Capacitor | Native plugin | dist-tag | Status |
79
79
  |---|---|---|---|---|
80
- | 7.x | 7.x (also runs on 8.x) | `@capgo/native-purchases 7.16.x` (or `^8` on Cap 8) | `@next` | **Current (prerelease)** |
81
- | 5.x | 5.x | `cordova-plugin-purchase ^13.x` | `@latest` | Maintenance |
80
+ | 7.x | 7.x (also runs on 8.x) | `@capgo/native-purchases 7.16.x` (or `^8` on Cap 8) | `@latest` | **Current** |
81
+ | 5.x | 5.x | `cordova-plugin-purchase ^13.x` | (pinned via `^5`) | Maintenance |
82
82
 
83
83
  ## Optional peer dependency
84
84
 
package/dist/index.cjs CHANGED
@@ -202,7 +202,7 @@ function mapPurchaseError(error, productId) {
202
202
  cause: error
203
203
  });
204
204
  }
205
- if (lower.includes("product not found")) {
205
+ if (lower.includes("product not found") || lower.includes("cannot find product")) {
206
206
  return new exports.IAPError({
207
207
  code: exports.IAPErrorCode.PRODUCT_NOT_FOUND,
208
208
  message: `Product "${productId}" was not found in the store catalog.`,
@@ -1737,6 +1737,24 @@ function createIAP(input) {
1737
1737
  cachedAt: null,
1738
1738
  products: Object.freeze([...config.products ?? []])
1739
1739
  };
1740
+ async function refreshEntitlements() {
1741
+ requireInitialized(state);
1742
+ const previous = state.entitlements;
1743
+ const fetched = await state.backend.getEntitlements();
1744
+ const next = freezeAll(fetched);
1745
+ try {
1746
+ state.cachedAt = await state.cache.save(next);
1747
+ } catch (error) {
1748
+ state.logger.warn(
1749
+ "Failed to persist refreshed entitlements; in-memory state still updated.",
1750
+ error
1751
+ );
1752
+ }
1753
+ state.entitlements = next;
1754
+ if (!entitlementsEqual(previous, next)) {
1755
+ state.emitter.emit("entitlements-changed", { entitlements: next, previous });
1756
+ }
1757
+ }
1740
1758
  return {
1741
1759
  async initialize() {
1742
1760
  if (state.destroyed) {
@@ -1838,7 +1856,7 @@ function createIAP(input) {
1838
1856
  logger: state.logger,
1839
1857
  onResume: async () => {
1840
1858
  try {
1841
- await this.refresh();
1859
+ await refreshEntitlements();
1842
1860
  } catch (error) {
1843
1861
  state.logger.warn("refreshOnResume: refresh() failed.", error);
1844
1862
  }
@@ -1849,7 +1867,7 @@ function createIAP(input) {
1849
1867
  state.logger.debug("Cache exceeds TTL; scheduling background refresh.");
1850
1868
  queueMicrotask(() => {
1851
1869
  if (!state.initialized || state.destroyed) return;
1852
- this.refresh().catch((error) => {
1870
+ refreshEntitlements().catch((error) => {
1853
1871
  state.logger.warn("TTL background refresh failed.", error);
1854
1872
  });
1855
1873
  });
@@ -1857,24 +1875,7 @@ function createIAP(input) {
1857
1875
  state.initialized = true;
1858
1876
  state.emitter.emit("ready", void 0);
1859
1877
  },
1860
- async refresh() {
1861
- requireInitialized(state);
1862
- const previous = state.entitlements;
1863
- const fetched = await state.backend.getEntitlements();
1864
- const next = freezeAll(fetched);
1865
- try {
1866
- state.cachedAt = await state.cache.save(next);
1867
- } catch (error) {
1868
- state.logger.warn(
1869
- "Failed to persist refreshed entitlements; in-memory state still updated.",
1870
- error
1871
- );
1872
- }
1873
- state.entitlements = next;
1874
- if (!entitlementsEqual(previous, next)) {
1875
- state.emitter.emit("entitlements-changed", { entitlements: next, previous });
1876
- }
1877
- },
1878
+ refresh: refreshEntitlements,
1878
1879
  async destroy() {
1879
1880
  if (state.destroyed) return;
1880
1881
  state.destroyed = true;