@nosslabs/iap 5.0.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/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@capacitor/core');
4
- require('cordova-plugin-purchase');
4
+ var nativePurchases = require('@capgo/native-purchases');
5
5
  var zod = require('zod');
6
6
  var preferences = require('@capacitor/preferences');
7
7
 
@@ -85,7 +85,7 @@ var init_errors = __esm({
85
85
  NOT_INITIALIZED: "Call iap.initialize() before this method, or recreate the instance after destroy().",
86
86
  // Native plugin
87
87
  PLATFORM_NOT_SUPPORTED: "In-app purchases run on iOS/Android only. Web is no-op by design \u2014 guard your purchase UI behind Capacitor.isNativePlatform().",
88
- BILLING_NOT_AVAILABLE: "cordova-plugin-purchase failed to initialize. Confirm the plugin is installed and `npx cap sync` has run; check device sandbox/test account is signed in.",
88
+ BILLING_NOT_AVAILABLE: "The store billing service is unavailable. Confirm @capgo/native-purchases is installed and `npx cap sync` has run; check the device sandbox/test account is signed in.",
89
89
  PRODUCT_NOT_FOUND: "Ensure the productId is registered in App Store Connect / Play Console AND in your createIAP({ products }) config.",
90
90
  USER_CANCELLED: "No action needed \u2014 the user dismissed the native purchase sheet.",
91
91
  PURCHASE_PENDING: "Android only: payment is awaiting external clearance (e.g. cash payment, bank verification). The backend will receive a Google RTDN webhook when it clears; call iap.refresh() afterward.",
@@ -139,295 +139,170 @@ var init_platform = __esm({
139
139
  }
140
140
  });
141
141
 
142
- // src/adapters/native/cdv/native-adapter.ts
142
+ // src/adapters/native/capgo/native-adapter.ts
143
143
  var native_adapter_exports = {};
144
144
  __export(native_adapter_exports, {
145
- CdvNativeAdapter: () => CdvNativeAdapter
145
+ CapgoNativeAdapter: () => CapgoNativeAdapter
146
146
  });
147
- function getCdv() {
148
- const candidate = globalThis.CdvPurchase;
149
- if (!candidate || !candidate.store) {
150
- throw new exports.IAPError({
151
- code: exports.IAPErrorCode.BILLING_NOT_AVAILABLE,
152
- message: "cordova-plugin-purchase is not available. Ensure the plugin is installed and `npx cap sync` has run."
153
- });
154
- }
155
- return candidate;
156
- }
157
- function currentCdvPlatform() {
158
- const cdv = getCdv();
159
- const platform = getPlatform();
160
- if (platform === "android") return cdv.Platform.GOOGLE_PLAY;
161
- return cdv.Platform.APPLE_APPSTORE;
162
- }
163
- function mapProductType(type) {
164
- const cdv = getCdv();
165
- switch (type) {
166
- case "subscription":
167
- return cdv.ProductType.PAID_SUBSCRIPTION;
168
- case "consumable":
169
- return cdv.ProductType.CONSUMABLE;
170
- default:
171
- return cdv.ProductType.NON_CONSUMABLE;
172
- }
173
- }
174
- function inferProductType(tx, configured) {
175
- const id = tx.products[0]?.id;
176
- if (!id) return "product";
177
- const match = configured.find((p) => p.id === id);
178
- return match?.type ?? "product";
179
- }
180
147
  function normalizeProduct(p, type) {
181
- const offer = p.getOffer();
182
- const phase = offer?.pricingPhases?.[0];
183
- const priceMicros = phase?.priceMicros?.toString() ?? "0";
184
- const priceString = phase?.price ?? "";
185
- const currency = phase?.currency ?? "";
148
+ const priceMicros = Math.round(p.price * 1e6).toString();
186
149
  return {
187
- id: p.id,
150
+ id: p.identifier,
188
151
  type,
189
- title: p.title ?? p.id,
190
- description: p.description ?? "",
191
- priceString,
152
+ title: p.title,
153
+ description: p.description,
154
+ priceString: p.priceString,
192
155
  priceMicros,
193
- currency
156
+ currency: p.currencyCode
194
157
  };
195
158
  }
196
- function normalizeTransaction(tx, productType, token) {
197
- const platform = tx.platform === "ios-appstore" ? "apple" : "google";
198
- const productId = tx.products[0]?.id ?? "";
199
- const result = {
159
+ function normalizeTransaction(tx, productType) {
160
+ const platform = inferPlatform(tx);
161
+ const token = platform === "google" ? tx.purchaseToken ?? tx.transactionId : tx.transactionId;
162
+ const native = {
200
163
  platform,
201
- productId,
164
+ productId: tx.productIdentifier,
202
165
  token,
203
166
  productType,
204
167
  raw: tx
205
168
  };
206
- if (platform === "google") {
207
- const packageName = googlePackageName(tx);
208
- if (packageName) result.packageName = packageName;
169
+ return native;
170
+ }
171
+ function inferPlatform(tx) {
172
+ if (tx.purchaseToken !== void 0 || tx.purchaseState !== void 0 || tx.orderId !== void 0) {
173
+ return "google";
209
174
  }
210
- return result;
175
+ if (tx.receipt !== void 0 || tx.jwsRepresentation !== void 0) {
176
+ return "apple";
177
+ }
178
+ return getPlatform() === "android" ? "google" : "apple";
211
179
  }
212
- function googlePackageName(tx) {
213
- const googleTx = tx;
214
- return googleTx.nativePurchase?.packageName;
180
+ function inferProductType(tx) {
181
+ if (tx.productType === "subs") return "subscription";
182
+ return "product";
215
183
  }
216
- function transactionToken(tx) {
217
- if (tx.platform === "ios-appstore") {
218
- return tx.transactionId || null;
219
- }
220
- const googleTx = tx;
221
- return googleTx.nativePurchase?.purchaseToken ?? googleTx.parentReceipt?.purchaseToken ?? tx.transactionId ?? null;
184
+ function mapToPluginPurchaseType(type) {
185
+ return type === "subscription" ? nativePurchases.PURCHASE_TYPE.SUBS : nativePurchases.PURCHASE_TYPE.INAPP;
222
186
  }
223
- function mapOrderError(err, productId) {
224
- const cdv = getCdv();
225
- const cancelled = cdv.ErrorCode?.PAYMENT_CANCELLED;
226
- if (cancelled !== void 0 && err.code === cancelled) {
187
+ function mapPurchaseError(error, productId) {
188
+ if (error instanceof exports.IAPError) return error;
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ const lower = message.toLowerCase();
191
+ if (lower.includes("cancel")) {
227
192
  return new exports.IAPError({
228
193
  code: exports.IAPErrorCode.USER_CANCELLED,
229
- message: "User cancelled the native purchase sheet.",
230
- cause: err
194
+ message: `Purchase of "${productId}" was cancelled.`,
195
+ cause: error
196
+ });
197
+ }
198
+ if (lower.includes("pending")) {
199
+ return new exports.IAPError({
200
+ code: exports.IAPErrorCode.PURCHASE_PENDING,
201
+ message: `Purchase of "${productId}" is pending external clearance.`,
202
+ cause: error
203
+ });
204
+ }
205
+ if (lower.includes("product not found") || lower.includes("cannot find product")) {
206
+ return new exports.IAPError({
207
+ code: exports.IAPErrorCode.PRODUCT_NOT_FOUND,
208
+ message: `Product "${productId}" was not found in the store catalog.`,
209
+ cause: error
231
210
  });
232
211
  }
233
212
  return new exports.IAPError({
234
213
  code: exports.IAPErrorCode.STORE_ERROR,
235
- message: err.message ?? `order() failed for ${productId}.`,
236
- cause: err
214
+ message: `Native purchase of "${productId}" failed.`,
215
+ cause: error
237
216
  });
238
217
  }
239
- var CdvNativeAdapter;
218
+ var CapgoNativeAdapter;
240
219
  var init_native_adapter = __esm({
241
- "src/adapters/native/cdv/native-adapter.ts"() {
220
+ "src/adapters/native/capgo/native-adapter.ts"() {
242
221
  init_errors();
243
222
  init_platform();
244
- CdvNativeAdapter = class {
245
- products;
246
- bootstrapped = false;
247
- bootstrapping = null;
248
- pendingFinish = /* @__PURE__ */ new Map();
249
- /** Long-lived bootstrap-time .approved() listener — kept for dispose(). */
250
- bootstrapApprovedHandler = null;
251
- constructor(opts) {
252
- this.products = opts.products;
253
- }
223
+ CapgoNativeAdapter = class {
254
224
  async isAvailable() {
255
225
  try {
256
- await this.bootstrap();
257
- return true;
226
+ const result = await nativePurchases.NativePurchases.isBillingSupported();
227
+ return result.isBillingSupported;
258
228
  } catch {
259
229
  return false;
260
230
  }
261
231
  }
262
232
  async getProducts(requests) {
263
233
  if (requests.length === 0) return [];
264
- const store = await this.ensureStore();
265
- await store.update();
266
- const out = [];
234
+ const inappIds = [];
235
+ const subsIds = [];
236
+ const requestById = /* @__PURE__ */ new Map();
267
237
  for (const req of requests) {
268
- const native = store.get(req.id);
269
- if (!native) continue;
270
- out.push(normalizeProduct(native, req.type));
238
+ requestById.set(req.id, req.type);
239
+ if (req.type === "subscription") {
240
+ subsIds.push(req.id);
241
+ } else {
242
+ inappIds.push(req.id);
243
+ }
271
244
  }
272
- return out;
245
+ const [inapp, subs] = await Promise.all([
246
+ inappIds.length > 0 ? nativePurchases.NativePurchases.getProducts({
247
+ productIdentifiers: inappIds,
248
+ productType: nativePurchases.PURCHASE_TYPE.INAPP
249
+ }) : Promise.resolve({ products: [] }),
250
+ subsIds.length > 0 ? nativePurchases.NativePurchases.getProducts({
251
+ productIdentifiers: subsIds,
252
+ productType: nativePurchases.PURCHASE_TYPE.SUBS
253
+ }) : Promise.resolve({ products: [] })
254
+ ]);
255
+ const all = [...inapp.products, ...subs.products];
256
+ return all.map((p) => normalizeProduct(p, requestById.get(p.identifier) ?? "product"));
273
257
  }
274
258
  async purchaseProduct(opts) {
275
- const store = await this.ensureStore();
276
- const native = store.get(opts.productId);
277
- if (!native) {
278
- throw new exports.IAPError({
279
- code: exports.IAPErrorCode.PRODUCT_NOT_FOUND,
280
- message: `Product "${opts.productId}" not registered or not available from the store.`
281
- });
282
- }
283
- const offer = opts.androidPlanId ? native.getOffer(opts.androidPlanId) ?? native.getOffer() : native.getOffer();
284
- if (!offer) {
285
- throw new exports.IAPError({
286
- code: exports.IAPErrorCode.PRODUCT_NOT_FOUND,
287
- message: `Product "${opts.productId}" has no purchasable offer${opts.androidPlanId ? ` (planId="${opts.androidPlanId}")` : ""}.`
259
+ const purchaseType = mapToPluginPurchaseType(opts.productType);
260
+ const isConsumable = opts.productType === "consumable";
261
+ let tx;
262
+ try {
263
+ tx = await nativePurchases.NativePurchases.purchaseProduct({
264
+ productIdentifier: opts.productId,
265
+ productType: purchaseType,
266
+ planIdentifier: opts.androidPlanId,
267
+ appAccountToken: opts.appAccountToken,
268
+ isConsumable,
269
+ autoAcknowledgePurchases: false
288
270
  });
271
+ } catch (error) {
272
+ throw mapPurchaseError(error, opts.productId);
289
273
  }
290
- return new Promise((resolve, reject) => {
291
- let settled = false;
292
- const cleanup = () => {
293
- store.off(handleApproved);
294
- };
295
- const handleApproved = (tx) => {
296
- if (settled) return;
297
- if (!tx.products.some((p) => p.id === opts.productId)) return;
298
- const token = transactionToken(tx);
299
- if (!token) {
300
- settled = true;
301
- cleanup();
302
- reject(
303
- new exports.IAPError({
304
- code: exports.IAPErrorCode.STORE_ERROR,
305
- message: `Approved transaction for "${opts.productId}" has no token; cannot verify.`
306
- })
307
- );
308
- return;
309
- }
310
- settled = true;
311
- cleanup();
312
- const normalized = normalizeTransaction(tx, opts.productType, token);
313
- this.pendingFinish.set(token, tx);
314
- resolve(normalized);
315
- };
316
- store.when().approved(handleApproved);
317
- const additionalData = opts.appAccountToken ? { applicationUsername: opts.appAccountToken } : void 0;
318
- void Promise.resolve(offer.order(additionalData)).then((err) => {
319
- if (settled) return;
320
- if (!err) return;
321
- settled = true;
322
- cleanup();
323
- reject(mapOrderError(err, opts.productId));
324
- }).catch((cause) => {
325
- if (settled) return;
326
- settled = true;
327
- cleanup();
328
- reject(
329
- new exports.IAPError({
330
- code: exports.IAPErrorCode.STORE_ERROR,
331
- message: `order() rejected for ${opts.productId}.`,
332
- cause
333
- })
334
- );
335
- });
336
- });
274
+ return normalizeTransaction(tx, opts.productType);
337
275
  }
338
276
  async getOwnedTransactions() {
339
- const store = await this.ensureStore();
340
- await store.restorePurchases();
341
- const out = [];
342
- for (const tx of store.localTransactions) {
343
- if (tx.state !== getCdv().TransactionState.APPROVED) continue;
344
- const token = transactionToken(tx);
345
- if (!token) continue;
346
- const normalized = normalizeTransaction(tx, inferProductType(tx, this.products), token);
347
- this.pendingFinish.set(token, tx);
348
- out.push(normalized);
349
- }
350
- return out;
277
+ const result = await nativePurchases.NativePurchases.getPurchases();
278
+ return result.purchases.filter((tx) => tx.purchaseState === void 0 || tx.purchaseState === "1").map((tx) => normalizeTransaction(tx, inferProductType(tx)));
351
279
  }
352
280
  async acknowledge(transaction) {
353
- const cdvTx = this.pendingFinish.get(transaction.token);
354
- if (!cdvTx) {
355
- return;
356
- }
357
281
  try {
358
- await cdvTx.finish();
359
- } catch (cause) {
282
+ await nativePurchases.NativePurchases.acknowledgePurchase({
283
+ purchaseToken: transaction.token
284
+ });
285
+ } catch (error) {
360
286
  throw new exports.IAPError({
361
287
  code: exports.IAPErrorCode.STORE_ERROR,
362
- message: `Failed to finish transaction for ${transaction.productId}.`,
363
- cause,
288
+ message: `Failed to acknowledge transaction for ${transaction.productId}.`,
289
+ cause: error,
364
290
  recoverable: true
365
291
  });
366
292
  }
367
- this.pendingFinish.delete(transaction.token);
368
293
  }
369
294
  async manageSubscriptions() {
370
- const store = await this.ensureStore();
371
- const err = await store.manageSubscriptions();
372
- if (err) {
295
+ try {
296
+ await nativePurchases.NativePurchases.manageSubscriptions();
297
+ } catch (error) {
373
298
  throw new exports.IAPError({
374
299
  code: exports.IAPErrorCode.STORE_ERROR,
375
- message: err.message ?? "Failed to open subscription management."
300
+ message: "Failed to open the native subscription management UI.",
301
+ cause: error
376
302
  });
377
303
  }
378
304
  }
379
305
  async dispose() {
380
- if (this.bootstrapApprovedHandler) {
381
- try {
382
- const cdv = globalThis.CdvPurchase;
383
- cdv?.store?.off(this.bootstrapApprovedHandler);
384
- } catch {
385
- }
386
- this.bootstrapApprovedHandler = null;
387
- }
388
- this.pendingFinish.clear();
389
- this.bootstrapped = false;
390
- this.bootstrapping = null;
391
- }
392
- // ----- internals -----
393
- async ensureStore() {
394
- await this.bootstrap();
395
- return getCdv().store;
396
- }
397
- bootstrap() {
398
- if (this.bootstrapped) return Promise.resolve();
399
- if (this.bootstrapping) return this.bootstrapping;
400
- this.bootstrapping = (async () => {
401
- const cdv = getCdv();
402
- const platform = currentCdvPlatform();
403
- cdv.store.register(
404
- this.products.map((p) => ({
405
- id: p.id,
406
- type: mapProductType(p.type),
407
- platform
408
- }))
409
- );
410
- const errors = await cdv.store.initialize([platform]);
411
- if (errors && errors.length > 0) {
412
- const first = errors[0];
413
- throw new exports.IAPError({
414
- code: exports.IAPErrorCode.BILLING_NOT_AVAILABLE,
415
- message: first?.message ?? "cordova-plugin-purchase initialize() reported errors."
416
- });
417
- }
418
- const handler = (tx) => {
419
- const token = transactionToken(tx);
420
- if (!token) return;
421
- if (!this.pendingFinish.has(token)) {
422
- this.pendingFinish.set(token, tx);
423
- }
424
- };
425
- this.bootstrapApprovedHandler = handler;
426
- cdv.store.when().approved(handler);
427
- await cdv.store.update();
428
- this.bootstrapped = true;
429
- })();
430
- return this.bootstrapping;
431
306
  }
432
307
  };
433
308
  }
@@ -960,11 +835,11 @@ var WebStubAdapter = class {
960
835
  };
961
836
 
962
837
  // src/adapters/native/index.ts
963
- async function selectNativeAdapter(options) {
838
+ async function selectNativeAdapter() {
964
839
  const platform = getPlatform();
965
840
  if (platform === "ios" || platform === "android") {
966
841
  const mod = await Promise.resolve().then(() => (init_native_adapter(), native_adapter_exports));
967
- return new mod.CdvNativeAdapter({ products: options.products });
842
+ return new mod.CapgoNativeAdapter();
968
843
  }
969
844
  return new WebStubAdapter();
970
845
  }
@@ -1862,6 +1737,24 @@ function createIAP(input) {
1862
1737
  cachedAt: null,
1863
1738
  products: Object.freeze([...config.products ?? []])
1864
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
+ }
1865
1758
  return {
1866
1759
  async initialize() {
1867
1760
  if (state.destroyed) {
@@ -1902,7 +1795,7 @@ function createIAP(input) {
1902
1795
  state.products = Object.freeze([...validated.data]);
1903
1796
  state.logger.debug(`Resolved ${validated.data.length} product(s) from backend manifest.`);
1904
1797
  }
1905
- state.adapter = await selectNativeAdapter({ products: state.products });
1798
+ state.adapter = await selectNativeAdapter();
1906
1799
  const configGetAuthHeaders = state.config.backend.getAuthHeaders;
1907
1800
  const getAuthHeaders = configGetAuthHeaders ? async () => configGetAuthHeaders() : async () => ({});
1908
1801
  const sharedDeps = {
@@ -1963,7 +1856,7 @@ function createIAP(input) {
1963
1856
  logger: state.logger,
1964
1857
  onResume: async () => {
1965
1858
  try {
1966
- await this.refresh();
1859
+ await refreshEntitlements();
1967
1860
  } catch (error) {
1968
1861
  state.logger.warn("refreshOnResume: refresh() failed.", error);
1969
1862
  }
@@ -1974,7 +1867,7 @@ function createIAP(input) {
1974
1867
  state.logger.debug("Cache exceeds TTL; scheduling background refresh.");
1975
1868
  queueMicrotask(() => {
1976
1869
  if (!state.initialized || state.destroyed) return;
1977
- this.refresh().catch((error) => {
1870
+ refreshEntitlements().catch((error) => {
1978
1871
  state.logger.warn("TTL background refresh failed.", error);
1979
1872
  });
1980
1873
  });
@@ -1982,24 +1875,7 @@ function createIAP(input) {
1982
1875
  state.initialized = true;
1983
1876
  state.emitter.emit("ready", void 0);
1984
1877
  },
1985
- async refresh() {
1986
- requireInitialized(state);
1987
- const previous = state.entitlements;
1988
- const fetched = await state.backend.getEntitlements();
1989
- const next = freezeAll(fetched);
1990
- try {
1991
- state.cachedAt = await state.cache.save(next);
1992
- } catch (error) {
1993
- state.logger.warn(
1994
- "Failed to persist refreshed entitlements; in-memory state still updated.",
1995
- error
1996
- );
1997
- }
1998
- state.entitlements = next;
1999
- if (!entitlementsEqual(previous, next)) {
2000
- state.emitter.emit("entitlements-changed", { entitlements: next, previous });
2001
- }
2002
- },
1878
+ refresh: refreshEntitlements,
2003
1879
  async destroy() {
2004
1880
  if (state.destroyed) return;
2005
1881
  state.destroyed = true;