@nosslabs/iap 7.0.0-next.0 → 7.1.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,95 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [7.1.0] — 2026-07-04
9
+
10
+ ### Added
11
+
12
+ - `iap.getStorefront(): Promise<Storefront | null>` — reads the user's App
13
+ Store / Google Play storefront (the country their store account is registered
14
+ to). Returns the new exported `Storefront` type, with `countryCode` normalized
15
+ to ISO 3166-1 alpha-2 across platforms (iOS reports alpha-3, Android alpha-2);
16
+ the raw native value is preserved on `countryCodeRaw`, plus the Apple
17
+ `storefrontId` (iOS only) and `platform`. Resolves `null` on web, when the
18
+ installed `@capgo/native-purchases` build doesn't register the native
19
+ `getStorefront` method, or when the storefront is unavailable (e.g. EU
20
+ alternative distribution). Read it live and treat it as a UX/targeting hint —
21
+ for compliance/entitlement decisions, trust the server-side signed storefront.
22
+
23
+ ### Changed
24
+
25
+ - Anchored the `7.x` line to **Capacitor 7**: peer dependencies narrowed to
26
+ `@capacitor/*: ^7.0.0` and `@capgo/native-purchases: ^7.16.2` (dropping the
27
+ `^8.0.0` allowances). The `^7.16.2` range admits the Capacitor-7 capgo build
28
+ that adds native `getStorefront`. Capacitor-8 support will ship in
29
+ `@nosslabs/iap` v8. (`getStorefront()` itself still degrades gracefully on
30
+ capgo builds that predate the native method.)
31
+
32
+ ## [7.0.0] — 2026-05-14
33
+
34
+ **GA of the Capacitor 7+ line.** `@latest` on npm moves from `5.0.0`
35
+ (Cap-5 maintenance) to `7.0.0` (Cap-7+). Cap-5 consumers stay on
36
+ `5.x` — `^5` ranges don't auto-resolve to `7.x`. The `5.x` maintenance
37
+ branch continues to receive patches.
38
+
39
+ ### Changed (BREAKING, vs `5.0.0` — bundled with the Cap-5→Cap-7 swap)
40
+
41
+ - **EventMap pruning.** Removed two events from the public
42
+ `EventMap` that were declared but never emitted in any prior
43
+ release: `'price-stale'` and `'error'`. Subscriptions to either
44
+ never fired, so no runtime behavior changes — only consumers who
45
+ had `iap.on('price-stale', …)` or `iap.on('error', …)` in their
46
+ TypeScript code need to remove those calls. The
47
+ `'recovery-dropped-permanent'` event (introduced in 0.4 / `5.0.0`)
48
+ remains.
49
+ - `IAPErrorOptions` is now file-local (was wrongly exported from
50
+ `src/lib/errors.ts` but never re-exported through `src/index.ts`,
51
+ so no consumer had access to it). No package-root-exported symbol
52
+ changes.
53
+
54
+ ### Added
55
+
56
+ - **`AppUserIdFetcherContext`** is now re-exported from the package
57
+ root, so a separately-defined async fetcher can be typed
58
+ explicitly:
59
+
60
+ ```ts
61
+ import type { AppUserIdFetcherContext } from '@nosslabs/iap';
62
+
63
+ const fetchUuid = async ({ authHeaders }: AppUserIdFetcherContext) => {
64
+ const r = await fetch('/api/iap/uuid', { method: 'POST', headers: authHeaders });
65
+ return (await r.json()).uuid;
66
+ };
67
+ ```
68
+
69
+ ### Fixed (since `7.0.0-next.0`)
70
+
71
+ - **iOS `Cannot find product for id <id>` now maps to
72
+ `PRODUCT_NOT_FOUND`** (previously fell through to `STORE_ERROR`).
73
+ The capgo plugin uses two different messages for the same
74
+ semantic on iOS vs Android; the adapter now handles both.
75
+ - **`refresh()` is safe to detach from the IAP instance.** Internal
76
+ callbacks no longer reference `this.refresh()`, so
77
+ `const { refresh } = iap;` works without a strict-mode `this`
78
+ binding error. Regression test added.
79
+
80
+ ### Notes
81
+
82
+ - Adapter JSDoc for `getOwnedTransactions()` now documents an iOS
83
+ quirk worth knowing: `@capgo/native-purchases`'s `getPurchases()`
84
+ bundles `Transaction.currentEntitlements` *plus* `Transaction.all`
85
+ (historical + revoked subscriptions). Android-side PENDING
86
+ purchases are filtered out (`purchaseState !== '1'`); iOS-side
87
+ historical transactions are not filtered and pass through to the
88
+ backend's `/restore` endpoint. Attesto evaluates each receipt and
89
+ returns per-transaction validity, so this is the documented
90
+ contract.
91
+ - Android user-cancellation reminder (carried from `7.0.0-next.0`):
92
+ Google Play Billing collapses user-cancel and other billing
93
+ errors into the same plugin rejection, so an Android cancel
94
+ surfaces as `status: 'failed'` rather than `'cancelled'`. iOS
95
+ still distinguishes reliably.
96
+
8
97
  ## [7.0.0-next.0] — 2026-05-14
9
98
 
10
99
  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
@@ -139,11 +139,293 @@ var init_platform = __esm({
139
139
  }
140
140
  });
141
141
 
142
+ // src/lib/iso-country.ts
143
+ function toAlpha2(code) {
144
+ if (!code) return null;
145
+ const normalized = code.trim().toUpperCase();
146
+ if (normalized.length === 2) return normalized;
147
+ if (normalized.length === 3) return ALPHA3_TO_ALPHA2[normalized] ?? null;
148
+ return null;
149
+ }
150
+ var ALPHA3_TO_ALPHA2;
151
+ var init_iso_country = __esm({
152
+ "src/lib/iso-country.ts"() {
153
+ ALPHA3_TO_ALPHA2 = {
154
+ ABW: "AW",
155
+ AFG: "AF",
156
+ AGO: "AO",
157
+ AIA: "AI",
158
+ ALA: "AX",
159
+ ALB: "AL",
160
+ AND: "AD",
161
+ ARE: "AE",
162
+ ARG: "AR",
163
+ ARM: "AM",
164
+ ASM: "AS",
165
+ ATA: "AQ",
166
+ ATF: "TF",
167
+ ATG: "AG",
168
+ AUS: "AU",
169
+ AUT: "AT",
170
+ AZE: "AZ",
171
+ BDI: "BI",
172
+ BEL: "BE",
173
+ BEN: "BJ",
174
+ BES: "BQ",
175
+ BFA: "BF",
176
+ BGD: "BD",
177
+ BGR: "BG",
178
+ BHR: "BH",
179
+ BHS: "BS",
180
+ BIH: "BA",
181
+ BLM: "BL",
182
+ BLR: "BY",
183
+ BLZ: "BZ",
184
+ BMU: "BM",
185
+ BOL: "BO",
186
+ BRA: "BR",
187
+ BRB: "BB",
188
+ BRN: "BN",
189
+ BTN: "BT",
190
+ BVT: "BV",
191
+ BWA: "BW",
192
+ CAF: "CF",
193
+ CAN: "CA",
194
+ CCK: "CC",
195
+ CHE: "CH",
196
+ CHL: "CL",
197
+ CHN: "CN",
198
+ CIV: "CI",
199
+ CMR: "CM",
200
+ COD: "CD",
201
+ COG: "CG",
202
+ COK: "CK",
203
+ COL: "CO",
204
+ COM: "KM",
205
+ CPV: "CV",
206
+ CRI: "CR",
207
+ CUB: "CU",
208
+ CUW: "CW",
209
+ CXR: "CX",
210
+ CYM: "KY",
211
+ CYP: "CY",
212
+ CZE: "CZ",
213
+ DEU: "DE",
214
+ DJI: "DJ",
215
+ DMA: "DM",
216
+ DNK: "DK",
217
+ DOM: "DO",
218
+ DZA: "DZ",
219
+ ECU: "EC",
220
+ EGY: "EG",
221
+ ERI: "ER",
222
+ ESH: "EH",
223
+ ESP: "ES",
224
+ EST: "EE",
225
+ ETH: "ET",
226
+ FIN: "FI",
227
+ FJI: "FJ",
228
+ FLK: "FK",
229
+ FRA: "FR",
230
+ FRO: "FO",
231
+ FSM: "FM",
232
+ GAB: "GA",
233
+ GBR: "GB",
234
+ GEO: "GE",
235
+ GGY: "GG",
236
+ GHA: "GH",
237
+ GIB: "GI",
238
+ GIN: "GN",
239
+ GLP: "GP",
240
+ GMB: "GM",
241
+ GNB: "GW",
242
+ GNQ: "GQ",
243
+ GRC: "GR",
244
+ GRD: "GD",
245
+ GRL: "GL",
246
+ GTM: "GT",
247
+ GUF: "GF",
248
+ GUM: "GU",
249
+ GUY: "GY",
250
+ HKG: "HK",
251
+ HMD: "HM",
252
+ HND: "HN",
253
+ HRV: "HR",
254
+ HTI: "HT",
255
+ HUN: "HU",
256
+ IDN: "ID",
257
+ IMN: "IM",
258
+ IND: "IN",
259
+ IOT: "IO",
260
+ IRL: "IE",
261
+ IRN: "IR",
262
+ IRQ: "IQ",
263
+ ISL: "IS",
264
+ ISR: "IL",
265
+ ITA: "IT",
266
+ JAM: "JM",
267
+ JEY: "JE",
268
+ JOR: "JO",
269
+ JPN: "JP",
270
+ KAZ: "KZ",
271
+ KEN: "KE",
272
+ KGZ: "KG",
273
+ KHM: "KH",
274
+ KIR: "KI",
275
+ KNA: "KN",
276
+ KOR: "KR",
277
+ KWT: "KW",
278
+ LAO: "LA",
279
+ LBN: "LB",
280
+ LBR: "LR",
281
+ LBY: "LY",
282
+ LCA: "LC",
283
+ LIE: "LI",
284
+ LKA: "LK",
285
+ LSO: "LS",
286
+ LTU: "LT",
287
+ LUX: "LU",
288
+ LVA: "LV",
289
+ MAC: "MO",
290
+ MAF: "MF",
291
+ MAR: "MA",
292
+ MCO: "MC",
293
+ MDA: "MD",
294
+ MDG: "MG",
295
+ MDV: "MV",
296
+ MEX: "MX",
297
+ MHL: "MH",
298
+ MKD: "MK",
299
+ MLI: "ML",
300
+ MLT: "MT",
301
+ MMR: "MM",
302
+ MNE: "ME",
303
+ MNG: "MN",
304
+ MNP: "MP",
305
+ MOZ: "MZ",
306
+ MRT: "MR",
307
+ MSR: "MS",
308
+ MTQ: "MQ",
309
+ MUS: "MU",
310
+ MWI: "MW",
311
+ MYS: "MY",
312
+ MYT: "YT",
313
+ NAM: "NA",
314
+ NCL: "NC",
315
+ NER: "NE",
316
+ NFK: "NF",
317
+ NGA: "NG",
318
+ NIC: "NI",
319
+ NIU: "NU",
320
+ NLD: "NL",
321
+ NOR: "NO",
322
+ NPL: "NP",
323
+ NRU: "NR",
324
+ NZL: "NZ",
325
+ OMN: "OM",
326
+ PAK: "PK",
327
+ PAN: "PA",
328
+ PCN: "PN",
329
+ PER: "PE",
330
+ PHL: "PH",
331
+ PLW: "PW",
332
+ PNG: "PG",
333
+ POL: "PL",
334
+ PRI: "PR",
335
+ PRK: "KP",
336
+ PRT: "PT",
337
+ PRY: "PY",
338
+ PSE: "PS",
339
+ PYF: "PF",
340
+ QAT: "QA",
341
+ REU: "RE",
342
+ ROU: "RO",
343
+ RUS: "RU",
344
+ RWA: "RW",
345
+ SAU: "SA",
346
+ SDN: "SD",
347
+ SEN: "SN",
348
+ SGP: "SG",
349
+ SGS: "GS",
350
+ SHN: "SH",
351
+ SJM: "SJ",
352
+ SLB: "SB",
353
+ SLE: "SL",
354
+ SLV: "SV",
355
+ SMR: "SM",
356
+ SOM: "SO",
357
+ SPM: "PM",
358
+ SRB: "RS",
359
+ SSD: "SS",
360
+ STP: "ST",
361
+ SUR: "SR",
362
+ SVK: "SK",
363
+ SVN: "SI",
364
+ SWE: "SE",
365
+ SWZ: "SZ",
366
+ SXM: "SX",
367
+ SYC: "SC",
368
+ SYR: "SY",
369
+ TCA: "TC",
370
+ TCD: "TD",
371
+ TGO: "TG",
372
+ THA: "TH",
373
+ TJK: "TJ",
374
+ TKL: "TK",
375
+ TKM: "TM",
376
+ TLS: "TL",
377
+ TON: "TO",
378
+ TTO: "TT",
379
+ TUN: "TN",
380
+ TUR: "TR",
381
+ TUV: "TV",
382
+ TWN: "TW",
383
+ TZA: "TZ",
384
+ UGA: "UG",
385
+ UKR: "UA",
386
+ UMI: "UM",
387
+ URY: "UY",
388
+ USA: "US",
389
+ UZB: "UZ",
390
+ VAT: "VA",
391
+ VCT: "VC",
392
+ VEN: "VE",
393
+ VGB: "VG",
394
+ VIR: "VI",
395
+ VNM: "VN",
396
+ VUT: "VU",
397
+ WLF: "WF",
398
+ WSM: "WS",
399
+ YEM: "YE",
400
+ ZAF: "ZA",
401
+ ZMB: "ZM",
402
+ ZWE: "ZW"
403
+ };
404
+ }
405
+ });
406
+
142
407
  // src/adapters/native/capgo/native-adapter.ts
143
408
  var native_adapter_exports = {};
144
409
  __export(native_adapter_exports, {
145
410
  CapgoNativeAdapter: () => CapgoNativeAdapter
146
411
  });
412
+ function nativeStorefrontRegistered() {
413
+ const headers = core.Capacitor.PluginHeaders;
414
+ return headers?.find((h) => h.name === "NativePurchases")?.methods?.some((m) => m.name === "getStorefront") ?? false;
415
+ }
416
+ function normalizeStorefront(raw) {
417
+ const code = raw?.countryCode?.trim();
418
+ if (!code) return null;
419
+ const platform = getPlatform() === "android" ? "google" : "apple";
420
+ return {
421
+ // alpha-2 when recognized; otherwise the uppercased raw code as a
422
+ // best-effort fallback (see `Storefront.countryCode`).
423
+ countryCode: toAlpha2(code) ?? code.toUpperCase(),
424
+ countryCodeRaw: code,
425
+ storefrontId: raw.storefrontId,
426
+ platform
427
+ };
428
+ }
147
429
  function normalizeProduct(p, type) {
148
430
  const priceMicros = Math.round(p.price * 1e6).toString();
149
431
  return {
@@ -202,7 +484,7 @@ function mapPurchaseError(error, productId) {
202
484
  cause: error
203
485
  });
204
486
  }
205
- if (lower.includes("product not found")) {
487
+ if (lower.includes("product not found") || lower.includes("cannot find product")) {
206
488
  return new exports.IAPError({
207
489
  code: exports.IAPErrorCode.PRODUCT_NOT_FOUND,
208
490
  message: `Product "${productId}" was not found in the store catalog.`,
@@ -219,6 +501,7 @@ var CapgoNativeAdapter;
219
501
  var init_native_adapter = __esm({
220
502
  "src/adapters/native/capgo/native-adapter.ts"() {
221
503
  init_errors();
504
+ init_iso_country();
222
505
  init_platform();
223
506
  CapgoNativeAdapter = class {
224
507
  async isAvailable() {
@@ -302,6 +585,24 @@ var init_native_adapter = __esm({
302
585
  });
303
586
  }
304
587
  }
588
+ /**
589
+ * Read the current storefront from the native plugin — which is expected to
590
+ * source it from StoreKit 2 `Storefront.current` on iOS (alpha-3) and
591
+ * `getBillingConfigAsync()` on Android (alpha-2) — normalizing `countryCode`
592
+ * to alpha-2. Silent like {@link CapgoNativeAdapter.isAvailable}: any
593
+ * unavailability — older plugin (no native method registered), native
594
+ * rejection, or empty country — resolves to `null` rather than throwing.
595
+ */
596
+ async getStorefront() {
597
+ if (!nativeStorefrontRegistered()) return null;
598
+ const np = nativePurchases.NativePurchases;
599
+ try {
600
+ const raw = await np.getStorefront?.();
601
+ return raw ? normalizeStorefront(raw) : null;
602
+ } catch {
603
+ return null;
604
+ }
605
+ }
305
606
  async dispose() {
306
607
  }
307
608
  };
@@ -832,6 +1133,9 @@ var WebStubAdapter = class {
832
1133
  message: "Subscription management is not supported on the web platform."
833
1134
  });
834
1135
  }
1136
+ async getStorefront() {
1137
+ return null;
1138
+ }
835
1139
  };
836
1140
 
837
1141
  // src/adapters/native/index.ts
@@ -1737,6 +2041,24 @@ function createIAP(input) {
1737
2041
  cachedAt: null,
1738
2042
  products: Object.freeze([...config.products ?? []])
1739
2043
  };
2044
+ async function refreshEntitlements() {
2045
+ requireInitialized(state);
2046
+ const previous = state.entitlements;
2047
+ const fetched = await state.backend.getEntitlements();
2048
+ const next = freezeAll(fetched);
2049
+ try {
2050
+ state.cachedAt = await state.cache.save(next);
2051
+ } catch (error) {
2052
+ state.logger.warn(
2053
+ "Failed to persist refreshed entitlements; in-memory state still updated.",
2054
+ error
2055
+ );
2056
+ }
2057
+ state.entitlements = next;
2058
+ if (!entitlementsEqual(previous, next)) {
2059
+ state.emitter.emit("entitlements-changed", { entitlements: next, previous });
2060
+ }
2061
+ }
1740
2062
  return {
1741
2063
  async initialize() {
1742
2064
  if (state.destroyed) {
@@ -1838,7 +2160,7 @@ function createIAP(input) {
1838
2160
  logger: state.logger,
1839
2161
  onResume: async () => {
1840
2162
  try {
1841
- await this.refresh();
2163
+ await refreshEntitlements();
1842
2164
  } catch (error) {
1843
2165
  state.logger.warn("refreshOnResume: refresh() failed.", error);
1844
2166
  }
@@ -1849,7 +2171,7 @@ function createIAP(input) {
1849
2171
  state.logger.debug("Cache exceeds TTL; scheduling background refresh.");
1850
2172
  queueMicrotask(() => {
1851
2173
  if (!state.initialized || state.destroyed) return;
1852
- this.refresh().catch((error) => {
2174
+ refreshEntitlements().catch((error) => {
1853
2175
  state.logger.warn("TTL background refresh failed.", error);
1854
2176
  });
1855
2177
  });
@@ -1857,24 +2179,7 @@ function createIAP(input) {
1857
2179
  state.initialized = true;
1858
2180
  state.emitter.emit("ready", void 0);
1859
2181
  },
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
- },
2182
+ refresh: refreshEntitlements,
1878
2183
  async destroy() {
1879
2184
  if (state.destroyed) return;
1880
2185
  state.destroyed = true;
@@ -1932,6 +2237,10 @@ function createIAP(input) {
1932
2237
  }
1933
2238
  return state.adapter.getProducts(state.products.map((p) => ({ id: p.id, type: p.type })));
1934
2239
  },
2240
+ async getStorefront() {
2241
+ requireInitialized(state);
2242
+ return state.adapter?.getStorefront?.() ?? null;
2243
+ },
1935
2244
  hasEntitlement(key) {
1936
2245
  return state.entitlements.some((e) => e.key === key);
1937
2246
  },