@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 +57 -0
- package/README.md +25 -4
- package/dist/index.cjs +96 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +96 -18
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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.
|
|
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
|
|
91
|
+
## Optional peer dependencies
|
|
92
92
|
|
|
93
|
-
|
|
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`.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2012
|
-
|
|
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
|
|
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
|
|
3537
|
-
code: exports.IAPErrorCode.STORAGE_ERROR,
|
|
3538
|
-
message: "Failed to persist unfinished transactions list.",
|
|
3615
|
+
throw toIAPError(
|
|
3539
3616
|
cause,
|
|
3540
|
-
|
|
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.
|
|
3882
|
+
var VERSION = "8.2.0";
|
|
3805
3883
|
|
|
3806
3884
|
exports.DEFAULT_PERMANENT_ERROR_CODES = DEFAULT_PERMANENT_ERROR_CODES;
|
|
3807
3885
|
exports.HttpBackendAdapter = HttpBackendAdapter;
|