@nosslabs/iap 8.0.0 → 8.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/dist/index.d.cts CHANGED
@@ -64,6 +64,403 @@ declare class HttpClient {
64
64
  private fetchWithTimeout;
65
65
  }
66
66
 
67
+ type ProductType = 'subscription' | 'product' | 'consumable';
68
+ interface ConfiguredProduct {
69
+ id: string;
70
+ type: ProductType;
71
+ /**
72
+ * Required for Android subscriptions — the base plan identifier configured in
73
+ * Google Play Console. Ignored on iOS and for non-subscription products.
74
+ *
75
+ * Keep it in step with Play Console. When it matches no offer, the native
76
+ * plugin does **not** fail — it falls back to the first available base
77
+ * plan, so a stale value silently purchases a different plan. See
78
+ * `docs/guide/native-layer.md`.
79
+ */
80
+ androidPlanId?: string;
81
+ }
82
+ /** What kind of offer this is, as the store classifies it. */
83
+ type SubscriptionOfferType = 'introductory' | 'promotional' | 'win-back' | 'unknown';
84
+ /**
85
+ * How the offer is paid for.
86
+ *
87
+ * - `free-trial` — no charge for the offer period.
88
+ * - `pay-up-front` — one discounted charge covering the whole offer period.
89
+ * - `pay-as-you-go` — a discounted charge each period, for `periodCount`
90
+ * periods.
91
+ */
92
+ type SubscriptionOfferPayment = 'free-trial' | 'pay-up-front' | 'pay-as-you-go' | 'unknown';
93
+ /**
94
+ * An introductory or promotional offer on a subscription, normalized across
95
+ * platforms.
96
+ *
97
+ * Render `priceString` as-is; it is the store's own localized string, in the
98
+ * store's currency and format. Never reconstruct a price from `priceMicros`
99
+ * for display — that is for arithmetic and comparison only.
100
+ */
101
+ interface SubscriptionOffer {
102
+ /**
103
+ * The store's identifier for the offer. `null` for an offer the store does
104
+ * not name — typically an iOS introductory offer, or an Android base-plan
105
+ * intro phase (which has no offer id of its own).
106
+ */
107
+ id: string | null;
108
+ type: SubscriptionOfferType;
109
+ payment: SubscriptionOfferPayment;
110
+ /** Localized price string for the offer, e.g. "$0.00" or "₱49.00". */
111
+ priceString: string;
112
+ /** BigInt-safe string of micro-units, e.g. "0" or "49000000". */
113
+ priceMicros: string;
114
+ /** ISO 4217 code, e.g. "PHP". */
115
+ currency: string;
116
+ /**
117
+ * Offer period as an ISO 8601 duration, e.g. `"P1W"`. `null` if unknown.
118
+ *
119
+ * Single-component on Android: the native plugin's period parser keeps
120
+ * only the largest non-zero unit, so a Play period of `P1M15D` arrives
121
+ * as `P1M`.
122
+ */
123
+ period: string | null;
124
+ /**
125
+ * How many periods the offer applies for.
126
+ *
127
+ * On iOS: `1` for a free trial or a pay-up-front offer, higher for
128
+ * pay-as-you-go.
129
+ *
130
+ * **On Android this is usually `0`**, and that does not mean zero
131
+ * periods. It is Play's `getBillingCycleCount()`, which reports `0` for
132
+ * both a non-recurring phase and an infinitely recurring one — so a
133
+ * pay-up-front Android offer reports `0` too. Treat `0` as "the store
134
+ * did not say" and do not render it as a count.
135
+ */
136
+ periodCount: number;
137
+ }
138
+ /**
139
+ * Native product info merged with configured metadata.
140
+ * Library-public shape — kept stable across Capacitor versions.
141
+ */
142
+ interface Product {
143
+ id: string;
144
+ type: ProductType;
145
+ title: string;
146
+ description: string;
147
+ /** Localized price string, e.g. "$4.99". Always render this — never hardcode. */
148
+ priceString: string;
149
+ /** BigInt as string to avoid precision loss, e.g. "4990000". */
150
+ priceMicros: string;
151
+ /** ISO 4217 code, e.g. "USD". */
152
+ currency: string;
153
+ /**
154
+ * The recurring billing period as an ISO 8601 duration, e.g. `"P1M"`.
155
+ *
156
+ * **iOS only.** `null` on Android: the native plugin collapses Play's
157
+ * pricing phases before exposing them and never surfaces a base plan's
158
+ * billing period, so it is not reachable. Where you need the cadence on
159
+ * both platforms, take it from your own product configuration — you chose
160
+ * the base plan's period in Play Console.
161
+ */
162
+ subscriptionPeriod?: string | null;
163
+ /**
164
+ * The subscription group this product belongs to. Products in one group are
165
+ * mutually exclusive, and on iOS introductory-offer eligibility is assessed
166
+ * per group rather than per product.
167
+ *
168
+ * **iOS only.** `null` on Android, which has no equivalent concept exposed.
169
+ */
170
+ subscriptionGroupId?: string | null;
171
+ /**
172
+ * The introductory offer on this subscription, or `null` when it has none.
173
+ *
174
+ * **Whether *this user* is eligible for it cannot be known on the device.**
175
+ * The native plugin exposes no eligibility query on either platform, so an
176
+ * offer being present here means "this product has one", not "this user
177
+ * will get it". The store's own purchase sheet applies eligibility at
178
+ * purchase time and is the authority. Write paywall copy that reads
179
+ * correctly either way, or confirm eligibility server-side.
180
+ */
181
+ introductoryOffer?: SubscriptionOffer | null;
182
+ /**
183
+ * Promotional and win-back offers on this subscription. Empty when there
184
+ * are none. Same eligibility caveat as {@link Product.introductoryOffer}.
185
+ *
186
+ * **On Android, do not render these as the offer's own terms.** The
187
+ * native plugin builds each promotional entry from the base plan's
188
+ * *standard* recurring pricing phase, not from the promotional phase —
189
+ * so a free-trial promo arrives as `payment: 'pay-as-you-go'` at the
190
+ * full price. Use them to know an offer exists and to identify it; take
191
+ * the terms from your backend or from Play Console. iOS reports the
192
+ * offer's real terms, and {@link Product.introductoryOffer} is
193
+ * unaffected on both platforms.
194
+ */
195
+ promotionalOffers?: SubscriptionOffer[];
196
+ /**
197
+ * Android: Play's token for the specific offer this entry describes, and
198
+ * the offer's id (`null` for a base plan with no offer).
199
+ *
200
+ * Exposed for identification and server-side reasoning only. A purchase
201
+ * **cannot** select an offer by token: the native plugin's purchase call
202
+ * takes a base plan id, and picks the offer itself. See
203
+ * `docs/guide/native-layer.md`.
204
+ */
205
+ androidOfferToken?: string | null;
206
+ androidOfferId?: string | null;
207
+ /**
208
+ * Android: the base plan this entry describes, as configured in Play
209
+ * Console. `null` on iOS, which has no base-plan concept — there a
210
+ * product id identifies the subscription directly.
211
+ *
212
+ * `id` is always the product id you configured, on both platforms, so it
213
+ * round-trips to `purchase({ productId })`. This is the extra level Play
214
+ * adds underneath it.
215
+ */
216
+ androidBasePlanId?: string | null;
217
+ }
218
+
219
+ /** Platform a transaction originated from. */
220
+ type Platform = 'apple' | 'google';
221
+ /**
222
+ * Where a subscription stands with the store, as the store reports it.
223
+ *
224
+ * iap passes these through; it does not interpret them. Deciding what counts
225
+ * as "entitled" — whether a grace period still grants access, say — is the
226
+ * consumer's business rule, and the authoritative answer comes from the
227
+ * backend (PLAN.md §14).
228
+ */
229
+ type SubscriptionState = 'subscribed' | 'expired' | 'revoked' | 'in-grace-period' | 'in-billing-retry' | 'unknown';
230
+ /**
231
+ * Renewal detail for an auto-renewing subscription, normalized from the
232
+ * store's own fields.
233
+ *
234
+ * **iOS only, in practice.** Every field is `null` on Android: the native
235
+ * plugin exposes neither `isAutoRenewing` nor a renewal date, and reports
236
+ * cancellation as always-null. On Android this information lives server-side
237
+ * (Google Play Developer API), so take it from your backend.
238
+ */
239
+ interface SubscriptionRenewal {
240
+ state: SubscriptionState | null;
241
+ /**
242
+ * Whether the subscription renews at the end of the current period.
243
+ * `false` means the user cancelled but still has access until it expires.
244
+ */
245
+ autoRenewing: boolean | null;
246
+ /** When the next period begins, ISO 8601. */
247
+ renewsAt: string | null;
248
+ /**
249
+ * The product the subscription renews into — different from the current
250
+ * one after a plan change takes effect at the period boundary.
251
+ */
252
+ renewalProductId: string | null;
253
+ /** When the current period ends, ISO 8601. */
254
+ expiresAt: string | null;
255
+ }
256
+ /**
257
+ * Normalized native transaction handed to core flows.
258
+ * Plugin-version differences are translated by the adapter so this shape
259
+ * stays stable across Capacitor majors.
260
+ */
261
+ interface NativeTransaction {
262
+ platform: Platform;
263
+ productId: string;
264
+ /**
265
+ * Apple: the StoreKit `transactionId` (numeric string) — **changes with
266
+ * every renewal**. Google: the `purchaseToken` from Play Billing.
267
+ *
268
+ * This is the handle for acknowledging *this* transaction. To store a
269
+ * reference to the purchase itself, use `purchaseReference` — which is
270
+ * what a consumer sees, as `StorePurchase.purchaseReference`.
271
+ */
272
+ token: string;
273
+ /**
274
+ * The stable reference to the purchase, which survives renewals — store
275
+ * this when you need to identify the subscription later.
276
+ *
277
+ * Apple: `originalTransactionId`, read from the signed `jwsRepresentation`
278
+ * payload (the plugin does not expose it as a field). Falls back to
279
+ * `token` when the JWS is absent or unreadable, so it is always set.
280
+ * Google: the `purchaseToken`, identical to `token`.
281
+ */
282
+ purchaseReference: string;
283
+ /** Google only — required by Attesto's `/v1/google/verify`. */
284
+ packageName?: string;
285
+ /** Product type the transaction was originally purchased as. */
286
+ productType: ProductType;
287
+ /**
288
+ * The binding this purchase was made with, read back off the transaction:
289
+ * `appAccountToken` on iOS, `obfuscatedAccountId` on Android. `null` when
290
+ * the purchase carried none.
291
+ *
292
+ * Use it to check what a purchase is bound to. iap never generates one —
293
+ * it is whatever was passed as `iap.purchase({ appUserId })`.
294
+ */
295
+ appUserId?: string | null;
296
+ /**
297
+ * Renewal detail for a subscription, or `null` for a non-subscription and
298
+ * on platforms that do not report it. See {@link SubscriptionRenewal}.
299
+ */
300
+ subscription?: SubscriptionRenewal | null;
301
+ /** Optional pass-through of plugin-native fields for backend hints. */
302
+ raw?: unknown;
303
+ }
304
+ /**
305
+ * Verified transaction returned by the consumer backend after Attesto check.
306
+ * Generic over the backend-defined transaction shape.
307
+ */
308
+ interface VerifiedTransaction<TExtra = Record<string, unknown>> {
309
+ id: string;
310
+ productId: string;
311
+ expiresAt: string | null;
312
+ verifiedAt: string;
313
+ raw?: TExtra;
314
+ }
315
+
316
+ /**
317
+ * How strongly a purchase was checked. Exposed rather than hidden, because
318
+ * the two platforms genuinely differ and a consumer deciding what to unlock
319
+ * offline deserves to know which one it got.
320
+ *
321
+ * - `store-signed` — iOS. StoreKit 2 verified Apple's signature over the
322
+ * transaction in native code before it ever reached the WebView; the
323
+ * native layer resolves a purchase only for a `VerificationResult.verified`
324
+ * and rejects anything else. So this is a cryptographic guarantee, made by
325
+ * the platform rather than by iap.
326
+ * - `store-reported` — Android. The native layer exposes neither Play's
327
+ * purchase signature nor the original JSON, so **no cryptographic check is
328
+ * possible on the device**. This means "Play told this device it owns
329
+ * this", which is a real answer and the one Play's own client APIs give —
330
+ * but it is the store's say-so, not proof.
331
+ *
332
+ * Neither is a substitute for server-side validation against the store's
333
+ * API. A rooted device can lie to the app about both.
334
+ */
335
+ type PurchaseAssurance = 'store-signed' | 'store-reported';
336
+ /** Why a local check refused a purchase. Stable strings, safe to switch on. */
337
+ type LocalVerificationError = 'PRODUCT_NOT_CONFIGURED' | 'BINDING_MISMATCH'
338
+ /**
339
+ * The app binds purchases to an account, and there is no account to bind
340
+ * to right now — a signed-out session, or a `verification.appUserId`
341
+ * getter that threw. Raised by `LocalVerificationAdapter`, never by
342
+ * {@link verifyPurchaseOnDevice}, which is told the binding rather than
343
+ * resolving it.
344
+ */
345
+ | 'BINDING_UNAVAILABLE' | 'NOT_PURCHASED' | 'REVOKED' | 'SUBSCRIPTION_EXPIRED';
346
+ /** A purchase that passed the on-device checks. */
347
+ interface LocallyVerifiedPurchase {
348
+ productId: string;
349
+ platform: Platform;
350
+ /**
351
+ * What the purchase was bought as.
352
+ *
353
+ * Exposed because it changes what an entitlement *means*, and a mapper
354
+ * needs to know. In particular iOS's owned set retains **finished
355
+ * consumables**, so a mapper that grants on every verified purchase
356
+ * re-grants a consumable on every refresh, forever. A consumable's effect
357
+ * belongs in your own records, not in a standing entitlement.
358
+ */
359
+ productType: ProductType;
360
+ /** Stable across renewals. See `NativeTransaction.purchaseReference`. */
361
+ purchaseReference: string;
362
+ /** The binding the purchase carried, or `null`. */
363
+ appUserId: string | null;
364
+ /** ISO 8601 for a subscription; `null` for a non-expiring purchase. */
365
+ expiresAt: string | null;
366
+ subscription: SubscriptionRenewal | null;
367
+ /** How strongly this was checked. See {@link PurchaseAssurance}. */
368
+ assurance: PurchaseAssurance;
369
+ }
370
+ type LocalVerification = {
371
+ valid: true;
372
+ purchase: LocallyVerifiedPurchase;
373
+ } | {
374
+ valid: false;
375
+ error: LocalVerificationError;
376
+ message: string;
377
+ };
378
+ interface LocalVerificationOptions {
379
+ /**
380
+ * Product ids the app sells. A transaction for anything else is refused —
381
+ * the store can report purchases from a previous version of the app, or
382
+ * from a product that has since been retired.
383
+ */
384
+ productIds: readonly string[];
385
+ /**
386
+ * When set, the purchase must carry this binding. Compared
387
+ * case-insensitively, because StoreKit returns an uppercase UUID string
388
+ * while callers deal in lowercase.
389
+ *
390
+ * `null` or omitted skips the check: a purchase made before the app
391
+ * started binding, or a flow with no user identity, carries none.
392
+ */
393
+ appUserId?: string | null;
394
+ /** Extra time past `expiresAt` during which a subscription still counts. */
395
+ graceMs?: number;
396
+ /** Injectable clock. Defaults to `Date.now`. */
397
+ now?: () => number;
398
+ }
399
+ /**
400
+ * Whether an ISO 8601 expiry has passed, by the **device** clock.
401
+ *
402
+ * The device clock is the only clock available with no backend, and it can
403
+ * be wrong — a user can set it back and extend a subscription. That is the
404
+ * accepted cost of working offline, and the reason this is not a substitute
405
+ * for server-side validation.
406
+ *
407
+ * An unparseable or absent expiry is treated as "does not expire" rather
408
+ * than "expired": refusing a purchase because its expiry string was
409
+ * malformed would revoke access over a formatting problem.
410
+ *
411
+ * `expiresAt` must be ISO 8601. `Date.parse` is lenient and
412
+ * implementation-defined for anything else — `'10/26/2026'` parses as
413
+ * *local* time in V8 — so a non-ISO string may compare against the wrong
414
+ * instant. Everything iap produces is ISO 8601; this matters only for a
415
+ * value you supply. An exact boundary (`now === expiry`) reads as active.
416
+ */
417
+ declare function isExpired(expiresAt: string | null | undefined, now: number, graceMs?: number): boolean;
418
+ /**
419
+ * Check a purchase on the device, with no backend.
420
+ *
421
+ * This is the whole of what can be checked without a server, and it is
422
+ * deliberately a short list:
423
+ *
424
+ * 1. The product is one the app actually sells.
425
+ * 2. The binding matches, when one is expected.
426
+ * 3. On Android, the store calls the purchase purchased rather than pending.
427
+ * 4. The purchase has not been revoked or refunded, where the platform
428
+ * says so locally (iOS does; Android does not).
429
+ * 5. A subscription has not expired by the device clock.
430
+ *
431
+ * What it does **not** do is verify a signature. On iOS it does not need to —
432
+ * the platform already did, natively, and {@link PurchaseAssurance}
433
+ * records that. On Android it cannot: the native layer exposes neither
434
+ * Play's purchase signature nor the original JSON it is computed over.
435
+ *
436
+ * So a passing result means "this device's store says it owns this, and the
437
+ * claim is internally consistent" — strong on iOS, the store's word on
438
+ * Android. For anything where being wrong is expensive, verify server-side.
439
+ */
440
+ declare function verifyPurchaseOnDevice(transaction: NativeTransaction, options: LocalVerificationOptions): LocalVerification;
441
+
442
+ declare const entitlementBaseSchema: z.ZodObject<{
443
+ key: z.ZodString;
444
+ productId: z.ZodString;
445
+ expiresAt: z.ZodNullable<z.ZodString>;
446
+ }, "strip", z.ZodTypeAny, {
447
+ key: string;
448
+ productId: string;
449
+ expiresAt: string | null;
450
+ }, {
451
+ key: string;
452
+ productId: string;
453
+ expiresAt: string | null;
454
+ }>;
455
+ /**
456
+ * Minimum shape every entitlement returned by the backend must satisfy.
457
+ * Consumer-defined fields pass through unvalidated unless the consumer
458
+ * supplies their own schema via `config.backend.entitlementSchema`.
459
+ */
460
+ type EntitlementBase = z.infer<typeof entitlementBaseSchema>;
461
+ /** Default shape used when no `TEntitlement` type parameter is given. */
462
+ type DefaultEntitlement = EntitlementBase;
463
+
67
464
  declare const backendConfigSchema: z.ZodEffects<z.ZodObject<{
68
465
  /**
69
466
  * Custom backend transport. If provided, all HTTP-specific fields below
@@ -200,22 +597,87 @@ declare const backendConfigSchema: z.ZodEffects<z.ZodObject<{
200
597
  timeoutMs?: number | undefined;
201
598
  retries?: number | undefined;
202
599
  }>;
600
+ /**
601
+ * Verify purchases on the device instead of through a backend.
602
+ *
603
+ * For an app with no server at all. Every safety guarantee still applies —
604
+ * the purchase flow, the acknowledge-after-verify ordering and the
605
+ * unfinished-transaction recovery are all above the adapter boundary and do
606
+ * not know which mode they are in. What changes is who answers "is this
607
+ * purchase real", and how strong that answer is.
608
+ *
609
+ * Read `docs/guide/offline-and-no-backend.md` before choosing this: on
610
+ * Android no cryptographic check is possible on the device, so a passing
611
+ * verification there means "Play told this device it owns this".
612
+ */
613
+ declare const verificationConfigSchema: z.ZodObject<{
614
+ mode: z.ZodLiteral<"local">;
615
+ /**
616
+ * Maps a verified purchase to the entitlements it grants. Required: iap
617
+ * does not know what a product entitles anyone to, and in local mode
618
+ * there is no backend to ask. Type is checked at runtime via a shape
619
+ * guard, not zod — see {@link VerificationConfig}.
620
+ */
621
+ entitlements: z.ZodUnknown;
622
+ /**
623
+ * The binding every purchase must carry, as passed to
624
+ * `purchase({ appUserId })`. A purchase bound to something else is
625
+ * refused. Omit it if the app does not bind purchases.
626
+ *
627
+ * Either the id itself, or a function returning it — in which case it is
628
+ * read **at the time of each check**, so the binding follows the session.
629
+ * A fixed string is captured at `createIAP()` time and cannot track a
630
+ * login change, which in a multi-user app means every purchase after a
631
+ * switch is refused as the previous user's.
632
+ *
633
+ * The function may return `null` for "nobody is signed in right now".
634
+ * That is **not** the same as using no binding: iap refuses the purchase
635
+ * rather than dropping the check. See `LocalVerificationAdapter`.
636
+ *
637
+ * Typed via the overlay on {@link VerificationConfig}; validated in the
638
+ * schema's `superRefine`, the same way the backend's functions are.
639
+ */
640
+ appUserId: z.ZodOptional<z.ZodUnknown>;
641
+ }, "strip", z.ZodTypeAny, {
642
+ mode: "local";
643
+ entitlements?: unknown;
644
+ appUserId?: unknown;
645
+ }, {
646
+ mode: "local";
647
+ entitlements?: unknown;
648
+ appUserId?: unknown;
649
+ }>;
203
650
  declare const storageConfigSchema: z.ZodObject<{
204
651
  type: z.ZodDefault<z.ZodEnum<["preferences", "memory", "custom"]>>;
205
652
  namespace: z.ZodDefault<z.ZodString>;
206
653
  adapter: z.ZodOptional<z.ZodUnknown>;
207
654
  }, "strip", z.ZodTypeAny, {
208
- type: "preferences" | "memory" | "custom";
655
+ type: "custom" | "preferences" | "memory";
209
656
  namespace: string;
210
657
  adapter?: unknown;
211
658
  }, {
212
- type?: "preferences" | "memory" | "custom" | undefined;
659
+ type?: "custom" | "preferences" | "memory" | undefined;
213
660
  adapter?: unknown;
214
661
  namespace?: string | undefined;
215
662
  }>;
216
663
  declare const optionsConfigSchema: z.ZodObject<{
217
664
  refreshOnResume: z.ZodDefault<z.ZodBoolean>;
218
665
  entitlementCacheTtlMs: z.ZodDefault<z.ZodNumber>;
666
+ /**
667
+ * Extra time past an entitlement's `expiresAt` during which it still
668
+ * counts as active, by the **device** clock.
669
+ *
670
+ * Exists because a renewal is not instantaneous: a subscription that
671
+ * renews at midnight may not have a fresh receipt on the device for
672
+ * minutes or hours, and locking the user out in that window is a worse
673
+ * error than letting a lapsed subscription run slightly long. Store
674
+ * grace periods and billing retry can stretch much further — on Android
675
+ * those are server-side only, so a backendless app cannot see them at all
676
+ * and this window is the only cushion it has.
677
+ *
678
+ * Defaults to 0: no grace, which is the strict reading of `expiresAt`.
679
+ */
680
+ entitlementGraceMs: z.ZodDefault<z.ZodNumber>;
219
681
  recoverUnfinishedTransactions: z.ZodDefault<z.ZodBoolean>;
220
682
  /**
221
683
  * Cap on how many unfinished transactions recovery inspects per launch.
@@ -243,20 +705,22 @@ declare const optionsConfigSchema: z.ZodObject<{
243
705
  }, "strip", z.ZodTypeAny, {
244
706
  refreshOnResume: boolean;
245
707
  entitlementCacheTtlMs: number;
708
+ entitlementGraceMs: number;
246
709
  recoverUnfinishedTransactions: boolean;
247
710
  recoveryMaxBatch: number;
248
711
  productPriceCacheTtlMs: number;
249
- logLevel: "silent" | "error" | "warn" | "info" | "debug";
712
+ logLevel: "error" | "silent" | "warn" | "info" | "debug";
250
713
  permanentErrorCodes?: string[] | undefined;
251
714
  logger?: unknown;
252
715
  }, {
253
716
  refreshOnResume?: boolean | undefined;
254
717
  entitlementCacheTtlMs?: number | undefined;
718
+ entitlementGraceMs?: number | undefined;
255
719
  recoverUnfinishedTransactions?: boolean | undefined;
256
720
  recoveryMaxBatch?: number | undefined;
257
721
  permanentErrorCodes?: string[] | undefined;
258
722
  productPriceCacheTtlMs?: number | undefined;
259
- logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
723
+ logLevel?: "error" | "silent" | "warn" | "info" | "debug" | undefined;
260
724
  logger?: unknown;
261
725
  }>;
262
726
  declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
@@ -281,15 +745,20 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
281
745
  */
282
746
  androidPlanId: z.ZodOptional<z.ZodString>;
283
747
  }, "strip", z.ZodTypeAny, {
284
- id: string;
285
748
  type: "subscription" | "product" | "consumable";
749
+ id: string;
286
750
  androidPlanId?: string | undefined;
287
751
  }, {
288
- id: string;
289
752
  type: "subscription" | "product" | "consumable";
753
+ id: string;
290
754
  androidPlanId?: string | undefined;
291
755
  }>, "many">>;
292
- backend: z.ZodEffects<z.ZodObject<{
756
+ /**
757
+ * The consumer's backend, which calls Attesto. Optional **only** when
758
+ * `verification` is set instead — one of the two has to answer
759
+ * whether a purchase is real.
760
+ */
761
+ backend: z.ZodOptional<z.ZodEffects<z.ZodObject<{
293
762
  /**
294
763
  * Custom backend transport. If provided, all HTTP-specific fields below
295
764
  * are ignored and the library uses this object directly for backend
@@ -424,23 +893,84 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
424
893
  entitlementSchema?: unknown;
425
894
  timeoutMs?: number | undefined;
426
895
  retries?: number | undefined;
427
- }>;
896
+ }>>;
897
+ /** On-device verification, for an app with no backend. */
898
+ verification: z.ZodOptional<z.ZodObject<{
899
+ mode: z.ZodLiteral<"local">;
900
+ /**
901
+ * Maps a verified purchase to the entitlements it grants. Required: iap
902
+ * does not know what a product entitles anyone to, and in local mode
903
+ * there is no backend to ask. Type is checked at runtime via a shape
904
+ * guard, not zod — see {@link VerificationConfig}.
905
+ */
906
+ entitlements: z.ZodUnknown;
907
+ /**
908
+ * The binding every purchase must carry, as passed to
909
+ * `purchase({ appUserId })`. A purchase bound to something else is
910
+ * refused. Omit it if the app does not bind purchases.
911
+ *
912
+ * Either the id itself, or a function returning it — in which case it is
913
+ * read **at the time of each check**, so the binding follows the session.
914
+ * A fixed string is captured at `createIAP()` time and cannot track a
915
+ * login change, which in a multi-user app means every purchase after a
916
+ * switch is refused as the previous user's.
917
+ *
918
+ * The function may return `null` for "nobody is signed in right now".
919
+ * That is **not** the same as using no binding: iap refuses the purchase
920
+ * rather than dropping the check. See `LocalVerificationAdapter`.
921
+ *
922
+ * Typed via the overlay on {@link VerificationConfig}; validated in the
923
+ * schema's `superRefine`, the same way the backend's functions are.
924
+ */
925
+ appUserId: z.ZodOptional<z.ZodUnknown>;
926
+ }, "strip", z.ZodTypeAny, {
927
+ mode: "local";
928
+ entitlements?: unknown;
929
+ appUserId?: unknown;
930
+ }, {
931
+ mode: "local";
932
+ entitlements?: unknown;
933
+ appUserId?: unknown;
934
+ }>>;
935
+ /** Android-specific settings. See {@link androidConfigSchema}. */
936
+ android: z.ZodDefault<z.ZodObject<{
937
+ packageName: z.ZodOptional<z.ZodString>;
938
+ }, "strip", z.ZodTypeAny, {
939
+ packageName?: string | undefined;
940
+ }, {
941
+ packageName?: string | undefined;
942
+ }>>;
428
943
  storage: z.ZodDefault<z.ZodObject<{
429
944
  type: z.ZodDefault<z.ZodEnum<["preferences", "memory", "custom"]>>;
430
945
  namespace: z.ZodDefault<z.ZodString>;
431
946
  adapter: z.ZodOptional<z.ZodUnknown>;
432
947
  }, "strip", z.ZodTypeAny, {
433
- type: "preferences" | "memory" | "custom";
948
+ type: "custom" | "preferences" | "memory";
434
949
  namespace: string;
435
950
  adapter?: unknown;
436
951
  }, {
437
- type?: "preferences" | "memory" | "custom" | undefined;
952
+ type?: "custom" | "preferences" | "memory" | undefined;
438
953
  adapter?: unknown;
439
954
  namespace?: string | undefined;
440
955
  }>>;
441
956
  options: z.ZodDefault<z.ZodObject<{
442
957
  refreshOnResume: z.ZodDefault<z.ZodBoolean>;
443
958
  entitlementCacheTtlMs: z.ZodDefault<z.ZodNumber>;
959
+ /**
960
+ * Extra time past an entitlement's `expiresAt` during which it still
961
+ * counts as active, by the **device** clock.
962
+ *
963
+ * Exists because a renewal is not instantaneous: a subscription that
964
+ * renews at midnight may not have a fresh receipt on the device for
965
+ * minutes or hours, and locking the user out in that window is a worse
966
+ * error than letting a lapsed subscription run slightly long. Store
967
+ * grace periods and billing retry can stretch much further — on Android
968
+ * those are server-side only, so a backendless app cannot see them at all
969
+ * and this window is the only cushion it has.
970
+ *
971
+ * Defaults to 0: no grace, which is the strict reading of `expiresAt`.
972
+ */
973
+ entitlementGraceMs: z.ZodDefault<z.ZodNumber>;
444
974
  recoverUnfinishedTransactions: z.ZodDefault<z.ZodBoolean>;
445
975
  /**
446
976
  * Cap on how many unfinished transactions recovery inspects per launch.
@@ -468,62 +998,52 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
468
998
  }, "strip", z.ZodTypeAny, {
469
999
  refreshOnResume: boolean;
470
1000
  entitlementCacheTtlMs: number;
1001
+ entitlementGraceMs: number;
471
1002
  recoverUnfinishedTransactions: boolean;
472
1003
  recoveryMaxBatch: number;
473
1004
  productPriceCacheTtlMs: number;
474
- logLevel: "silent" | "error" | "warn" | "info" | "debug";
1005
+ logLevel: "error" | "silent" | "warn" | "info" | "debug";
475
1006
  permanentErrorCodes?: string[] | undefined;
476
1007
  logger?: unknown;
477
1008
  }, {
478
1009
  refreshOnResume?: boolean | undefined;
479
1010
  entitlementCacheTtlMs?: number | undefined;
1011
+ entitlementGraceMs?: number | undefined;
480
1012
  recoverUnfinishedTransactions?: boolean | undefined;
481
1013
  recoveryMaxBatch?: number | undefined;
482
1014
  permanentErrorCodes?: string[] | undefined;
483
1015
  productPriceCacheTtlMs?: number | undefined;
484
- logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
1016
+ logLevel?: "error" | "silent" | "warn" | "info" | "debug" | undefined;
485
1017
  logger?: unknown;
486
1018
  }>>;
487
1019
  }, "strip", z.ZodTypeAny, {
488
1020
  options: {
489
1021
  refreshOnResume: boolean;
490
1022
  entitlementCacheTtlMs: number;
1023
+ entitlementGraceMs: number;
491
1024
  recoverUnfinishedTransactions: boolean;
492
1025
  recoveryMaxBatch: number;
493
1026
  productPriceCacheTtlMs: number;
494
- logLevel: "silent" | "error" | "warn" | "info" | "debug";
1027
+ logLevel: "error" | "silent" | "warn" | "info" | "debug";
495
1028
  permanentErrorCodes?: string[] | undefined;
496
1029
  logger?: unknown;
497
1030
  };
498
- backend: {
499
- timeoutMs: number;
500
- retries: number;
501
- adapter?: unknown;
502
- baseUrl?: string | undefined;
503
- endpoints?: {
504
- entitlements: string;
505
- restore: string;
506
- verifyApple?: string | undefined;
507
- verifyGoogle?: string | undefined;
508
- products?: string | undefined;
509
- } | undefined;
510
- getAuthHeaders?: unknown;
511
- requestTransform?: unknown;
512
- responseTransform?: unknown;
513
- entitlementSchema?: unknown;
1031
+ android: {
1032
+ packageName?: string | undefined;
514
1033
  };
515
1034
  storage: {
516
- type: "preferences" | "memory" | "custom";
1035
+ type: "custom" | "preferences" | "memory";
517
1036
  namespace: string;
518
1037
  adapter?: unknown;
519
1038
  };
520
1039
  products?: {
521
- id: string;
522
1040
  type: "subscription" | "product" | "consumable";
1041
+ id: string;
523
1042
  androidPlanId?: string | undefined;
524
1043
  }[] | undefined;
525
- }, {
526
- backend: {
1044
+ backend?: {
1045
+ timeoutMs: number;
1046
+ retries: number;
527
1047
  adapter?: unknown;
528
1048
  baseUrl?: string | undefined;
529
1049
  endpoints?: {
@@ -537,43 +1057,30 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
537
1057
  requestTransform?: unknown;
538
1058
  responseTransform?: unknown;
539
1059
  entitlementSchema?: unknown;
540
- timeoutMs?: number | undefined;
541
- retries?: number | undefined;
542
- };
1060
+ } | undefined;
1061
+ verification?: {
1062
+ mode: "local";
1063
+ entitlements?: unknown;
1064
+ appUserId?: unknown;
1065
+ } | undefined;
1066
+ }, {
543
1067
  options?: {
544
1068
  refreshOnResume?: boolean | undefined;
545
1069
  entitlementCacheTtlMs?: number | undefined;
1070
+ entitlementGraceMs?: number | undefined;
546
1071
  recoverUnfinishedTransactions?: boolean | undefined;
547
1072
  recoveryMaxBatch?: number | undefined;
548
1073
  permanentErrorCodes?: string[] | undefined;
549
1074
  productPriceCacheTtlMs?: number | undefined;
550
- logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
1075
+ logLevel?: "error" | "silent" | "warn" | "info" | "debug" | undefined;
551
1076
  logger?: unknown;
552
1077
  } | undefined;
553
1078
  products?: {
554
- id: string;
555
1079
  type: "subscription" | "product" | "consumable";
1080
+ id: string;
556
1081
  androidPlanId?: string | undefined;
557
1082
  }[] | undefined;
558
- storage?: {
559
- type?: "preferences" | "memory" | "custom" | undefined;
560
- adapter?: unknown;
561
- namespace?: string | undefined;
562
- } | undefined;
563
- }>, {
564
- options: {
565
- refreshOnResume: boolean;
566
- entitlementCacheTtlMs: number;
567
- recoverUnfinishedTransactions: boolean;
568
- recoveryMaxBatch: number;
569
- productPriceCacheTtlMs: number;
570
- logLevel: "silent" | "error" | "warn" | "info" | "debug";
571
- permanentErrorCodes?: string[] | undefined;
572
- logger?: unknown;
573
- };
574
- backend: {
575
- timeoutMs: number;
576
- retries: number;
1083
+ backend?: {
577
1084
  adapter?: unknown;
578
1085
  baseUrl?: string | undefined;
579
1086
  endpoints?: {
@@ -587,19 +1094,50 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
587
1094
  requestTransform?: unknown;
588
1095
  responseTransform?: unknown;
589
1096
  entitlementSchema?: unknown;
1097
+ timeoutMs?: number | undefined;
1098
+ retries?: number | undefined;
1099
+ } | undefined;
1100
+ verification?: {
1101
+ mode: "local";
1102
+ entitlements?: unknown;
1103
+ appUserId?: unknown;
1104
+ } | undefined;
1105
+ android?: {
1106
+ packageName?: string | undefined;
1107
+ } | undefined;
1108
+ storage?: {
1109
+ type?: "custom" | "preferences" | "memory" | undefined;
1110
+ adapter?: unknown;
1111
+ namespace?: string | undefined;
1112
+ } | undefined;
1113
+ }>, {
1114
+ options: {
1115
+ refreshOnResume: boolean;
1116
+ entitlementCacheTtlMs: number;
1117
+ entitlementGraceMs: number;
1118
+ recoverUnfinishedTransactions: boolean;
1119
+ recoveryMaxBatch: number;
1120
+ productPriceCacheTtlMs: number;
1121
+ logLevel: "error" | "silent" | "warn" | "info" | "debug";
1122
+ permanentErrorCodes?: string[] | undefined;
1123
+ logger?: unknown;
1124
+ };
1125
+ android: {
1126
+ packageName?: string | undefined;
590
1127
  };
591
1128
  storage: {
592
- type: "preferences" | "memory" | "custom";
1129
+ type: "custom" | "preferences" | "memory";
593
1130
  namespace: string;
594
1131
  adapter?: unknown;
595
1132
  };
596
1133
  products?: {
597
- id: string;
598
1134
  type: "subscription" | "product" | "consumable";
1135
+ id: string;
599
1136
  androidPlanId?: string | undefined;
600
1137
  }[] | undefined;
601
- }, {
602
- backend: {
1138
+ backend?: {
1139
+ timeoutMs: number;
1140
+ retries: number;
603
1141
  adapter?: unknown;
604
1142
  baseUrl?: string | undefined;
605
1143
  endpoints?: {
@@ -613,26 +1151,56 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
613
1151
  requestTransform?: unknown;
614
1152
  responseTransform?: unknown;
615
1153
  entitlementSchema?: unknown;
616
- timeoutMs?: number | undefined;
617
- retries?: number | undefined;
618
- };
1154
+ } | undefined;
1155
+ verification?: {
1156
+ mode: "local";
1157
+ entitlements?: unknown;
1158
+ appUserId?: unknown;
1159
+ } | undefined;
1160
+ }, {
619
1161
  options?: {
620
1162
  refreshOnResume?: boolean | undefined;
621
1163
  entitlementCacheTtlMs?: number | undefined;
1164
+ entitlementGraceMs?: number | undefined;
622
1165
  recoverUnfinishedTransactions?: boolean | undefined;
623
1166
  recoveryMaxBatch?: number | undefined;
624
1167
  permanentErrorCodes?: string[] | undefined;
625
1168
  productPriceCacheTtlMs?: number | undefined;
626
- logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
1169
+ logLevel?: "error" | "silent" | "warn" | "info" | "debug" | undefined;
627
1170
  logger?: unknown;
628
1171
  } | undefined;
629
1172
  products?: {
630
- id: string;
631
1173
  type: "subscription" | "product" | "consumable";
1174
+ id: string;
632
1175
  androidPlanId?: string | undefined;
633
1176
  }[] | undefined;
1177
+ backend?: {
1178
+ adapter?: unknown;
1179
+ baseUrl?: string | undefined;
1180
+ endpoints?: {
1181
+ entitlements: string;
1182
+ restore: string;
1183
+ verifyApple?: string | undefined;
1184
+ verifyGoogle?: string | undefined;
1185
+ products?: string | undefined;
1186
+ } | undefined;
1187
+ getAuthHeaders?: unknown;
1188
+ requestTransform?: unknown;
1189
+ responseTransform?: unknown;
1190
+ entitlementSchema?: unknown;
1191
+ timeoutMs?: number | undefined;
1192
+ retries?: number | undefined;
1193
+ } | undefined;
1194
+ verification?: {
1195
+ mode: "local";
1196
+ entitlements?: unknown;
1197
+ appUserId?: unknown;
1198
+ } | undefined;
1199
+ android?: {
1200
+ packageName?: string | undefined;
1201
+ } | undefined;
634
1202
  storage?: {
635
- type?: "preferences" | "memory" | "custom" | undefined;
1203
+ type?: "custom" | "preferences" | "memory" | undefined;
636
1204
  adapter?: unknown;
637
1205
  namespace?: string | undefined;
638
1206
  } | undefined;
@@ -659,37 +1227,34 @@ type BackendConfig = Omit<RawBackendConfig, 'getAuthHeaders' | 'requestTransform
659
1227
  type BackendConfigInput = Omit<RawBackendConfigInput, 'getAuthHeaders' | 'requestTransform' | 'responseTransform'> & FunctionTypedBackendOverlay;
660
1228
  type RawIAPConfig = z.infer<typeof iapConfigSchema>;
661
1229
  type RawIAPConfigInput = z.input<typeof iapConfigSchema>;
662
- type IAPConfig = Omit<RawIAPConfig, 'backend'> & {
663
- backend: BackendConfig;
1230
+ /**
1231
+ * Replace the schema's `unknown` typing for the entitlement mapper with its
1232
+ * real contract, the same way {@link BackendConfig} does for its functions.
1233
+ *
1234
+ * Generic over the consumer's entitlement shape. `LocallyVerifiedPurchase`
1235
+ * carries what a mapper needs to decide: the product, the expiry, the
1236
+ * binding, and how strongly the purchase was checked.
1237
+ */
1238
+ type VerificationConfig<TEntitlement extends EntitlementBase = EntitlementBase> = Omit<z.infer<typeof verificationConfigSchema>, 'entitlements' | 'appUserId'> & {
1239
+ entitlements: (purchase: LocallyVerifiedPurchase) => TEntitlement[];
1240
+ /**
1241
+ * The id, or a getter so the binding can follow the session. A getter
1242
+ * returning `null` means "nobody signed in", and refuses rather than
1243
+ * skipping the check.
1244
+ */
1245
+ appUserId?: string | (() => string | null);
664
1246
  };
665
- type IAPConfigInput = Omit<RawIAPConfigInput, 'backend'> & {
666
- backend: BackendConfigInput;
1247
+ type IAPConfig<TEntitlement extends EntitlementBase = EntitlementBase> = Omit<RawIAPConfig, 'backend' | 'verification'> & {
1248
+ backend?: BackendConfig;
1249
+ verification?: VerificationConfig<TEntitlement>;
1250
+ };
1251
+ type IAPConfigInput<TEntitlement extends EntitlementBase = EntitlementBase> = Omit<RawIAPConfigInput, 'backend' | 'verification'> & {
1252
+ backend?: BackendConfigInput;
1253
+ verification?: VerificationConfig<TEntitlement>;
667
1254
  };
668
1255
  type StorageConfig = z.infer<typeof storageConfigSchema>;
669
1256
  type OptionsConfig = z.infer<typeof optionsConfigSchema>;
670
1257
 
671
- declare const entitlementBaseSchema: z.ZodObject<{
672
- key: z.ZodString;
673
- productId: z.ZodString;
674
- expiresAt: z.ZodNullable<z.ZodString>;
675
- }, "strip", z.ZodTypeAny, {
676
- key: string;
677
- productId: string;
678
- expiresAt: string | null;
679
- }, {
680
- key: string;
681
- productId: string;
682
- expiresAt: string | null;
683
- }>;
684
- /**
685
- * Minimum shape every entitlement returned by the backend must satisfy.
686
- * Consumer-defined fields pass through unvalidated unless the consumer
687
- * supplies their own schema via `config.backend.entitlementSchema`.
688
- */
689
- type EntitlementBase = z.infer<typeof entitlementBaseSchema>;
690
- /** Default shape used when no `TEntitlement` type parameter is given. */
691
- type DefaultEntitlement = EntitlementBase;
692
-
693
1258
  declare const IAPErrorCode: {
694
1259
  readonly INVALID_CONFIG: "INVALID_CONFIG";
695
1260
  readonly NOT_INITIALIZED: "NOT_INITIALIZED";
@@ -713,8 +1278,9 @@ declare const IAPErrorCode: {
713
1278
  readonly STORAGE_ERROR: "STORAGE_ERROR";
714
1279
  /**
715
1280
  * The supplied `appUserId` (literal or fetcher-returned) is not a valid
716
- * UUID v4. Apple requires a UUID for `appAccountToken`; we enforce the
717
- * same constraint cross-platform for a consistent contract.
1281
+ * RFC 9562 UUID. Apple requires a UUID for `appAccountToken`; we enforce
1282
+ * the same constraint cross-platform for a consistent contract. Any
1283
+ * version is accepted — see `isValidUuid` for what is still refused.
718
1284
  */
719
1285
  readonly INVALID_APP_USER_ID: "INVALID_APP_USER_ID";
720
1286
  /**
@@ -747,126 +1313,45 @@ declare class IAPError extends Error {
747
1313
  }
748
1314
  declare function isIAPError(error: unknown): error is IAPError;
749
1315
 
750
- type ProductType = 'subscription' | 'product' | 'consumable';
751
- interface ConfiguredProduct {
752
- id: string;
753
- type: ProductType;
754
- /**
755
- * Required for Android subscriptions — the base plan identifier configured in
756
- * Google Play Console. Ignored on iOS and for non-subscription products.
757
- */
758
- androidPlanId?: string;
759
- }
760
- /**
761
- * Native product info merged with configured metadata.
762
- * Library-public shape — kept stable across Capacitor versions.
763
- */
764
- interface Product {
765
- id: string;
766
- type: ProductType;
767
- title: string;
768
- description: string;
769
- /** Localized price string, e.g. "$4.99". Always render this — never hardcode. */
770
- priceString: string;
771
- /** BigInt as string to avoid precision loss, e.g. "4990000". */
772
- priceMicros: string;
773
- /** ISO 4217 code, e.g. "USD". */
774
- currency: string;
775
- }
776
-
777
- /** Platform a transaction originated from. */
778
- type Platform = 'apple' | 'google';
779
1316
  /**
780
- * Normalized native transaction handed to core flows.
781
- * Plugin-version differences are translated by the adapter so this shape
782
- * stays stable across Capacitor majors.
1317
+ * The **store's** view of a purchase, as reported by StoreKit or Play
1318
+ * Billing — distinct from {@link VerifiedTransaction}, which is your
1319
+ * backend's view after Attesto validated the receipt.
1320
+ *
1321
+ * Both appear on a successful purchase because they answer different
1322
+ * questions. The backend's `transaction.id` identifies the receipt *it*
1323
+ * recorded; `purchaseReference` identifies the purchase as the store sees
1324
+ * it, which is what survives renewals and reinstalls.
783
1325
  */
784
- interface NativeTransaction {
1326
+ interface StorePurchase {
785
1327
  platform: Platform;
786
1328
  productId: string;
787
1329
  /**
788
- * Apple: the StoreKit `transactionId` (numeric string).
789
- * Google: the `purchaseToken` from Play Billing.
1330
+ * The store's stable reference to this purchase: Apple's
1331
+ * `originalTransactionId`, Google's `purchaseToken`. Unchanged across
1332
+ * renewals, so this is what to persist if you need to recognise the
1333
+ * subscription later — re-deriving a server-side token from it months
1334
+ * after the purchase, for instance.
1335
+ *
1336
+ * Recoverable after a reinstall via `restorePurchases()`, which is the
1337
+ * only route once the install that made the purchase is gone.
1338
+ *
1339
+ * Treat it as a receipt: useful for correlation, not something to leak.
790
1340
  */
791
- token: string;
792
- /** Google only — required by Attesto's `/v1/google/verify`. */
793
- packageName?: string;
794
- /** Product type the transaction was originally purchased as. */
795
- productType: ProductType;
796
- /** Optional pass-through of plugin-native fields for backend hints. */
797
- raw?: unknown;
798
- }
799
- /**
800
- * Verified transaction returned by the consumer backend after Attesto check.
801
- * Generic over the backend-defined transaction shape.
802
- */
803
- interface VerifiedTransaction<TExtra = Record<string, unknown>> {
804
- id: string;
805
- productId: string;
806
- expiresAt: string | null;
807
- verifiedAt: string;
808
- raw?: TExtra;
809
- }
810
-
811
- /**
812
- * Map of event name → payload. Used by the typed event emitter so consumer
813
- * subscriptions are statically checked.
814
- */
815
- interface EventMap<TEntitlement extends EntitlementBase = EntitlementBase> {
816
- ready: undefined;
817
- 'purchase-started': {
818
- productId: string;
819
- };
820
- 'purchase-success': {
821
- productId: string;
822
- transaction: VerifiedTransaction;
823
- };
824
- 'purchase-cancelled': {
825
- productId: string;
826
- };
827
- 'purchase-pending': {
828
- productId: string;
829
- };
830
- 'purchase-failed': {
831
- productId: string;
832
- error: IAPError;
833
- };
834
- 'verification-failed': {
835
- productId: string;
836
- error: IAPError;
837
- };
838
- 'restore-started': undefined;
839
- 'restore-completed': {
840
- restored: number;
841
- entitlements: TEntitlement[];
842
- };
843
- 'entitlements-changed': {
844
- entitlements: TEntitlement[];
845
- previous: TEntitlement[];
846
- };
1341
+ purchaseReference: string;
847
1342
  /**
848
- * Recovery classified an `unfinished_transactions` entry as permanently
849
- * invalid (per `options.permanentErrorCodes`) and removed it from
850
- * storage. Will not be retried on subsequent launches. Useful for ops
851
- * logging / alerting on stuck-loop self-heal events.
852
- *
853
- * **Token is unmasked.** Receipt tokens (Apple `transactionId` /
854
- * Google `purchaseToken`) are useful for correlation in debugging
855
- * but are receipts you don't want to leak — treat as sensitive. Mask
856
- * before forwarding to external analytics / logging services. iap's
857
- * own internal logs use a masked form (see `lib/redact.ts`).
1343
+ * The binding this purchase was made with — the value passed as
1344
+ * `purchase({ appUserId })`, read back off the transaction. `null` when
1345
+ * the purchase carried none. iap never generates one.
858
1346
  */
859
- 'recovery-dropped-permanent': {
860
- productId: string;
861
- token: string;
862
- error: string;
863
- message?: string;
864
- };
1347
+ appUserId: string | null;
1348
+ /**
1349
+ * Renewal detail for a subscription, or `null` when the store reported
1350
+ * none — which is always the case on Android. See
1351
+ * {@link SubscriptionRenewal}.
1352
+ */
1353
+ subscription: SubscriptionRenewal | null;
865
1354
  }
866
- type EventName<TEntitlement extends EntitlementBase = EntitlementBase> = keyof EventMap<TEntitlement>;
867
- type EventPayload<K extends EventName<TEntitlement>, TEntitlement extends EntitlementBase = EntitlementBase> = EventMap<TEntitlement>[K];
868
- type Unsubscribe = () => void;
869
-
870
1355
  /**
871
1356
  * Convenience context the library passes to a function-form
872
1357
  * {@link AppUserId} fetcher. `authHeaders` is the result of awaiting
@@ -886,7 +1371,7 @@ interface AppUserIdFetcherContext {
886
1371
  authHeaders: Record<string, string>;
887
1372
  }
888
1373
  /**
889
- * Value supplied to `iap.purchase({ appUserId })`. Either a UUID v4
1374
+ * Value supplied to `iap.purchase({ appUserId })`. Either a UUID
890
1375
  * string the caller already has (e.g. from local cache / app state) or
891
1376
  * an async fetcher the library invokes once per purchase to retrieve
892
1377
  * the UUID from the caller's backend (typical pattern: backend mints +
@@ -902,10 +1387,12 @@ interface AppUserIdFetcherContext {
902
1387
  * requests can be reused without redefining a helper. See
903
1388
  * {@link AppUserIdFetcherContext}.
904
1389
  *
905
- * Either form is validated as UUID v4 before being forwarded to
906
- * StoreKit's `appAccountToken` (iOS) / Play Billing's
907
- * `obfuscatedAccountId` (Android). Non-UUID values throw
908
- * `IAPError(INVALID_APP_USER_ID)`. A throwing/rejecting fetcher
1390
+ * Either form is validated as an RFC 9562 UUID — **any version**, so a
1391
+ * deterministic UUIDv5 over a stable name works as well as a random v4 —
1392
+ * before being forwarded to StoreKit's `appAccountToken` (iOS) / Play
1393
+ * Billing's `obfuscatedAccountId` (Android). Non-UUID values throw
1394
+ * `IAPError(INVALID_APP_USER_ID)`, as do the nil and max UUIDs, which
1395
+ * carry no identity. A throwing/rejecting fetcher
909
1396
  * surfaces as `IAPError(APP_USER_ID_FETCH_FAILED, cause: <original>)`.
910
1397
  */
911
1398
  type AppUserId = string | (() => Promise<string>) | ((ctx: AppUserIdFetcherContext) => Promise<string>);
@@ -928,7 +1415,10 @@ interface PurchaseOptions {
928
1415
  type PurchaseResult<TEntitlement extends EntitlementBase = EntitlementBase> = {
929
1416
  status: 'success';
930
1417
  productId: string;
1418
+ /** Your backend's view of the receipt, after Attesto validated it. */
931
1419
  transaction: VerifiedTransaction;
1420
+ /** The store's own view of the purchase. See {@link StorePurchase}. */
1421
+ purchase: StorePurchase;
932
1422
  entitlements: TEntitlement[];
933
1423
  } | {
934
1424
  status: 'cancelled';
@@ -948,7 +1438,114 @@ type PurchaseResult<TEntitlement extends EntitlementBase = EntitlementBase> = {
948
1438
  interface RestoreResult<TEntitlement extends EntitlementBase = EntitlementBase> {
949
1439
  restored: number;
950
1440
  entitlements: TEntitlement[];
1441
+ /**
1442
+ * The store's view of every transaction the account currently owns — one
1443
+ * entry per transaction submitted, in the order the store reported them.
1444
+ * Empty when the store reports nothing owned.
1445
+ *
1446
+ * This is the route to a {@link StorePurchase.purchaseReference} after a
1447
+ * reinstall or on a new device, where the install that made the purchase
1448
+ * no longer exists to have kept it.
1449
+ */
1450
+ purchases: StorePurchase[];
1451
+ }
1452
+
1453
+ /**
1454
+ * Map of event name → payload. Used by the typed event emitter so consumer
1455
+ * subscriptions are statically checked.
1456
+ */
1457
+ interface EventMap<TEntitlement extends EntitlementBase = EntitlementBase> {
1458
+ ready: undefined;
1459
+ 'purchase-started': {
1460
+ productId: string;
1461
+ };
1462
+ 'purchase-success': {
1463
+ productId: string;
1464
+ transaction: VerifiedTransaction;
1465
+ };
1466
+ 'purchase-cancelled': {
1467
+ productId: string;
1468
+ };
1469
+ 'purchase-pending': {
1470
+ productId: string;
1471
+ };
1472
+ /**
1473
+ * A purchase previously reported as `purchase-pending` has resolved.
1474
+ *
1475
+ * `purchased` means it cleared and the entitlement is in place (the
1476
+ * transaction has been verified and acknowledged by the time this fires).
1477
+ * `cancelled` means the store no longer has it, either waiting or owned —
1478
+ * the payment failed, expired, or the user abandoned it.
1479
+ *
1480
+ * **`cancelled` only fires where the platform can distinguish "gone" from
1481
+ * "still waiting"** — Android, via Play's purchase state. On iOS a
1482
+ * deferred purchase that is never approved produces no signal at all, so
1483
+ * iap says nothing rather than guessing; the pending mark simply stays
1484
+ * until a transaction arrives.
1485
+ *
1486
+ * Fires at most once per pending purchase. Use it to clear
1487
+ * pending-state copy a paywall put up on `purchase-pending`.
1488
+ */
1489
+ 'purchase-pending-resolved': {
1490
+ productId: string;
1491
+ status: 'purchased' | 'cancelled';
1492
+ };
1493
+ /**
1494
+ * A transaction arrived outside a `purchase()` call — a renewal, a refund
1495
+ * or revocation, an Ask-to-Buy approval, a pending purchase clearing.
1496
+ *
1497
+ * Fires before iap has verified it, so treat the payload as "the store
1498
+ * says something happened", not as an entitlement change. The
1499
+ * authoritative signal is `entitlements-changed`, which follows if the
1500
+ * backend confirms it.
1501
+ *
1502
+ * iOS only in practice: the native layer feeds this from Apple's
1503
+ * `Transaction.updates` and has no Android equivalent, where the same
1504
+ * ground is covered by reconciliation at launch and on resume.
1505
+ */
1506
+ 'transaction-updated': {
1507
+ productId: string;
1508
+ purchase: StorePurchase;
1509
+ };
1510
+ 'purchase-failed': {
1511
+ productId: string;
1512
+ error: IAPError;
1513
+ };
1514
+ 'verification-failed': {
1515
+ productId: string;
1516
+ error: IAPError;
1517
+ };
1518
+ 'restore-started': undefined;
1519
+ 'restore-completed': {
1520
+ restored: number;
1521
+ entitlements: TEntitlement[];
1522
+ };
1523
+ 'entitlements-changed': {
1524
+ entitlements: TEntitlement[];
1525
+ previous: TEntitlement[];
1526
+ };
1527
+ /**
1528
+ * Recovery classified an `unfinished_transactions` entry as permanently
1529
+ * invalid (per `options.permanentErrorCodes`) and removed it from
1530
+ * storage. Will not be retried on subsequent launches. Useful for ops
1531
+ * logging / alerting on stuck-loop self-heal events.
1532
+ *
1533
+ * **Token is unmasked.** Receipt tokens (Apple `transactionId` /
1534
+ * Google `purchaseToken`) are useful for correlation in debugging
1535
+ * but are receipts you don't want to leak — treat as sensitive. Mask
1536
+ * before forwarding to external analytics / logging services. iap's
1537
+ * own internal logs use a masked form (see `lib/redact.ts`).
1538
+ */
1539
+ 'recovery-dropped-permanent': {
1540
+ productId: string;
1541
+ token: string;
1542
+ error: string;
1543
+ message?: string;
1544
+ };
951
1545
  }
1546
+ type EventName<TEntitlement extends EntitlementBase = EntitlementBase> = keyof EventMap<TEntitlement>;
1547
+ type EventPayload<K extends EventName<TEntitlement>, TEntitlement extends EntitlementBase = EntitlementBase> = EventMap<TEntitlement>[K];
1548
+ type Unsubscribe = () => void;
952
1549
 
953
1550
  /**
954
1551
  * The user's App Store / Google Play storefront — i.e. the country their
@@ -1020,8 +1617,9 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
1020
1617
  * can render the right UI without try/catch gymnastics.
1021
1618
  *
1022
1619
  * `opts.appUserId` is optional. When provided (string or async fetcher
1023
- * returning a string), the resolved value is validated as a UUID v4
1024
- * and forwarded to StoreKit's `appAccountToken` (iOS) / Play
1620
+ * returning a string), the resolved value is validated as an RFC 9562
1621
+ * UUID of any version and forwarded to StoreKit's `appAccountToken`
1622
+ * (iOS) / Play
1025
1623
  * Billing's `obfuscatedAccountId` (Android) — making it available on
1026
1624
  * Attesto's verify response and outbound webhook payload as
1027
1625
  * `appUserId` so backends can join on user identity directly. See
@@ -1082,14 +1680,64 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
1082
1680
  * signed storefront from your backend instead.
1083
1681
  */
1084
1682
  getStorefront(): Promise<Storefront | null>;
1683
+ /**
1684
+ * Whether an **active** entitlement with this key is held.
1685
+ *
1686
+ * Active means its `expiresAt` has not passed by the device clock, plus
1687
+ * any `options.entitlementGraceMs`. An entitlement is never dropped
1688
+ * because a refresh failed or the cache is old — only the store's own
1689
+ * deadline, or an authoritative negative, takes one away.
1690
+ */
1085
1691
  hasEntitlement(key: string): boolean;
1086
- /** Returns a defensive shallow copy. Each entitlement is frozen. */
1692
+ /**
1693
+ * The active entitlements, as a defensive shallow copy. Each is frozen.
1694
+ * Expired ones are omitted — see {@link IAP.hasEntitlement}.
1695
+ */
1087
1696
  getEntitlements(): TEntitlement[];
1088
- /** Returns a frozen entitlement reference, or null if missing. */
1697
+ /** A frozen active entitlement, or null if missing or expired. */
1089
1698
  getEntitlement(key: string): TEntitlement | null;
1699
+ /**
1700
+ * When the entitlements were last confirmed by the verification source —
1701
+ * your backend, or the device in local mode. ISO 8601, or `null` if they
1702
+ * have never been confirmed on this install.
1703
+ *
1704
+ * Exposed so an app can decide for itself how much staleness it will
1705
+ * tolerate. iap deliberately does not revoke on staleness: a cached
1706
+ * entitlement stands until it expires, because a network error must never
1707
+ * cost a paying user their access.
1708
+ */
1709
+ getEntitlementsVerifiedAt(): string | null;
1710
+ /**
1711
+ * Erase everything iap has stored on this device: the entitlement cache,
1712
+ * the unfinished-transaction queue, and any pending-purchase marks.
1713
+ *
1714
+ * For a "delete everything" control. `destroy()` deliberately leaves the
1715
+ * cache in place (a new instance should see the user's entitlements
1716
+ * immediately); this is the opposite, and does not tear the instance down —
1717
+ * the instance stays usable and a `refresh()` will repopulate from the
1718
+ * verification source.
1719
+ *
1720
+ * Best-effort on storage: a failure to clear one key is logged and the
1721
+ * rest is still cleared. Emits `entitlements-changed` with an empty list,
1722
+ * so a reactive UI follows.
1723
+ *
1724
+ * Throws `ALREADY_IN_PROGRESS` if a `purchase()` is in flight. That
1725
+ * purchase would re-populate the state and re-persist the cache moments
1726
+ * after the wipe, and leave its unfinished entry behind, so the erase
1727
+ * would look like it had silently failed. Await the purchase, then reset.
1728
+ *
1729
+ * A no-op after `destroy()`, rather than an error: a teardown racing a
1730
+ * "delete my data" tap is a real sequence, not a programming mistake.
1731
+ *
1732
+ * It does **not** revoke the purchase. The store still owns it, and a
1733
+ * restore will bring the entitlement back — which is correct: the user
1734
+ * paid.
1735
+ */
1736
+ reset(): Promise<void>;
1090
1737
  on<K extends EventName<TEntitlement>>(event: K, handler: (payload: EventPayload<K, TEntitlement>) => void): Unsubscribe;
1091
1738
  }
1092
- declare function createIAP<TEntitlement extends EntitlementBase = EntitlementBase>(input: IAPConfigInput): IAP<TEntitlement>;
1739
+
1740
+ declare function createIAP<TEntitlement extends EntitlementBase = EntitlementBase>(input: IAPConfigInput<TEntitlement>): IAP<TEntitlement>;
1093
1741
 
1094
1742
  interface VerifyAppleRequest {
1095
1743
  productId: string;
@@ -1143,6 +1791,20 @@ type VerifyResponse<TEntitlement extends EntitlementBase = EntitlementBase> = {
1143
1791
  type RestoreResponse<TEntitlement extends EntitlementBase = EntitlementBase> = {
1144
1792
  valid: true;
1145
1793
  entitlements: TEntitlement[];
1794
+ /**
1795
+ * Optional: the tokens that were actually verified, when the
1796
+ * adapter judged them individually.
1797
+ *
1798
+ * When present, only these are acknowledged. It exists because a
1799
+ * single consolidated `valid: true` cannot say *which* of the
1800
+ * submitted transactions passed — and acknowledging one that did
1801
+ * not finalises a purchase the adapter just refused, waiving
1802
+ * Play's refund window for it.
1803
+ *
1804
+ * A backend that validates the batch as a whole omits it, and
1805
+ * every submitted transaction is acknowledged as before.
1806
+ */
1807
+ verifiedTokens?: string[];
1146
1808
  } | {
1147
1809
  valid: false;
1148
1810
  error: string;
@@ -1186,6 +1848,32 @@ interface BackendAdapter<TEntitlement extends EntitlementBase = EntitlementBase>
1186
1848
  * not a registration.
1187
1849
  */
1188
1850
  listProducts?(): Promise<ConfiguredProduct[]>;
1851
+ /**
1852
+ * Optional: verify a transaction **without** extracting it into the HTTP
1853
+ * request shapes.
1854
+ *
1855
+ * `VerifyAppleRequest` and `VerifyGoogleRequest` carry what a server needs
1856
+ * to look a receipt up — a product id and a token. An on-device check
1857
+ * needs more than that: the binding, the subscription state, the expiry.
1858
+ * Rather than widen the HTTP contract (and so change what gets sent over
1859
+ * the wire), an adapter that can answer from the transaction itself
1860
+ * implements this, and the router prefers it.
1861
+ *
1862
+ * Implemented by the built-in local-verification adapter. The HTTP path
1863
+ * does not implement it and is unaffected.
1864
+ */
1865
+ verifyNative?(transaction: NativeTransaction): Promise<VerifyResponse<TEntitlement>>;
1866
+ /**
1867
+ * Optional counterpart to {@link verifyNative} for restore: the owned
1868
+ * transactions, unextracted.
1869
+ *
1870
+ * Avoids two problems for an adapter that answers from the transaction.
1871
+ * It needs the fields `RestoreRequestTransaction` drops; and building
1872
+ * that request requires a `packageName` on every Google transaction,
1873
+ * which such an adapter never reads — so a request it would discard
1874
+ * could fail the restore outright.
1875
+ */
1876
+ restoreNative?(owned: NativeTransaction[]): Promise<RestoreResponse<TEntitlement>>;
1189
1877
  }
1190
1878
 
1191
1879
  /**
@@ -1270,4 +1958,4 @@ declare class HttpBackendAdapter<TEntitlement extends EntitlementBase = Entitlem
1270
1958
  listProducts(): Promise<ConfiguredProduct[]>;
1271
1959
  }
1272
1960
 
1273
- export { type AppUserId, type AppUserIdFetcherContext, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, DEFAULT_PERMANENT_ERROR_CODES, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type Storefront, type Unsubscribe, VERSION, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isIAPError };
1961
+ export { type AppUserId, type AppUserIdFetcherContext, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, DEFAULT_PERMANENT_ERROR_CODES, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LocalVerification, type LocalVerificationError, type LocalVerificationOptions, type LocallyVerifiedPurchase, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseAssurance, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type StorePurchase, type Storefront, type SubscriptionOffer, type SubscriptionOfferPayment, type SubscriptionOfferType, type SubscriptionRenewal, type SubscriptionState, type Unsubscribe, VERSION, type VerificationConfig, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isExpired, isIAPError, verifyPurchaseOnDevice };