@nosslabs/iap 8.1.0 → 8.2.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
@@ -3,6 +3,63 @@
3
3
  All notable changes to `@nosslabs/iap` will be documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [8.2.0] — 2026-10-27
7
+
8
+ ### Changed
9
+
10
+ - **`@capacitor/preferences` is now an optional peer, loaded only when it is used.** It was a
11
+ required peer reached through a *static* import, so it ended up in the bundle's top-level
12
+ imports and every consumer shipped the native plugin — including one passing
13
+ `storage.type: 'custom'` with its own storage, which never calls Preferences at all.
14
+
15
+ `PreferencesAdapter` now reaches the plugin through `await import()`, memoised per instance,
16
+ the way the resume listener reaches `@capacitor/app`. Neither `dist/index.js` nor
17
+ `dist/index.cjs` names it at the top level any more, asserted on the built artifact by a new
18
+ `npm run verify:bundle` step in CI and before publish — a static import behaves identically
19
+ at runtime and differs only in what the bundler hoists, so no unit test can see it.
20
+
21
+ Choosing `storage.type: 'preferences'` (still the default) without the plugin installed now
22
+ fails at `initialize()` with `INVALID_CONFIG`, naming what to install and the two
23
+ alternatives that need no plugin. It is reported there, via the new optional
24
+ `StorageAdapter.ensureAvailable()`, rather than at the first cache read: that read treats any
25
+ failure as "nothing cached", so a missing plugin would have looked like a cold start while
26
+ iap silently persisted nothing.
27
+
28
+ **If you use `storage.type: 'preferences'`, keep the plugin installed** — but check it is in
29
+ your *own* `dependencies`. npm 7+ installs a required peer into the tree without listing it
30
+ in your `package.json`; now that it is optional, a fresh install or a regenerated lockfile
31
+ can drop it, and `npx cap sync` with it.
32
+
33
+ If you pass your own adapter or use `'memory'`, you can uninstall it — and then you must
34
+ **externalise it in your bundler**, because a dynamic `import()` is still resolved at build
35
+ time: without it Vite/Rollup fails with `failed to resolve import "@capacitor/preferences"`.
36
+ The installation guide has the one-line config. (This is not new to this release — the
37
+ optional `@capacitor/app` has always behaved the same way — but it is now worth stating.)
38
+
39
+ - **Developed and tested against `@capgo/native-purchases@8.9.6`** (was `8.6.5`). The peer
40
+ range is unchanged at `^8.2.0`. Nothing in iap broke, and two of the plugin's documented
41
+ gaps closed — both caught by the inverted contract guards rather than by reading release
42
+ notes:
43
+
44
+ - **Purchase tokens no longer reach logcat.** Through `8.6.5` the Android plugin logged them
45
+ in the clear with no debug gate, which [the native-layer
46
+ page](https://iap.nossdev.com/guide/native-layer) warned about. Every Android log now goes
47
+ through a `NativePurchasesLog` wrapper gated on `BuildConfig.DEBUG` or an explicit
48
+ `debugLogging` config, and the purchase summary it emits carries no token. Verified by
49
+ asserting that no `android.util.Log` call survives outside the wrapper, so a regression
50
+ fails CI; the warning is off the page.
51
+ - **`purchaseProduct()` accepts an `offerToken`.** Choosing a specific offer within a base
52
+ plan is now possible at the native layer. iap does not forward one yet — `purchase()` has
53
+ no way to name an offer — so this is recorded as a feature to build rather than a gap to
54
+ live with.
55
+
56
+ The plugin also moved to **Play Billing Library 9.1.0** (from 8.3.0), a native major. It
57
+ changes nothing iap relies on, and that was checked against the 9.1.0 artifact rather than
58
+ assumed: `Purchase.PurchaseState` still reads `UNSPECIFIED_STATE = 0, PURCHASED = 1,
59
+ PENDING = 2`, and `consumeAsync`, `acknowledgePurchase` and `queryPurchasesAsync` keep their
60
+ signatures — the three facts the Android purchase-state check and the consumable path are
61
+ built on.
62
+
6
63
  ## [8.1.0] — 2026-10-27
7
64
 
8
65
  ### Changed
package/README.md CHANGED
@@ -84,20 +84,41 @@ It does **not**: talk to Attesto directly, manage user auth, or ship paywall UI.
84
84
 
85
85
  | `@nosslabs/iap` | Capacitor | Native plugin | dist-tag | Status |
86
86
  |---|---|---|---|---|
87
- | 8.x | 8.x | `@capgo/native-purchases ^8` | `@latest`, `@latest-8` | **Current** — `8.0.0` |
87
+ | 8.x | 8.x | `@capgo/native-purchases ^8.2` | `@latest`, `@latest-8` | **Current** — `8.2.0` |
88
88
  | 7.x | 7.x | `@capgo/native-purchases@lts-v7` | `@latest-7` (pin via `^7`) | Maintenance |
89
89
  | 5.x | 5.x | `cordova-plugin-purchase ^13.x` | `@latest-5` (pin via `^5`) | Maintenance |
90
90
 
91
- ## Optional peer dependency
91
+ ## Optional peer dependencies
92
92
 
93
- If you want auto-refresh on app resume (default behavior):
93
+ Both are reached through a lazy `import()`, so an app that does not use them never loads them
94
+ and keeps them out of its bundle. If you *uninstall* one, externalise it in your bundler
95
+ (`build.rollupOptions.external` in Vite) — a dynamic import is still resolved at build time.
96
+ See the [installation guide](https://iap.nossdev.com/guide/installation).
97
+
98
+ **`@capacitor/app`** — auto-refresh on app resume (the default behaviour):
94
99
 
95
100
  ```bash
96
101
  npm install @capacitor/app
97
102
  npx cap sync
98
103
  ```
99
104
 
100
- Or disable the listener with `options.refreshOnResume: false`. See [installation guide](https://iap.nossdev.com/guide/installation#optional-app-resume-listener).
105
+ Or disable the listener with `options.refreshOnResume: false`.
106
+
107
+ **`@capacitor/preferences`** — the entitlement cache and unfinished-transaction storage, under
108
+ the default `storage.type: 'preferences'`:
109
+
110
+ ```bash
111
+ npm install @capacitor/preferences
112
+ npx cap sync
113
+ ```
114
+
115
+ Skip it if you set `storage.type` to `'custom'` (your own adapter) or `'memory'`. Choosing
116
+ `'preferences'` without it fails at `initialize()` with a message saying so, rather than
117
+ silently persisting nothing.
118
+
119
+ If you use the default `'preferences'` and never declared the package yourself, add it to your
120
+ `dependencies` now: npm installed it for you while it was a *required* peer, and that will stop
121
+ happening.
101
122
 
102
123
  ## Development
103
124
 
package/dist/index.cjs CHANGED
@@ -3,7 +3,6 @@
3
3
  var core = require('@capacitor/core');
4
4
  var nativePurchases = require('@capgo/native-purchases');
5
5
  var zod = require('zod');
6
- var preferences = require('@capacitor/preferences');
7
6
 
8
7
  var __defProp = Object.defineProperty;
9
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1979,39 +1978,122 @@ init_errors();
1979
1978
  var PreferencesAdapter = class {
1980
1979
  prefix;
1981
1980
  knownKeys = /* @__PURE__ */ new Set();
1981
+ /**
1982
+ * Boxed, and that is not incidental — see {@link PreferencesAdapter.preferences}.
1983
+ */
1984
+ plugin = null;
1982
1985
  constructor(namespace) {
1983
1986
  this.prefix = `${namespace}.`;
1984
1987
  }
1988
+ /**
1989
+ * The plugin, inside a box.
1990
+ *
1991
+ * The box is load-bearing. `registerPlugin()` hands back a **Proxy** that
1992
+ * answers any property, `then` included — so resolving a promise *with*
1993
+ * the plugin makes it look like a thenable, and promise assimilation
1994
+ * calls `Preferences.then()`. On web that rejects with
1995
+ * `"Preferences.then()" is not implemented on web`, from inside
1996
+ * `await this.preferences()`, for every storage operation.
1997
+ *
1998
+ * Wrapping it in a plain object means the promise resolves with something
1999
+ * that has no `then`, so nothing is assimilated.
2000
+ */
2001
+ preferences() {
2002
+ this.plugin ??= import('@capacitor/preferences').then((mod) => ({ preferences: mod.Preferences })).catch((cause) => {
2003
+ if (isModuleNotFound(cause)) throw notInstalled(cause);
2004
+ this.plugin = null;
2005
+ throw new exports.IAPError({
2006
+ code: exports.IAPErrorCode.STORAGE_ERROR,
2007
+ message: "Could not load @capacitor/preferences. The package is installed, so this is most likely a chunk that failed to load; it is worth retrying.",
2008
+ cause,
2009
+ recoverable: true
2010
+ });
2011
+ });
2012
+ return this.plugin;
2013
+ }
2014
+ /**
2015
+ * Resolve the plugin now, so a missing one is reported at `initialize()`.
2016
+ *
2017
+ * See the note on lazy loading above for why this is not left to the first
2018
+ * `get()`.
2019
+ */
2020
+ async ensureAvailable() {
2021
+ await this.preferences();
2022
+ }
1985
2023
  async get(key) {
2024
+ const { preferences } = await this.preferences();
1986
2025
  try {
1987
- const result = await preferences.Preferences.get({ key: this.prefix + key });
2026
+ const result = await preferences.get({ key: this.prefix + key });
1988
2027
  return result.value;
1989
2028
  } catch (cause) {
1990
2029
  throw wrap(cause, `Preferences.get failed for "${key}".`);
1991
2030
  }
1992
2031
  }
1993
2032
  async set(key, value) {
2033
+ const { preferences } = await this.preferences();
1994
2034
  try {
1995
- await preferences.Preferences.set({ key: this.prefix + key, value });
2035
+ await preferences.set({ key: this.prefix + key, value });
1996
2036
  this.knownKeys.add(key);
1997
2037
  } catch (cause) {
1998
2038
  throw wrap(cause, `Preferences.set failed for "${key}".`);
1999
2039
  }
2000
2040
  }
2001
2041
  async remove(key) {
2042
+ const { preferences } = await this.preferences();
2002
2043
  try {
2003
- await preferences.Preferences.remove({ key: this.prefix + key });
2044
+ await preferences.remove({ key: this.prefix + key });
2004
2045
  this.knownKeys.delete(key);
2005
2046
  } catch (cause) {
2006
2047
  throw wrap(cause, `Preferences.remove failed for "${key}".`);
2007
2048
  }
2008
2049
  }
2009
2050
  async clear() {
2051
+ const { preferences } = await this.preferences();
2010
2052
  const keys = [...this.knownKeys];
2011
- this.knownKeys.clear();
2012
- await Promise.all(keys.map((k) => preferences.Preferences.remove({ key: this.prefix + k })));
2053
+ const failed = [];
2054
+ const errors = [];
2055
+ await Promise.all(
2056
+ keys.map(async (key) => {
2057
+ try {
2058
+ await preferences.remove({ key: this.prefix + key });
2059
+ this.knownKeys.delete(key);
2060
+ } catch (cause) {
2061
+ failed.push(key);
2062
+ errors.push(cause);
2063
+ }
2064
+ })
2065
+ );
2066
+ if (failed.length > 0) {
2067
+ throw wrap(errors[0], `Preferences.clear failed for ${failed.length} key(s).`);
2068
+ }
2013
2069
  }
2014
2070
  };
2071
+ function isModuleNotFound(cause) {
2072
+ let current = cause;
2073
+ for (let depth = 0; depth < 5 && current; depth += 1) {
2074
+ const code = current.code;
2075
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
2076
+ const message = current instanceof Error ? current.message : String(current);
2077
+ if (/cannot find (module|package)|could not resolve|failed to resolve/i.test(message)) {
2078
+ return true;
2079
+ }
2080
+ const next = current.cause;
2081
+ if (next === current) break;
2082
+ current = next;
2083
+ }
2084
+ return false;
2085
+ }
2086
+ function notInstalled(cause) {
2087
+ return new exports.IAPError({
2088
+ code: exports.IAPErrorCode.INVALID_CONFIG,
2089
+ message: 'storage.type is "preferences" (the default) but @capacitor/preferences is not installed. Install it (npm i @capacitor/preferences && npx cap sync), or choose storage that needs no plugin: storage.type "memory", or "custom" with your own adapter.',
2090
+ // The per-code hint for INVALID_CONFIG sends the reader to check field
2091
+ // paths against the config schema. There are none to check, and no
2092
+ // config change fixes this — the message above already says what to do.
2093
+ includeHint: false,
2094
+ cause
2095
+ });
2096
+ }
2015
2097
  function wrap(cause, message) {
2016
2098
  return new exports.IAPError({
2017
2099
  code: exports.IAPErrorCode.STORAGE_ERROR,
@@ -2112,12 +2194,7 @@ var EntitlementCache = class {
2112
2194
  try {
2113
2195
  await this.storage.set(ENTITLEMENTS_KEY, JSON.stringify(envelope));
2114
2196
  } catch (cause) {
2115
- throw new exports.IAPError({
2116
- code: exports.IAPErrorCode.STORAGE_ERROR,
2117
- message: "Failed to persist entitlement cache.",
2118
- cause,
2119
- recoverable: true
2120
- });
2197
+ throw toIAPError(cause, "Failed to persist entitlement cache.", exports.IAPErrorCode.STORAGE_ERROR);
2121
2198
  }
2122
2199
  return cachedAt;
2123
2200
  }
@@ -3064,6 +3141,7 @@ async function initializeIap(state, refreshEntitlements) {
3064
3141
  state.logger.debug("initialize() called more than once; ignoring.");
3065
3142
  return;
3066
3143
  }
3144
+ await state.storage.ensureAvailable?.();
3067
3145
  const platform = getPlatform();
3068
3146
  if (!isNative()) {
3069
3147
  state.logger.info("Native purchases unavailable on web; entitlement queries still functional.");
@@ -3357,6 +3435,7 @@ async function resetIap(state) {
3357
3435
  recoverable: true
3358
3436
  });
3359
3437
  }
3438
+ await state.storage.ensureAvailable?.();
3360
3439
  const previous = state.entitlements;
3361
3440
  state.entitlements = [];
3362
3441
  state.cachedAt = null;
@@ -3533,12 +3612,11 @@ var UnfinishedTransactionsStore = class {
3533
3612
  try {
3534
3613
  await this.storage.set(STORE_KEY2, JSON.stringify(entries));
3535
3614
  } catch (cause) {
3536
- throw new exports.IAPError({
3537
- code: exports.IAPErrorCode.STORAGE_ERROR,
3538
- message: "Failed to persist unfinished transactions list.",
3615
+ throw toIAPError(
3539
3616
  cause,
3540
- recoverable: true
3541
- });
3617
+ "Failed to persist unfinished transactions list.",
3618
+ exports.IAPErrorCode.STORAGE_ERROR
3619
+ );
3542
3620
  }
3543
3621
  }
3544
3622
  async safeClear() {
@@ -3801,7 +3879,7 @@ function resolveLogger(level, candidate) {
3801
3879
  init_errors();
3802
3880
 
3803
3881
  // src/version.ts
3804
- var VERSION = "8.1.0";
3882
+ var VERSION = "8.2.0";
3805
3883
 
3806
3884
  exports.DEFAULT_PERMANENT_ERROR_CODES = DEFAULT_PERMANENT_ERROR_CODES;
3807
3885
  exports.HttpBackendAdapter = HttpBackendAdapter;