@nosslabs/iap 5.0.0 → 7.0.0-next.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.d.cts CHANGED
@@ -968,9 +968,7 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
968
968
  */
969
969
  refresh(): Promise<void>;
970
970
  /**
971
- * Tear down. Removes event listeners and disposes the native adapter
972
- * (which clears its `pendingFinish` map and removes the long-lived
973
- * `.approved()` listener on cdv).
971
+ * Tear down. Removes event listeners and disposes the native adapter.
974
972
  *
975
973
  * NOTE 1: persisted entitlement cache is NOT cleared. If you're handling
976
974
  * a logout for a multi-user app, also call your storage adapter's
@@ -980,10 +978,11 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
980
978
  *
981
979
  * NOTE 2: calling `destroy()` while a `purchase()` is in flight may
982
980
  * leave the result in an inconsistent state — the backend may have
983
- * recorded the entitlement but the native `acknowledge()` call will
984
- * be a no-op (because cdv's `pendingFinish` was cleared mid-flow).
985
- * On Android this means Google auto-refunds in 3 days. Avoid by
986
- * awaiting the in-flight `purchase()` before calling `destroy()`.
981
+ * recorded the entitlement but the native `acknowledge()` call may not
982
+ * have run yet. On Android this means Google auto-refunds in 3 days
983
+ * (the unfinished-transaction recovery on the next launch re-acks, but
984
+ * only if it runs within that window). Avoid by awaiting the in-flight
985
+ * `purchase()` before calling `destroy()`.
987
986
  */
988
987
  destroy(): Promise<void>;
989
988
  /**
package/dist/index.d.ts CHANGED
@@ -968,9 +968,7 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
968
968
  */
969
969
  refresh(): Promise<void>;
970
970
  /**
971
- * Tear down. Removes event listeners and disposes the native adapter
972
- * (which clears its `pendingFinish` map and removes the long-lived
973
- * `.approved()` listener on cdv).
971
+ * Tear down. Removes event listeners and disposes the native adapter.
974
972
  *
975
973
  * NOTE 1: persisted entitlement cache is NOT cleared. If you're handling
976
974
  * a logout for a multi-user app, also call your storage adapter's
@@ -980,10 +978,11 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
980
978
  *
981
979
  * NOTE 2: calling `destroy()` while a `purchase()` is in flight may
982
980
  * leave the result in an inconsistent state — the backend may have
983
- * recorded the entitlement but the native `acknowledge()` call will
984
- * be a no-op (because cdv's `pendingFinish` was cleared mid-flow).
985
- * On Android this means Google auto-refunds in 3 days. Avoid by
986
- * awaiting the in-flight `purchase()` before calling `destroy()`.
981
+ * recorded the entitlement but the native `acknowledge()` call may not
982
+ * have run yet. On Android this means Google auto-refunds in 3 days
983
+ * (the unfinished-transaction recovery on the next launch re-acks, but
984
+ * only if it runs within that window). Avoid by awaiting the in-flight
985
+ * `purchase()` before calling `destroy()`.
987
986
  */
988
987
  destroy(): Promise<void>;
989
988
  /**
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Capacitor } from '@capacitor/core';
2
- import 'cordova-plugin-purchase';
2
+ import { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';
3
3
  import { z } from 'zod';
4
4
  import { Preferences } from '@capacitor/preferences';
5
5
 
@@ -83,7 +83,7 @@ var init_errors = __esm({
83
83
  NOT_INITIALIZED: "Call iap.initialize() before this method, or recreate the instance after destroy().",
84
84
  // Native plugin
85
85
  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().",
86
- 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.",
86
+ 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.",
87
87
  PRODUCT_NOT_FOUND: "Ensure the productId is registered in App Store Connect / Play Console AND in your createIAP({ products }) config.",
88
88
  USER_CANCELLED: "No action needed \u2014 the user dismissed the native purchase sheet.",
89
89
  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.",
@@ -137,295 +137,170 @@ var init_platform = __esm({
137
137
  }
138
138
  });
139
139
 
140
- // src/adapters/native/cdv/native-adapter.ts
140
+ // src/adapters/native/capgo/native-adapter.ts
141
141
  var native_adapter_exports = {};
142
142
  __export(native_adapter_exports, {
143
- CdvNativeAdapter: () => CdvNativeAdapter
143
+ CapgoNativeAdapter: () => CapgoNativeAdapter
144
144
  });
145
- function getCdv() {
146
- const candidate = globalThis.CdvPurchase;
147
- if (!candidate || !candidate.store) {
148
- throw new IAPError({
149
- code: IAPErrorCode.BILLING_NOT_AVAILABLE,
150
- message: "cordova-plugin-purchase is not available. Ensure the plugin is installed and `npx cap sync` has run."
151
- });
152
- }
153
- return candidate;
154
- }
155
- function currentCdvPlatform() {
156
- const cdv = getCdv();
157
- const platform = getPlatform();
158
- if (platform === "android") return cdv.Platform.GOOGLE_PLAY;
159
- return cdv.Platform.APPLE_APPSTORE;
160
- }
161
- function mapProductType(type) {
162
- const cdv = getCdv();
163
- switch (type) {
164
- case "subscription":
165
- return cdv.ProductType.PAID_SUBSCRIPTION;
166
- case "consumable":
167
- return cdv.ProductType.CONSUMABLE;
168
- default:
169
- return cdv.ProductType.NON_CONSUMABLE;
170
- }
171
- }
172
- function inferProductType(tx, configured) {
173
- const id = tx.products[0]?.id;
174
- if (!id) return "product";
175
- const match = configured.find((p) => p.id === id);
176
- return match?.type ?? "product";
177
- }
178
145
  function normalizeProduct(p, type) {
179
- const offer = p.getOffer();
180
- const phase = offer?.pricingPhases?.[0];
181
- const priceMicros = phase?.priceMicros?.toString() ?? "0";
182
- const priceString = phase?.price ?? "";
183
- const currency = phase?.currency ?? "";
146
+ const priceMicros = Math.round(p.price * 1e6).toString();
184
147
  return {
185
- id: p.id,
148
+ id: p.identifier,
186
149
  type,
187
- title: p.title ?? p.id,
188
- description: p.description ?? "",
189
- priceString,
150
+ title: p.title,
151
+ description: p.description,
152
+ priceString: p.priceString,
190
153
  priceMicros,
191
- currency
154
+ currency: p.currencyCode
192
155
  };
193
156
  }
194
- function normalizeTransaction(tx, productType, token) {
195
- const platform = tx.platform === "ios-appstore" ? "apple" : "google";
196
- const productId = tx.products[0]?.id ?? "";
197
- const result = {
157
+ function normalizeTransaction(tx, productType) {
158
+ const platform = inferPlatform(tx);
159
+ const token = platform === "google" ? tx.purchaseToken ?? tx.transactionId : tx.transactionId;
160
+ const native = {
198
161
  platform,
199
- productId,
162
+ productId: tx.productIdentifier,
200
163
  token,
201
164
  productType,
202
165
  raw: tx
203
166
  };
204
- if (platform === "google") {
205
- const packageName = googlePackageName(tx);
206
- if (packageName) result.packageName = packageName;
167
+ return native;
168
+ }
169
+ function inferPlatform(tx) {
170
+ if (tx.purchaseToken !== void 0 || tx.purchaseState !== void 0 || tx.orderId !== void 0) {
171
+ return "google";
172
+ }
173
+ if (tx.receipt !== void 0 || tx.jwsRepresentation !== void 0) {
174
+ return "apple";
207
175
  }
208
- return result;
176
+ return getPlatform() === "android" ? "google" : "apple";
209
177
  }
210
- function googlePackageName(tx) {
211
- const googleTx = tx;
212
- return googleTx.nativePurchase?.packageName;
178
+ function inferProductType(tx) {
179
+ if (tx.productType === "subs") return "subscription";
180
+ return "product";
213
181
  }
214
- function transactionToken(tx) {
215
- if (tx.platform === "ios-appstore") {
216
- return tx.transactionId || null;
217
- }
218
- const googleTx = tx;
219
- return googleTx.nativePurchase?.purchaseToken ?? googleTx.parentReceipt?.purchaseToken ?? tx.transactionId ?? null;
182
+ function mapToPluginPurchaseType(type) {
183
+ return type === "subscription" ? PURCHASE_TYPE.SUBS : PURCHASE_TYPE.INAPP;
220
184
  }
221
- function mapOrderError(err, productId) {
222
- const cdv = getCdv();
223
- const cancelled = cdv.ErrorCode?.PAYMENT_CANCELLED;
224
- if (cancelled !== void 0 && err.code === cancelled) {
185
+ function mapPurchaseError(error, productId) {
186
+ if (error instanceof IAPError) return error;
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const lower = message.toLowerCase();
189
+ if (lower.includes("cancel")) {
225
190
  return new IAPError({
226
191
  code: IAPErrorCode.USER_CANCELLED,
227
- message: "User cancelled the native purchase sheet.",
228
- cause: err
192
+ message: `Purchase of "${productId}" was cancelled.`,
193
+ cause: error
194
+ });
195
+ }
196
+ if (lower.includes("pending")) {
197
+ return new IAPError({
198
+ code: IAPErrorCode.PURCHASE_PENDING,
199
+ message: `Purchase of "${productId}" is pending external clearance.`,
200
+ cause: error
201
+ });
202
+ }
203
+ if (lower.includes("product not found")) {
204
+ return new IAPError({
205
+ code: IAPErrorCode.PRODUCT_NOT_FOUND,
206
+ message: `Product "${productId}" was not found in the store catalog.`,
207
+ cause: error
229
208
  });
230
209
  }
231
210
  return new IAPError({
232
211
  code: IAPErrorCode.STORE_ERROR,
233
- message: err.message ?? `order() failed for ${productId}.`,
234
- cause: err
212
+ message: `Native purchase of "${productId}" failed.`,
213
+ cause: error
235
214
  });
236
215
  }
237
- var CdvNativeAdapter;
216
+ var CapgoNativeAdapter;
238
217
  var init_native_adapter = __esm({
239
- "src/adapters/native/cdv/native-adapter.ts"() {
218
+ "src/adapters/native/capgo/native-adapter.ts"() {
240
219
  init_errors();
241
220
  init_platform();
242
- CdvNativeAdapter = class {
243
- products;
244
- bootstrapped = false;
245
- bootstrapping = null;
246
- pendingFinish = /* @__PURE__ */ new Map();
247
- /** Long-lived bootstrap-time .approved() listener — kept for dispose(). */
248
- bootstrapApprovedHandler = null;
249
- constructor(opts) {
250
- this.products = opts.products;
251
- }
221
+ CapgoNativeAdapter = class {
252
222
  async isAvailable() {
253
223
  try {
254
- await this.bootstrap();
255
- return true;
224
+ const result = await NativePurchases.isBillingSupported();
225
+ return result.isBillingSupported;
256
226
  } catch {
257
227
  return false;
258
228
  }
259
229
  }
260
230
  async getProducts(requests) {
261
231
  if (requests.length === 0) return [];
262
- const store = await this.ensureStore();
263
- await store.update();
264
- const out = [];
232
+ const inappIds = [];
233
+ const subsIds = [];
234
+ const requestById = /* @__PURE__ */ new Map();
265
235
  for (const req of requests) {
266
- const native = store.get(req.id);
267
- if (!native) continue;
268
- out.push(normalizeProduct(native, req.type));
236
+ requestById.set(req.id, req.type);
237
+ if (req.type === "subscription") {
238
+ subsIds.push(req.id);
239
+ } else {
240
+ inappIds.push(req.id);
241
+ }
269
242
  }
270
- return out;
243
+ const [inapp, subs] = await Promise.all([
244
+ inappIds.length > 0 ? NativePurchases.getProducts({
245
+ productIdentifiers: inappIds,
246
+ productType: PURCHASE_TYPE.INAPP
247
+ }) : Promise.resolve({ products: [] }),
248
+ subsIds.length > 0 ? NativePurchases.getProducts({
249
+ productIdentifiers: subsIds,
250
+ productType: PURCHASE_TYPE.SUBS
251
+ }) : Promise.resolve({ products: [] })
252
+ ]);
253
+ const all = [...inapp.products, ...subs.products];
254
+ return all.map((p) => normalizeProduct(p, requestById.get(p.identifier) ?? "product"));
271
255
  }
272
256
  async purchaseProduct(opts) {
273
- const store = await this.ensureStore();
274
- const native = store.get(opts.productId);
275
- if (!native) {
276
- throw new IAPError({
277
- code: IAPErrorCode.PRODUCT_NOT_FOUND,
278
- message: `Product "${opts.productId}" not registered or not available from the store.`
279
- });
280
- }
281
- const offer = opts.androidPlanId ? native.getOffer(opts.androidPlanId) ?? native.getOffer() : native.getOffer();
282
- if (!offer) {
283
- throw new IAPError({
284
- code: IAPErrorCode.PRODUCT_NOT_FOUND,
285
- message: `Product "${opts.productId}" has no purchasable offer${opts.androidPlanId ? ` (planId="${opts.androidPlanId}")` : ""}.`
257
+ const purchaseType = mapToPluginPurchaseType(opts.productType);
258
+ const isConsumable = opts.productType === "consumable";
259
+ let tx;
260
+ try {
261
+ tx = await NativePurchases.purchaseProduct({
262
+ productIdentifier: opts.productId,
263
+ productType: purchaseType,
264
+ planIdentifier: opts.androidPlanId,
265
+ appAccountToken: opts.appAccountToken,
266
+ isConsumable,
267
+ autoAcknowledgePurchases: false
286
268
  });
269
+ } catch (error) {
270
+ throw mapPurchaseError(error, opts.productId);
287
271
  }
288
- return new Promise((resolve, reject) => {
289
- let settled = false;
290
- const cleanup = () => {
291
- store.off(handleApproved);
292
- };
293
- const handleApproved = (tx) => {
294
- if (settled) return;
295
- if (!tx.products.some((p) => p.id === opts.productId)) return;
296
- const token = transactionToken(tx);
297
- if (!token) {
298
- settled = true;
299
- cleanup();
300
- reject(
301
- new IAPError({
302
- code: IAPErrorCode.STORE_ERROR,
303
- message: `Approved transaction for "${opts.productId}" has no token; cannot verify.`
304
- })
305
- );
306
- return;
307
- }
308
- settled = true;
309
- cleanup();
310
- const normalized = normalizeTransaction(tx, opts.productType, token);
311
- this.pendingFinish.set(token, tx);
312
- resolve(normalized);
313
- };
314
- store.when().approved(handleApproved);
315
- const additionalData = opts.appAccountToken ? { applicationUsername: opts.appAccountToken } : void 0;
316
- void Promise.resolve(offer.order(additionalData)).then((err) => {
317
- if (settled) return;
318
- if (!err) return;
319
- settled = true;
320
- cleanup();
321
- reject(mapOrderError(err, opts.productId));
322
- }).catch((cause) => {
323
- if (settled) return;
324
- settled = true;
325
- cleanup();
326
- reject(
327
- new IAPError({
328
- code: IAPErrorCode.STORE_ERROR,
329
- message: `order() rejected for ${opts.productId}.`,
330
- cause
331
- })
332
- );
333
- });
334
- });
272
+ return normalizeTransaction(tx, opts.productType);
335
273
  }
336
274
  async getOwnedTransactions() {
337
- const store = await this.ensureStore();
338
- await store.restorePurchases();
339
- const out = [];
340
- for (const tx of store.localTransactions) {
341
- if (tx.state !== getCdv().TransactionState.APPROVED) continue;
342
- const token = transactionToken(tx);
343
- if (!token) continue;
344
- const normalized = normalizeTransaction(tx, inferProductType(tx, this.products), token);
345
- this.pendingFinish.set(token, tx);
346
- out.push(normalized);
347
- }
348
- return out;
275
+ const result = await NativePurchases.getPurchases();
276
+ return result.purchases.filter((tx) => tx.purchaseState === void 0 || tx.purchaseState === "1").map((tx) => normalizeTransaction(tx, inferProductType(tx)));
349
277
  }
350
278
  async acknowledge(transaction) {
351
- const cdvTx = this.pendingFinish.get(transaction.token);
352
- if (!cdvTx) {
353
- return;
354
- }
355
279
  try {
356
- await cdvTx.finish();
357
- } catch (cause) {
280
+ await NativePurchases.acknowledgePurchase({
281
+ purchaseToken: transaction.token
282
+ });
283
+ } catch (error) {
358
284
  throw new IAPError({
359
285
  code: IAPErrorCode.STORE_ERROR,
360
- message: `Failed to finish transaction for ${transaction.productId}.`,
361
- cause,
286
+ message: `Failed to acknowledge transaction for ${transaction.productId}.`,
287
+ cause: error,
362
288
  recoverable: true
363
289
  });
364
290
  }
365
- this.pendingFinish.delete(transaction.token);
366
291
  }
367
292
  async manageSubscriptions() {
368
- const store = await this.ensureStore();
369
- const err = await store.manageSubscriptions();
370
- if (err) {
293
+ try {
294
+ await NativePurchases.manageSubscriptions();
295
+ } catch (error) {
371
296
  throw new IAPError({
372
297
  code: IAPErrorCode.STORE_ERROR,
373
- message: err.message ?? "Failed to open subscription management."
298
+ message: "Failed to open the native subscription management UI.",
299
+ cause: error
374
300
  });
375
301
  }
376
302
  }
377
303
  async dispose() {
378
- if (this.bootstrapApprovedHandler) {
379
- try {
380
- const cdv = globalThis.CdvPurchase;
381
- cdv?.store?.off(this.bootstrapApprovedHandler);
382
- } catch {
383
- }
384
- this.bootstrapApprovedHandler = null;
385
- }
386
- this.pendingFinish.clear();
387
- this.bootstrapped = false;
388
- this.bootstrapping = null;
389
- }
390
- // ----- internals -----
391
- async ensureStore() {
392
- await this.bootstrap();
393
- return getCdv().store;
394
- }
395
- bootstrap() {
396
- if (this.bootstrapped) return Promise.resolve();
397
- if (this.bootstrapping) return this.bootstrapping;
398
- this.bootstrapping = (async () => {
399
- const cdv = getCdv();
400
- const platform = currentCdvPlatform();
401
- cdv.store.register(
402
- this.products.map((p) => ({
403
- id: p.id,
404
- type: mapProductType(p.type),
405
- platform
406
- }))
407
- );
408
- const errors = await cdv.store.initialize([platform]);
409
- if (errors && errors.length > 0) {
410
- const first = errors[0];
411
- throw new IAPError({
412
- code: IAPErrorCode.BILLING_NOT_AVAILABLE,
413
- message: first?.message ?? "cordova-plugin-purchase initialize() reported errors."
414
- });
415
- }
416
- const handler = (tx) => {
417
- const token = transactionToken(tx);
418
- if (!token) return;
419
- if (!this.pendingFinish.has(token)) {
420
- this.pendingFinish.set(token, tx);
421
- }
422
- };
423
- this.bootstrapApprovedHandler = handler;
424
- cdv.store.when().approved(handler);
425
- await cdv.store.update();
426
- this.bootstrapped = true;
427
- })();
428
- return this.bootstrapping;
429
304
  }
430
305
  };
431
306
  }
@@ -958,11 +833,11 @@ var WebStubAdapter = class {
958
833
  };
959
834
 
960
835
  // src/adapters/native/index.ts
961
- async function selectNativeAdapter(options) {
836
+ async function selectNativeAdapter() {
962
837
  const platform = getPlatform();
963
838
  if (platform === "ios" || platform === "android") {
964
839
  const mod = await Promise.resolve().then(() => (init_native_adapter(), native_adapter_exports));
965
- return new mod.CdvNativeAdapter({ products: options.products });
840
+ return new mod.CapgoNativeAdapter();
966
841
  }
967
842
  return new WebStubAdapter();
968
843
  }
@@ -1900,7 +1775,7 @@ function createIAP(input) {
1900
1775
  state.products = Object.freeze([...validated.data]);
1901
1776
  state.logger.debug(`Resolved ${validated.data.length} product(s) from backend manifest.`);
1902
1777
  }
1903
- state.adapter = await selectNativeAdapter({ products: state.products });
1778
+ state.adapter = await selectNativeAdapter();
1904
1779
  const configGetAuthHeaders = state.config.backend.getAuthHeaders;
1905
1780
  const getAuthHeaders = configGetAuthHeaders ? async () => configGetAuthHeaders() : async () => ({});
1906
1781
  const sharedDeps = {