expo-iap 6.4.0 → 6.4.2
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/AGENTS.md +4 -2
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -2
- package/build/index.js.map +1 -1
- package/build/index.kepler.d.ts +1 -0
- package/build/index.kepler.d.ts.map +1 -1
- package/build/index.kepler.js +1 -0
- package/build/index.kepler.js.map +1 -1
- package/build/purchase-flow-core.d.ts +4 -0
- package/build/purchase-flow-core.d.ts.map +1 -1
- package/build/purchase-flow-core.js +33 -2
- package/build/purchase-flow-core.js.map +1 -1
- package/bun.lock +2 -1
- package/ios/ExpoIap.podspec +16 -4
- package/openiap-versions.json +3 -3
- package/osv-scanner.toml +10 -5
- package/package.json +3 -2
- package/plugin/build/withIAP.js +8 -7
- package/plugin/build/withLocalOpenIAP.d.ts +4 -2
- package/plugin/build/withLocalOpenIAP.js +40 -49
- package/plugin/src/withIAP.ts +13 -9
- package/plugin/src/withLocalOpenIAP.ts +47 -63
- package/scripts/verify-ios-swiftpm-consumer.sh +42 -0
- package/src/index.kepler.ts +1 -0
- package/src/index.ts +4 -1
- package/src/purchase-flow-core.ts +47 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purchase-flow-core.d.ts","sourceRoot":"","sources":["../src/purchase-flow-core.ts"],"names":[],"mappings":"AAQA,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC;;;OAGG;IACH,4BAA4B,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,wFAAwF;IACxF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"purchase-flow-core.d.ts","sourceRoot":"","sources":["../src/purchase-flow-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -178,6 +178,7 @@ export function createPurchaseFlow(options) {
|
|
|
178
178
|
}
|
|
179
179
|
async function recover(includeFinished) {
|
|
180
180
|
const { getPendingTransactionsIOS } = options;
|
|
181
|
+
const settledBefore = new Set(settled.keys());
|
|
181
182
|
// iOS lists unfinished transactions directly; other stores list owned
|
|
182
183
|
// purchases, and isUnfinished picks the ones still to settle.
|
|
183
184
|
let pendingIOS;
|
|
@@ -197,7 +198,13 @@ export function createPurchaseFlow(options) {
|
|
|
197
198
|
available.some((purchase) => purchase.store === "apple")) {
|
|
198
199
|
throw new Error("recoverPurchases needs getPendingTransactionsIOS on iOS to find unfinished transactions");
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
+
// A restore confirms each Apple purchase once, through its newest
|
|
202
|
+
// transaction: older renewals add nothing, and the unfinished ones come
|
|
203
|
+
// from the pending list. Without that list, every transaction stands in.
|
|
204
|
+
const owned = includeFinished && pendingIOS
|
|
205
|
+
? newestAppleTransactions(available)
|
|
206
|
+
: available;
|
|
207
|
+
const unfinished = owned.filter((purchase) => includeFinished ||
|
|
201
208
|
(purchase.store === "apple"
|
|
202
209
|
? pendingIOSFailed
|
|
203
210
|
: isUnfinished(purchase)));
|
|
@@ -213,7 +220,9 @@ export function createPurchaseFlow(options) {
|
|
|
213
220
|
break;
|
|
214
221
|
if (!includeFinished && settled.has(purchase.id))
|
|
215
222
|
continue;
|
|
216
|
-
|
|
223
|
+
// A restore re-confirms what settled before it, not what the listener
|
|
224
|
+
// settled while it ran.
|
|
225
|
+
const status = await deliver(purchase, false, includeFinished && settledBefore.has(purchase.id));
|
|
217
226
|
outcomes.push({ purchase, status });
|
|
218
227
|
}
|
|
219
228
|
return outcomes;
|
|
@@ -241,6 +250,28 @@ export function createPurchaseFlow(options) {
|
|
|
241
250
|
},
|
|
242
251
|
};
|
|
243
252
|
}
|
|
253
|
+
// Keeps each Apple purchase's newest transaction, in list order. One without
|
|
254
|
+
// an original id or a date cannot be ranked, so it stays.
|
|
255
|
+
function newestAppleTransactions(purchases) {
|
|
256
|
+
const chainOf = (purchase) => purchase.store === "apple" && (purchase.transactionDate ?? 0) > 0
|
|
257
|
+
? purchase.originalTransactionIdentifierIOS || undefined
|
|
258
|
+
: undefined;
|
|
259
|
+
const newest = new Map();
|
|
260
|
+
for (const purchase of purchases) {
|
|
261
|
+
const key = chainOf(purchase);
|
|
262
|
+
if (!key)
|
|
263
|
+
continue;
|
|
264
|
+
const kept = newest.get(key);
|
|
265
|
+
if (!kept ||
|
|
266
|
+
(purchase.transactionDate ?? 0) > (kept.transactionDate ?? 0)) {
|
|
267
|
+
newest.set(key, purchase);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return purchases.filter((purchase) => {
|
|
271
|
+
const key = chainOf(purchase);
|
|
272
|
+
return !key || newest.get(key) === purchase;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
244
275
|
// A settled purchase is finished, or rejected for good where the store
|
|
245
276
|
// settles it, so the same delivery need not be confirmed again.
|
|
246
277
|
function isSettled(status) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purchase-flow-core.js","sourceRoot":"","sources":["../src/purchase-flow-core.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module; apps use the SDK root or Client Protocol /purchase-flow.\n// Purchase flow shared by every OpenIAP SDK. A confirmer (the app's backend,\n// IAPKit, or any other verification service) confirms each purchase; this\n// module decides when the app may finish a transaction, and makes no network\n// calls of its own. Verification services live in separate adapters.\n// Ports in other languages run purchase-flow.vectors.json.\n// Guide: https://openiap.dev/docs/features/validation\n\n/** Published purchase shape; optional adapter evidence stays for compatibility. */\nexport interface PurchaseFlowPurchase {\n id: string;\n productId: string;\n purchaseState: string;\n purchaseToken?: string | null;\n store: string;\n storeId?: string | null;\n ids?: string[] | null;\n quantity?: number;\n isAcknowledgedAndroid?: boolean | null;\n /**\n * A requested plan change that has not applied yet: Google Play waits for\n * its payment, Amazon applies it at the next renewal.\n */\n pendingPurchaseUpdateAndroid?: { products: string[] } | null;\n /** Apple's `Sandbox` or `Production`, which a confirmer matches against its verdict. */\n environmentIOS?: string | null;\n /** The Amazon user the receipt belongs to, which Amazon RVS checks. */\n userIdAmazon?: string | null;\n revocationDateIOS?: number | null;\n isUpgradedIOS?: boolean | null;\n expirationDateIOS?: number | null;\n}\n\n/** Body the app sends its backend, which takes the user from its own session. */\nexport interface PurchaseConfirmationRequest {\n store: \"apple\" | \"google\";\n /** Apple JWS or Google Play purchase token, as the store issued it. */\n purchaseToken: string;\n /** The product the app expected, to compare with the store's answer. */\n productId: string;\n}\n\n/**\n * A confirmer's verdict on one purchase. Only `entitled` grants access;\n * `verified` and `linked` mean the purchase is valid but access is not granted\n * yet. `ownership_conflict` sets `alreadyGranted` when another account already\n * got access, which lets the app finish it. `invalid` and\n * `test_account_restricted` are settled rejections: return\n * `verification_delayed` for any answer that may still change.\n */\nexport type PurchaseConfirmation =\n | { status: \"store_pending\" }\n | { status: \"verification_delayed\"; retryAfterMs: number }\n | { status: \"ownership_conflict\"; alreadyGranted: boolean }\n | { status: \"test_account_restricted\" }\n | { status: \"invalid\"; reason?: string }\n | { status: \"verified\"; retryAfterMs?: number }\n | { status: \"linked\"; retryAfterMs?: number }\n | { status: \"entitled\"; alreadyGranted: boolean };\n\ntype Finishable = \"entitled\" | \"ownership_conflict\" | \"test_account_restricted\";\n\n/**\n * Where a purchase stands after the flow ran. `finishError` means finishing\n * failed and recovery retries it; an `entitled` status still grants access.\n * `changePending` marks an `entitled` purchase whose requested upgrade,\n * downgrade, or prepaid top-up has not applied yet (Google Play waits for its\n * payment, Amazon applies it at the next renewal): access stays on the current\n * plan. `recovered` marks an `invalid` purchase recovery found, not one the\n * user just bought.\n */\nexport type PurchaseFlowStatus =\n | Exclude<PurchaseConfirmation, { status: Finishable | \"invalid\" }>\n | (Extract<PurchaseConfirmation, { status: \"entitled\" }> & {\n finishError?: unknown;\n changePending?: true;\n })\n | (Extract<\n PurchaseConfirmation,\n { status: \"ownership_conflict\" | \"test_account_restricted\" }\n > & {\n finishError?: unknown;\n })\n | (Extract<PurchaseConfirmation, { status: \"invalid\" }> & {\n finishError?: unknown;\n recovered?: true;\n })\n | { status: \"failed\"; error: unknown };\n\nexport interface PurchaseFlowOutcome<P extends PurchaseFlowPurchase> {\n purchase: P;\n status: PurchaseFlowStatus;\n}\n\nexport interface PurchaseFlowOptions<P extends PurchaseFlowPurchase> {\n /**\n * Confirms and grants access before returning `entitled`; the flow finishes\n * the purchase immediately after. Throws or timeouts leave it unfinished\n * for recovery. An app backend or any verification service can answer here.\n */\n confirmWithBackend: (\n purchase: P,\n context: { isConsumable: boolean },\n ) => Promise<PurchaseConfirmation>;\n /**\n * Milliseconds `confirmWithBackend` may take before the purchase ends as\n * `failed`, so a hung call cannot hold it for the session. Default 30000.\n */\n confirmTimeoutMs?: number;\n finishTransaction: (args: {\n purchase: P;\n isConsumable: boolean;\n }) => Promise<unknown>;\n /**\n * From the app's product config. On Android a wrong answer consumes a\n * non-consumable or blocks rebuying a consumable.\n */\n isConsumable: (productId: string) => boolean;\n getAvailablePurchases: () => Promise<P[]>;\n /**\n * The SDK's `getPendingTransactionsIOS` on iOS, `undefined` elsewhere: off\n * iOS an SDK may answer `[]`, which would hide unacknowledged Android\n * purchases.\n * Recovery reads unfinished StoreKit transactions, consumables included,\n * from it. If it throws, recovery confirms every Apple purchase\n * `getAvailablePurchases` lists instead.\n */\n getPendingTransactionsIOS: (() => Promise<P[]>) | undefined;\n /**\n * Registers for the store's purchase deliveries; the flow processes each one\n * until `dispose`. SDKs pass their purchase listener.\n */\n listenForPurchases?: (onPurchase: (purchase: P) => void) => {\n remove: () => void;\n };\n /**\n * Whether the user's own request produced a delivered purchase. Only then\n * may an `invalid` delivery offer buying again; without this, no delivery\n * the flow listened for does.\n */\n wasRequested?: (purchase: P) => boolean;\n}\n\nexport interface RecoverPurchasesOptions {\n /** Also re-confirm purchases that were already finished, for a restore button. */\n includeFinished?: boolean;\n}\n\nexport interface PurchaseFlow<P extends PurchaseFlowPurchase> {\n /**\n * Confirms one purchase the user just bought. Finishes it once access is\n * granted to this or another account, and a settled rejection on iOS only:\n * Google Play refunds a rejected purchase left unacknowledged, while iOS\n * would redeliver it forever.\n */\n processPurchase: (purchase: P) => Promise<PurchaseFlowStatus>;\n /**\n * Runs unfinished purchases (or every owned one) through the flow, one at a\n * time; a call made while a pass runs shares it. Throws if it meets an\n * Apple purchase without `getPendingTransactionsIOS`.\n */\n recoverPurchases: (\n options?: RecoverPurchasesOptions,\n ) => Promise<PurchaseFlowOutcome<P>[]>;\n /**\n * Calls `listener` with every outcome: once per confirmation, however many\n * deliveries shared it, and once per delivery of an already settled\n * purchase. Returns the function that unsubscribes it.\n */\n subscribe: (\n listener: (outcome: PurchaseFlowOutcome<P>) => void,\n ) => () => void;\n /**\n * Stops listening and publishing, and stops a recovery pass from confirming\n * anything more; confirmations already running still finish. Call it when\n * the signed-in account changes, then create the next account's flow.\n */\n dispose: () => void;\n}\n\n/**\n * How far a store's purchase time may trail the moment the app asked for it\n * and still count as that request: device clocks run ahead. A redelivered\n * receipt is far older.\n */\nexport const PURCHASE_REQUEST_CLOCK_SKEW_MS = 600_000;\n\ninterface PurchaseRun<P> {\n task: Promise<PurchaseFlowStatus>;\n latest: P;\n requested: boolean;\n}\n\n/**\n * Single-product, single-unit purchases only. Create one flow per\n * authenticated app account. Capture that session in confirmWithBackend and\n * dispose the flow after switching accounts.\n */\nexport function createPurchaseFlow<P extends PurchaseFlowPurchase>(\n options: PurchaseFlowOptions<P>,\n): PurchaseFlow<P> {\n // A listener and a recovery pass can deliver the same transaction at once.\n const runs = new Map<string, PurchaseRun<P>>();\n // Finished purchases, and rejections the store settles on its own. A\n // recovery pass can hold a store list read before the listener settled one.\n const settled = new Map<string, PurchaseFlowStatus>();\n const recoveries = new Map<boolean, Promise<PurchaseFlowOutcome<P>[]>>();\n const listeners = new Set<(outcome: PurchaseFlowOutcome<P>) => void>();\n let disposed = false;\n // setTimeout runs a delay past 2^31 - 1 ms, Infinity included, at once.\n const confirmTimeoutMs = Math.min(\n options.confirmTimeoutMs ?? 30_000,\n 2_147_483_647,\n );\n const subscription = options.listenForPurchases?.((purchase) => {\n // A pending delivery leaves the mark for the completed one that follows.\n const requested =\n purchase.purchaseState !== \"pending\" &&\n (options.wasRequested?.(purchase) ?? false);\n void deliver(purchase, requested);\n });\n\n function publish(outcome: PurchaseFlowOutcome<P>): void {\n if (disposed) return;\n for (const listener of listeners) {\n try {\n listener(outcome);\n } catch {\n // One subscriber's error must not keep the outcome from the others.\n }\n }\n }\n\n // Marked per delivery: a shared run may have started from an older copy of\n // the purchase, before Play attached the pending change.\n function forDelivery(\n purchase: P,\n status: PurchaseFlowStatus,\n requested: boolean,\n ): PurchaseFlowStatus {\n if (status.status === \"entitled\" && purchase.pendingPurchaseUpdateAndroid) {\n return { ...status, changePending: true };\n }\n // An old expired renewal must not ask a subscriber to buy again.\n if (status.status === \"invalid\" && !requested) {\n return { ...status, recovered: true };\n }\n return status;\n }\n\n function processPurchase(purchase: P): Promise<PurchaseFlowStatus> {\n return deliver(purchase, true);\n }\n\n function deliver(\n purchase: P,\n requested: boolean,\n reconfirm = false,\n ): Promise<PurchaseFlowStatus> {\n const immediate = immediateStatus(purchase, reconfirm);\n if (immediate) {\n const status = forDelivery(purchase, immediate, requested);\n publish({ purchase, status });\n return Promise.resolve(status);\n }\n let run = runs.get(purchase.id);\n if (run) {\n run.latest = purchase;\n run.requested ||= requested;\n } else {\n const started: PurchaseRun<P> = {\n latest: purchase,\n requested,\n task: confirmAndFinish(purchase).then((status) => {\n runs.delete(purchase.id);\n if (isSettled(status)) settled.set(purchase.id, status);\n publish({\n purchase: started.latest,\n status: forDelivery(started.latest, status, started.requested),\n });\n return status;\n }),\n };\n runs.set(purchase.id, started);\n run = started;\n }\n return run.task.then((status) => forDelivery(purchase, status, requested));\n }\n\n function immediateStatus(\n purchase: P,\n reconfirm: boolean,\n ): PurchaseFlowStatus | undefined {\n try {\n assertSupportedPurchase(purchase);\n } catch (error) {\n return { status: \"failed\", error };\n }\n // Payment is not complete; the store delivers the purchase again once it is.\n if (purchase.purchaseState === \"pending\")\n return { status: \"store_pending\" };\n return reconfirm || runs.has(purchase.id)\n ? undefined\n : settled.get(purchase.id);\n }\n\n async function confirmAndFinish(purchase: P): Promise<PurchaseFlowStatus> {\n let confirmation: PurchaseConfirmation | null;\n let isConsumable: boolean;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n // Read once: finishing must consume exactly what confirmation granted.\n isConsumable = options.isConsumable(purchase.productId);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new Error(\n `confirmWithBackend did not answer within ${confirmTimeoutMs} ms`,\n ),\n );\n }, confirmTimeoutMs);\n });\n confirmation = readConfirmation(\n await Promise.race([\n options.confirmWithBackend(purchase, { isConsumable }),\n timeout,\n ]),\n );\n } catch (error) {\n return { status: \"failed\", error };\n } finally {\n clearTimeout(timer);\n }\n if (!confirmation) {\n return {\n status: \"failed\",\n error: new Error(\"Unrecognized purchase confirmation from the backend\"),\n };\n }\n // Finishing settles a purchase for good, so only a settled verdict does.\n switch (confirmation.status) {\n case \"entitled\":\n return await finish(purchase, isConsumable, confirmation);\n case \"ownership_conflict\":\n return confirmation.alreadyGranted\n ? await finish(purchase, isConsumable, confirmation)\n : confirmation;\n case \"invalid\":\n case \"test_account_restricted\":\n return purchase.store === \"apple\"\n ? await finish(purchase, isConsumable, confirmation)\n : confirmation;\n default:\n return confirmation;\n }\n }\n\n async function finish(\n purchase: P,\n isConsumable: boolean,\n confirmation: Extract<\n PurchaseConfirmation,\n { status: Finishable | \"invalid\" }\n >,\n ): Promise<PurchaseFlowStatus> {\n try {\n // Play keeps an acknowledged purchase finished; a restore need not redo it.\n if (purchase.isAcknowledgedAndroid === true && !isConsumable) {\n return confirmation;\n }\n await options.finishTransaction({ purchase, isConsumable });\n return confirmation;\n } catch (finishError) {\n return { ...confirmation, finishError };\n }\n }\n\n function recoverPurchases(\n recoverOptions: RecoverPurchasesOptions = {},\n ): Promise<PurchaseFlowOutcome<P>[]> {\n const includeFinished = recoverOptions.includeFinished ?? false;\n let pass = recoveries.get(includeFinished);\n if (!pass) {\n pass = recover(includeFinished).finally(() => {\n recoveries.delete(includeFinished);\n });\n recoveries.set(includeFinished, pass);\n }\n return pass;\n }\n\n async function recover(\n includeFinished: boolean,\n ): Promise<PurchaseFlowOutcome<P>[]> {\n const { getPendingTransactionsIOS } = options;\n // iOS lists unfinished transactions directly; other stores list owned\n // purchases, and isUnfinished picks the ones still to settle.\n let pendingIOS: P[] | undefined;\n let pendingIOSFailed = false;\n try {\n pendingIOS = await getPendingTransactionsIOS?.();\n } catch {\n // One unverified transaction fails the whole list. Re-confirming a\n // finished purchase is safe, so every owned Apple purchase stands in.\n pendingIOSFailed = true;\n }\n const available =\n includeFinished || !pendingIOS\n ? await options.getAvailablePurchases()\n : [];\n if (\n !getPendingTransactionsIOS &&\n available.some((purchase) => purchase.store === \"apple\")\n ) {\n throw new Error(\n \"recoverPurchases needs getPendingTransactionsIOS on iOS to find unfinished transactions\",\n );\n }\n const unfinished = available.filter(\n (purchase) =>\n includeFinished ||\n (purchase.store === \"apple\"\n ? pendingIOSFailed\n : isUnfinished(purchase)),\n );\n const purchases = new Map<string, P>();\n for (const purchase of [...(pendingIOS ?? []), ...unfinished]) {\n if (!purchases.has(purchase.id)) purchases.set(purchase.id, purchase);\n }\n const outcomes: PurchaseFlowOutcome<P>[] = [];\n for (const purchase of purchases.values()) {\n // The next account's flow confirms the rest with its own session.\n if (disposed) break;\n if (!includeFinished && settled.has(purchase.id)) continue;\n const status = await deliver(purchase, false, includeFinished);\n outcomes.push({ purchase, status });\n }\n return outcomes;\n }\n\n function isUnfinished(purchase: P): boolean {\n // Amazon and most community stores report no acknowledgment, and Amazon\n // approves a Kids purchase later; a listed consumable is unconsumed.\n return (\n purchase.isAcknowledgedAndroid !== true ||\n options.isConsumable(purchase.productId)\n );\n }\n\n return {\n processPurchase,\n recoverPurchases,\n subscribe(listener) {\n const entry = (outcome: PurchaseFlowOutcome<P>) => listener(outcome);\n listeners.add(entry);\n return () => {\n listeners.delete(entry);\n };\n },\n dispose() {\n disposed = true;\n listeners.clear();\n subscription?.remove();\n },\n };\n}\n\n// A settled purchase is finished, or rejected for good where the store\n// settles it, so the same delivery need not be confirmed again.\nfunction isSettled(status: PurchaseFlowStatus): boolean {\n switch (status.status) {\n case \"entitled\":\n case \"invalid\":\n case \"test_account_restricted\":\n return status.finishError === undefined;\n case \"ownership_conflict\":\n return status.alreadyGranted && status.finishError === undefined;\n default:\n return false;\n }\n}\n\n/** Builds the backend request body; throws for a purchase without Apple or Google evidence. */\nexport function toPurchaseConfirmationRequest(\n purchase: PurchaseFlowPurchase,\n): PurchaseConfirmationRequest {\n const { store, purchaseToken, productId } = purchase;\n if ((store !== \"apple\" && store !== \"google\") || !purchaseToken) {\n throw new Error(\n `Purchase ${productId} has no Apple or Google purchase token to confirm`,\n );\n }\n assertSupportedPurchase(purchase);\n return { store, purchaseToken, productId };\n}\n\nfunction assertSupportedPurchase(purchase: PurchaseFlowPurchase): void {\n if (purchase.quantity !== undefined && purchase.quantity !== 1) {\n throw new Error(\"Purchase flow supports only single-unit purchases\");\n }\n if (new Set(purchase.ids ?? []).size > 1) {\n throw new Error(\n \"Purchase flow cannot finish a multi-product purchase before every product is granted\",\n );\n }\n}\n\nexport type PurchaseFlowGuidanceKey =\n | \"payment_pending\"\n | \"change_pending\"\n | \"verification_delayed\"\n | \"owned_by_another_account\"\n | \"test_purchase_not_accepted\"\n | \"purchase_not_valid\"\n | \"access_pending\"\n | \"access_granted\"\n | \"try_again_later\";\n\n/** Neutral guidance an app maps to its own copy. */\nexport interface PurchaseFlowGuidance {\n key: PurchaseFlowGuidanceKey;\n /**\n * True only for an `invalid` purchase the user just made. False whenever the\n * user may already have paid: never ask them to buy again.\n */\n offerPurchase: boolean;\n /** Earliest time to run the purchase through the flow again. */\n retryAfterMs?: number;\n}\n\nexport function describePurchaseFlowStatus(\n status: PurchaseFlowStatus,\n): PurchaseFlowGuidance {\n switch (status.status) {\n case \"store_pending\":\n return { key: \"payment_pending\", offerPurchase: false };\n case \"verification_delayed\":\n return {\n key: \"verification_delayed\",\n offerPurchase: false,\n retryAfterMs: status.retryAfterMs,\n };\n case \"ownership_conflict\":\n return { key: \"owned_by_another_account\", offerPurchase: false };\n case \"test_account_restricted\":\n return { key: \"test_purchase_not_accepted\", offerPurchase: false };\n case \"invalid\":\n return {\n key: \"purchase_not_valid\",\n offerPurchase: status.recovered !== true,\n };\n case \"verified\":\n case \"linked\":\n return status.retryAfterMs === undefined\n ? { key: \"access_pending\", offerPurchase: false }\n : {\n key: \"access_pending\",\n offerPurchase: false,\n retryAfterMs: status.retryAfterMs,\n };\n case \"entitled\":\n return {\n key: status.changePending ? \"change_pending\" : \"access_granted\",\n offerPurchase: false,\n };\n case \"failed\":\n return { key: \"try_again_later\", offerPurchase: false };\n }\n}\n\n// Backend JSON is untrusted: anything off-contract becomes `failed`, so a\n// malformed body can never finish a transaction.\nfunction readConfirmation(value: unknown): PurchaseConfirmation | null {\n if (typeof value !== \"object\" || value === null) return null;\n const { status, retryAfterMs, alreadyGranted, reason } = value as Record<\n string,\n unknown\n >;\n const retry =\n typeof retryAfterMs === \"number\" &&\n Number.isFinite(retryAfterMs) &&\n retryAfterMs >= 0\n ? retryAfterMs\n : undefined;\n switch (status) {\n case \"store_pending\":\n case \"test_account_restricted\":\n return { status };\n case \"invalid\":\n return typeof reason === \"string\" ? { status, reason } : { status };\n case \"verification_delayed\":\n return retry === undefined ? null : { status, retryAfterMs: retry };\n case \"verified\":\n case \"linked\":\n return retry === undefined ? { status } : { status, retryAfterMs: retry };\n case \"ownership_conflict\":\n case \"entitled\":\n return typeof alreadyGranted === \"boolean\"\n ? { status, alreadyGranted }\n : null;\n default:\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"purchase-flow-core.js","sourceRoot":"","sources":["../src/purchase-flow-core.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module; apps use the SDK root or Client Protocol /purchase-flow.\n// Purchase flow shared by every OpenIAP SDK. A confirmer (the app's backend,\n// IAPKit, or any other verification service) confirms each purchase; this\n// module decides when the app may finish a transaction, and makes no network\n// calls of its own. Verification services live in separate adapters.\n// Ports in other languages run purchase-flow.vectors.json.\n// Guide: https://openiap.dev/docs/features/validation\n\n/** Published purchase shape; optional adapter evidence stays for compatibility. */\nexport interface PurchaseFlowPurchase {\n id: string;\n productId: string;\n purchaseState: string;\n purchaseToken?: string | null;\n store: string;\n storeId?: string | null;\n ids?: string[] | null;\n quantity?: number;\n isAcknowledgedAndroid?: boolean | null;\n /**\n * A requested plan change that has not applied yet: Google Play waits for\n * its payment, Amazon applies it at the next renewal.\n */\n pendingPurchaseUpdateAndroid?: { products: string[] } | null;\n /** Apple's `Sandbox` or `Production`, which a confirmer matches against its verdict. */\n environmentIOS?: string | null;\n /** The Amazon user the receipt belongs to, which Amazon RVS checks. */\n userIdAmazon?: string | null;\n revocationDateIOS?: number | null;\n isUpgradedIOS?: boolean | null;\n expirationDateIOS?: number | null;\n /** Shared by an Apple subscription's renewals, so a restore reads only the newest. */\n originalTransactionIdentifierIOS?: string | null;\n /** Milliseconds since the epoch. */\n transactionDate?: number;\n}\n\n/** Body the app sends its backend, which takes the user from its own session. */\nexport interface PurchaseConfirmationRequest {\n store: \"apple\" | \"google\";\n /** Apple JWS or Google Play purchase token, as the store issued it. */\n purchaseToken: string;\n /** The product the app expected, to compare with the store's answer. */\n productId: string;\n}\n\n/**\n * A confirmer's verdict on one purchase. Only `entitled` grants access;\n * `verified` and `linked` mean the purchase is valid but access is not granted\n * yet. `ownership_conflict` sets `alreadyGranted` when another account already\n * got access, which lets the app finish it. `invalid` and\n * `test_account_restricted` are settled rejections: return\n * `verification_delayed` for any answer that may still change.\n */\nexport type PurchaseConfirmation =\n | { status: \"store_pending\" }\n | { status: \"verification_delayed\"; retryAfterMs: number }\n | { status: \"ownership_conflict\"; alreadyGranted: boolean }\n | { status: \"test_account_restricted\" }\n | { status: \"invalid\"; reason?: string }\n | { status: \"verified\"; retryAfterMs?: number }\n | { status: \"linked\"; retryAfterMs?: number }\n | { status: \"entitled\"; alreadyGranted: boolean };\n\ntype Finishable = \"entitled\" | \"ownership_conflict\" | \"test_account_restricted\";\n\n/**\n * Where a purchase stands after the flow ran. `finishError` means finishing\n * failed and recovery retries it; an `entitled` status still grants access.\n * `changePending` marks an `entitled` purchase whose requested upgrade,\n * downgrade, or prepaid top-up has not applied yet (Google Play waits for its\n * payment, Amazon applies it at the next renewal): access stays on the current\n * plan. `recovered` marks an `invalid` purchase recovery found, not one the\n * user just bought.\n */\nexport type PurchaseFlowStatus =\n | Exclude<PurchaseConfirmation, { status: Finishable | \"invalid\" }>\n | (Extract<PurchaseConfirmation, { status: \"entitled\" }> & {\n finishError?: unknown;\n changePending?: true;\n })\n | (Extract<\n PurchaseConfirmation,\n { status: \"ownership_conflict\" | \"test_account_restricted\" }\n > & {\n finishError?: unknown;\n })\n | (Extract<PurchaseConfirmation, { status: \"invalid\" }> & {\n finishError?: unknown;\n recovered?: true;\n })\n | { status: \"failed\"; error: unknown };\n\nexport interface PurchaseFlowOutcome<P extends PurchaseFlowPurchase> {\n purchase: P;\n status: PurchaseFlowStatus;\n}\n\nexport interface PurchaseFlowOptions<P extends PurchaseFlowPurchase> {\n /**\n * Confirms and grants access before returning `entitled`; the flow finishes\n * the purchase immediately after. Throws or timeouts leave it unfinished\n * for recovery. An app backend or any verification service can answer here.\n */\n confirmWithBackend: (\n purchase: P,\n context: { isConsumable: boolean },\n ) => Promise<PurchaseConfirmation>;\n /**\n * Milliseconds `confirmWithBackend` may take before the purchase ends as\n * `failed`, so a hung call cannot hold it for the session. Default 30000.\n */\n confirmTimeoutMs?: number;\n finishTransaction: (args: {\n purchase: P;\n isConsumable: boolean;\n }) => Promise<unknown>;\n /**\n * From the app's product config. On Android a wrong answer consumes a\n * non-consumable or blocks rebuying a consumable.\n */\n isConsumable: (productId: string) => boolean;\n getAvailablePurchases: () => Promise<P[]>;\n /**\n * The SDK's `getPendingTransactionsIOS` on iOS, `undefined` elsewhere: off\n * iOS an SDK may answer `[]`, which would hide unacknowledged Android\n * purchases.\n * Recovery reads unfinished StoreKit transactions, consumables included,\n * from it. If it throws, recovery confirms every Apple purchase\n * `getAvailablePurchases` lists instead.\n */\n getPendingTransactionsIOS: (() => Promise<P[]>) | undefined;\n /**\n * Registers for the store's purchase deliveries; the flow processes each one\n * until `dispose`. SDKs pass their purchase listener.\n */\n listenForPurchases?: (onPurchase: (purchase: P) => void) => {\n remove: () => void;\n };\n /**\n * Whether the user's own request produced a delivered purchase. Only then\n * may an `invalid` delivery offer buying again; without this, no delivery\n * the flow listened for does.\n */\n wasRequested?: (purchase: P) => boolean;\n}\n\nexport interface RecoverPurchasesOptions {\n /** Also re-confirm purchases that were already finished, for a restore button. */\n includeFinished?: boolean;\n}\n\nexport interface PurchaseFlow<P extends PurchaseFlowPurchase> {\n /**\n * Confirms one purchase the user just bought. Finishes it once access is\n * granted to this or another account, and a settled rejection on iOS only:\n * Google Play refunds a rejected purchase left unacknowledged, while iOS\n * would redeliver it forever.\n */\n processPurchase: (purchase: P) => Promise<PurchaseFlowStatus>;\n /**\n * Runs unfinished purchases (or every owned one) through the flow, one at a\n * time; a call made while a pass runs shares it. Throws if it meets an\n * Apple purchase without `getPendingTransactionsIOS`.\n */\n recoverPurchases: (\n options?: RecoverPurchasesOptions,\n ) => Promise<PurchaseFlowOutcome<P>[]>;\n /**\n * Calls `listener` with every outcome: once per confirmation, however many\n * deliveries shared it, and once per delivery of an already settled\n * purchase. Returns the function that unsubscribes it.\n */\n subscribe: (\n listener: (outcome: PurchaseFlowOutcome<P>) => void,\n ) => () => void;\n /**\n * Stops listening and publishing, and stops a recovery pass from confirming\n * anything more; confirmations already running still finish. Call it when\n * the signed-in account changes, then create the next account's flow.\n */\n dispose: () => void;\n}\n\n/**\n * How far a store's purchase time may trail the moment the app asked for it\n * and still count as that request: device clocks run ahead. A redelivered\n * receipt is far older.\n */\nexport const PURCHASE_REQUEST_CLOCK_SKEW_MS = 600_000;\n\ninterface PurchaseRun<P> {\n task: Promise<PurchaseFlowStatus>;\n latest: P;\n requested: boolean;\n}\n\n/**\n * Single-product, single-unit purchases only. Create one flow per\n * authenticated app account. Capture that session in confirmWithBackend and\n * dispose the flow after switching accounts.\n */\nexport function createPurchaseFlow<P extends PurchaseFlowPurchase>(\n options: PurchaseFlowOptions<P>,\n): PurchaseFlow<P> {\n // A listener and a recovery pass can deliver the same transaction at once.\n const runs = new Map<string, PurchaseRun<P>>();\n // Finished purchases, and rejections the store settles on its own. A\n // recovery pass can hold a store list read before the listener settled one.\n const settled = new Map<string, PurchaseFlowStatus>();\n const recoveries = new Map<boolean, Promise<PurchaseFlowOutcome<P>[]>>();\n const listeners = new Set<(outcome: PurchaseFlowOutcome<P>) => void>();\n let disposed = false;\n // setTimeout runs a delay past 2^31 - 1 ms, Infinity included, at once.\n const confirmTimeoutMs = Math.min(\n options.confirmTimeoutMs ?? 30_000,\n 2_147_483_647,\n );\n const subscription = options.listenForPurchases?.((purchase) => {\n // A pending delivery leaves the mark for the completed one that follows.\n const requested =\n purchase.purchaseState !== \"pending\" &&\n (options.wasRequested?.(purchase) ?? false);\n void deliver(purchase, requested);\n });\n\n function publish(outcome: PurchaseFlowOutcome<P>): void {\n if (disposed) return;\n for (const listener of listeners) {\n try {\n listener(outcome);\n } catch {\n // One subscriber's error must not keep the outcome from the others.\n }\n }\n }\n\n // Marked per delivery: a shared run may have started from an older copy of\n // the purchase, before Play attached the pending change.\n function forDelivery(\n purchase: P,\n status: PurchaseFlowStatus,\n requested: boolean,\n ): PurchaseFlowStatus {\n if (status.status === \"entitled\" && purchase.pendingPurchaseUpdateAndroid) {\n return { ...status, changePending: true };\n }\n // An old expired renewal must not ask a subscriber to buy again.\n if (status.status === \"invalid\" && !requested) {\n return { ...status, recovered: true };\n }\n return status;\n }\n\n function processPurchase(purchase: P): Promise<PurchaseFlowStatus> {\n return deliver(purchase, true);\n }\n\n function deliver(\n purchase: P,\n requested: boolean,\n reconfirm = false,\n ): Promise<PurchaseFlowStatus> {\n const immediate = immediateStatus(purchase, reconfirm);\n if (immediate) {\n const status = forDelivery(purchase, immediate, requested);\n publish({ purchase, status });\n return Promise.resolve(status);\n }\n let run = runs.get(purchase.id);\n if (run) {\n run.latest = purchase;\n run.requested ||= requested;\n } else {\n const started: PurchaseRun<P> = {\n latest: purchase,\n requested,\n task: confirmAndFinish(purchase).then((status) => {\n runs.delete(purchase.id);\n if (isSettled(status)) settled.set(purchase.id, status);\n publish({\n purchase: started.latest,\n status: forDelivery(started.latest, status, started.requested),\n });\n return status;\n }),\n };\n runs.set(purchase.id, started);\n run = started;\n }\n return run.task.then((status) => forDelivery(purchase, status, requested));\n }\n\n function immediateStatus(\n purchase: P,\n reconfirm: boolean,\n ): PurchaseFlowStatus | undefined {\n try {\n assertSupportedPurchase(purchase);\n } catch (error) {\n return { status: \"failed\", error };\n }\n // Payment is not complete; the store delivers the purchase again once it is.\n if (purchase.purchaseState === \"pending\")\n return { status: \"store_pending\" };\n return reconfirm || runs.has(purchase.id)\n ? undefined\n : settled.get(purchase.id);\n }\n\n async function confirmAndFinish(purchase: P): Promise<PurchaseFlowStatus> {\n let confirmation: PurchaseConfirmation | null;\n let isConsumable: boolean;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n // Read once: finishing must consume exactly what confirmation granted.\n isConsumable = options.isConsumable(purchase.productId);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new Error(\n `confirmWithBackend did not answer within ${confirmTimeoutMs} ms`,\n ),\n );\n }, confirmTimeoutMs);\n });\n confirmation = readConfirmation(\n await Promise.race([\n options.confirmWithBackend(purchase, { isConsumable }),\n timeout,\n ]),\n );\n } catch (error) {\n return { status: \"failed\", error };\n } finally {\n clearTimeout(timer);\n }\n if (!confirmation) {\n return {\n status: \"failed\",\n error: new Error(\"Unrecognized purchase confirmation from the backend\"),\n };\n }\n // Finishing settles a purchase for good, so only a settled verdict does.\n switch (confirmation.status) {\n case \"entitled\":\n return await finish(purchase, isConsumable, confirmation);\n case \"ownership_conflict\":\n return confirmation.alreadyGranted\n ? await finish(purchase, isConsumable, confirmation)\n : confirmation;\n case \"invalid\":\n case \"test_account_restricted\":\n return purchase.store === \"apple\"\n ? await finish(purchase, isConsumable, confirmation)\n : confirmation;\n default:\n return confirmation;\n }\n }\n\n async function finish(\n purchase: P,\n isConsumable: boolean,\n confirmation: Extract<\n PurchaseConfirmation,\n { status: Finishable | \"invalid\" }\n >,\n ): Promise<PurchaseFlowStatus> {\n try {\n // Play keeps an acknowledged purchase finished; a restore need not redo it.\n if (purchase.isAcknowledgedAndroid === true && !isConsumable) {\n return confirmation;\n }\n await options.finishTransaction({ purchase, isConsumable });\n return confirmation;\n } catch (finishError) {\n return { ...confirmation, finishError };\n }\n }\n\n function recoverPurchases(\n recoverOptions: RecoverPurchasesOptions = {},\n ): Promise<PurchaseFlowOutcome<P>[]> {\n const includeFinished = recoverOptions.includeFinished ?? false;\n let pass = recoveries.get(includeFinished);\n if (!pass) {\n pass = recover(includeFinished).finally(() => {\n recoveries.delete(includeFinished);\n });\n recoveries.set(includeFinished, pass);\n }\n return pass;\n }\n\n async function recover(\n includeFinished: boolean,\n ): Promise<PurchaseFlowOutcome<P>[]> {\n const { getPendingTransactionsIOS } = options;\n const settledBefore = new Set(settled.keys());\n // iOS lists unfinished transactions directly; other stores list owned\n // purchases, and isUnfinished picks the ones still to settle.\n let pendingIOS: P[] | undefined;\n let pendingIOSFailed = false;\n try {\n pendingIOS = await getPendingTransactionsIOS?.();\n } catch {\n // One unverified transaction fails the whole list. Re-confirming a\n // finished purchase is safe, so every owned Apple purchase stands in.\n pendingIOSFailed = true;\n }\n const available =\n includeFinished || !pendingIOS\n ? await options.getAvailablePurchases()\n : [];\n if (\n !getPendingTransactionsIOS &&\n available.some((purchase) => purchase.store === \"apple\")\n ) {\n throw new Error(\n \"recoverPurchases needs getPendingTransactionsIOS on iOS to find unfinished transactions\",\n );\n }\n // A restore confirms each Apple purchase once, through its newest\n // transaction: older renewals add nothing, and the unfinished ones come\n // from the pending list. Without that list, every transaction stands in.\n const owned =\n includeFinished && pendingIOS\n ? newestAppleTransactions(available)\n : available;\n const unfinished = owned.filter(\n (purchase) =>\n includeFinished ||\n (purchase.store === \"apple\"\n ? pendingIOSFailed\n : isUnfinished(purchase)),\n );\n const purchases = new Map<string, P>();\n for (const purchase of [...(pendingIOS ?? []), ...unfinished]) {\n if (!purchases.has(purchase.id)) purchases.set(purchase.id, purchase);\n }\n const outcomes: PurchaseFlowOutcome<P>[] = [];\n for (const purchase of purchases.values()) {\n // The next account's flow confirms the rest with its own session.\n if (disposed) break;\n if (!includeFinished && settled.has(purchase.id)) continue;\n // A restore re-confirms what settled before it, not what the listener\n // settled while it ran.\n const status = await deliver(\n purchase,\n false,\n includeFinished && settledBefore.has(purchase.id),\n );\n outcomes.push({ purchase, status });\n }\n return outcomes;\n }\n\n function isUnfinished(purchase: P): boolean {\n // Amazon and most community stores report no acknowledgment, and Amazon\n // approves a Kids purchase later; a listed consumable is unconsumed.\n return (\n purchase.isAcknowledgedAndroid !== true ||\n options.isConsumable(purchase.productId)\n );\n }\n\n return {\n processPurchase,\n recoverPurchases,\n subscribe(listener) {\n const entry = (outcome: PurchaseFlowOutcome<P>) => listener(outcome);\n listeners.add(entry);\n return () => {\n listeners.delete(entry);\n };\n },\n dispose() {\n disposed = true;\n listeners.clear();\n subscription?.remove();\n },\n };\n}\n\n// Keeps each Apple purchase's newest transaction, in list order. One without\n// an original id or a date cannot be ranked, so it stays.\nfunction newestAppleTransactions<P extends PurchaseFlowPurchase>(\n purchases: P[],\n): P[] {\n const chainOf = (purchase: P) =>\n purchase.store === \"apple\" && (purchase.transactionDate ?? 0) > 0\n ? purchase.originalTransactionIdentifierIOS || undefined\n : undefined;\n const newest = new Map<string, P>();\n for (const purchase of purchases) {\n const key = chainOf(purchase);\n if (!key) continue;\n const kept = newest.get(key);\n if (\n !kept ||\n (purchase.transactionDate ?? 0) > (kept.transactionDate ?? 0)\n ) {\n newest.set(key, purchase);\n }\n }\n return purchases.filter((purchase) => {\n const key = chainOf(purchase);\n return !key || newest.get(key) === purchase;\n });\n}\n\n// A settled purchase is finished, or rejected for good where the store\n// settles it, so the same delivery need not be confirmed again.\nfunction isSettled(status: PurchaseFlowStatus): boolean {\n switch (status.status) {\n case \"entitled\":\n case \"invalid\":\n case \"test_account_restricted\":\n return status.finishError === undefined;\n case \"ownership_conflict\":\n return status.alreadyGranted && status.finishError === undefined;\n default:\n return false;\n }\n}\n\n/** Builds the backend request body; throws for a purchase without Apple or Google evidence. */\nexport function toPurchaseConfirmationRequest(\n purchase: PurchaseFlowPurchase,\n): PurchaseConfirmationRequest {\n const { store, purchaseToken, productId } = purchase;\n if ((store !== \"apple\" && store !== \"google\") || !purchaseToken) {\n throw new Error(\n `Purchase ${productId} has no Apple or Google purchase token to confirm`,\n );\n }\n assertSupportedPurchase(purchase);\n return { store, purchaseToken, productId };\n}\n\nfunction assertSupportedPurchase(purchase: PurchaseFlowPurchase): void {\n if (purchase.quantity !== undefined && purchase.quantity !== 1) {\n throw new Error(\"Purchase flow supports only single-unit purchases\");\n }\n if (new Set(purchase.ids ?? []).size > 1) {\n throw new Error(\n \"Purchase flow cannot finish a multi-product purchase before every product is granted\",\n );\n }\n}\n\nexport type PurchaseFlowGuidanceKey =\n | \"payment_pending\"\n | \"change_pending\"\n | \"verification_delayed\"\n | \"owned_by_another_account\"\n | \"test_purchase_not_accepted\"\n | \"purchase_not_valid\"\n | \"access_pending\"\n | \"access_granted\"\n | \"try_again_later\";\n\n/** Neutral guidance an app maps to its own copy. */\nexport interface PurchaseFlowGuidance {\n key: PurchaseFlowGuidanceKey;\n /**\n * True only for an `invalid` purchase the user just made. False whenever the\n * user may already have paid: never ask them to buy again.\n */\n offerPurchase: boolean;\n /** Earliest time to run the purchase through the flow again. */\n retryAfterMs?: number;\n}\n\nexport function describePurchaseFlowStatus(\n status: PurchaseFlowStatus,\n): PurchaseFlowGuidance {\n switch (status.status) {\n case \"store_pending\":\n return { key: \"payment_pending\", offerPurchase: false };\n case \"verification_delayed\":\n return {\n key: \"verification_delayed\",\n offerPurchase: false,\n retryAfterMs: status.retryAfterMs,\n };\n case \"ownership_conflict\":\n return { key: \"owned_by_another_account\", offerPurchase: false };\n case \"test_account_restricted\":\n return { key: \"test_purchase_not_accepted\", offerPurchase: false };\n case \"invalid\":\n return {\n key: \"purchase_not_valid\",\n offerPurchase: status.recovered !== true,\n };\n case \"verified\":\n case \"linked\":\n return status.retryAfterMs === undefined\n ? { key: \"access_pending\", offerPurchase: false }\n : {\n key: \"access_pending\",\n offerPurchase: false,\n retryAfterMs: status.retryAfterMs,\n };\n case \"entitled\":\n return {\n key: status.changePending ? \"change_pending\" : \"access_granted\",\n offerPurchase: false,\n };\n case \"failed\":\n return { key: \"try_again_later\", offerPurchase: false };\n }\n}\n\n// Backend JSON is untrusted: anything off-contract becomes `failed`, so a\n// malformed body can never finish a transaction.\nfunction readConfirmation(value: unknown): PurchaseConfirmation | null {\n if (typeof value !== \"object\" || value === null) return null;\n const { status, retryAfterMs, alreadyGranted, reason } = value as Record<\n string,\n unknown\n >;\n const retry =\n typeof retryAfterMs === \"number\" &&\n Number.isFinite(retryAfterMs) &&\n retryAfterMs >= 0\n ? retryAfterMs\n : undefined;\n switch (status) {\n case \"store_pending\":\n case \"test_account_restricted\":\n return { status };\n case \"invalid\":\n return typeof reason === \"string\" ? { status, reason } : { status };\n case \"verification_delayed\":\n return retry === undefined ? null : { status, retryAfterMs: retry };\n case \"verified\":\n case \"linked\":\n return retry === undefined ? { status } : { status, retryAfterMs: retry };\n case \"ownership_conflict\":\n case \"entitled\":\n return typeof alreadyGranted === \"boolean\"\n ? { status, alreadyGranted }\n : null;\n default:\n return null;\n }\n}\n"]}
|
package/bun.lock
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
41
|
"overrides": {
|
|
42
|
+
"node-forge": "1.4.2",
|
|
42
43
|
"shell-quote": "1.12.0",
|
|
43
44
|
"uuid": "11.1.1",
|
|
44
45
|
"zod": "4.6.7",
|
|
@@ -1402,7 +1403,7 @@
|
|
|
1402
1403
|
|
|
1403
1404
|
"node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="],
|
|
1404
1405
|
|
|
1405
|
-
"node-forge": ["node-forge@1.4.
|
|
1406
|
+
"node-forge": ["node-forge@1.4.2", "", {}, "sha512-rRtAR5XLJ8gCKd9du4qrKAsgCG1ObGP4gLeNJMa4vYSeq4bmBMOJk5IHkrviBV0q7uoAJlVd8HEQYQSVUb4FGQ=="],
|
|
1406
1407
|
|
|
1407
1408
|
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
|
1408
1409
|
|
package/ios/ExpoIap.podspec
CHANGED
|
@@ -12,16 +12,28 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.author = package['author']
|
|
13
13
|
s.homepage = package['homepage']
|
|
14
14
|
# Do not change iOS from 13.4: 15.0 can make expo prebuild exclude the module in
|
|
15
|
-
# some Expo SDKs (https://github.com/hyochan/expo-iap/issues/168).
|
|
16
|
-
#
|
|
17
|
-
# tvOS is 16.0, the openiap dependency's minimum tvOS deployment target.
|
|
15
|
+
# some Expo SDKs (https://github.com/hyochan/expo-iap/issues/168). Apps still
|
|
16
|
+
# need iOS 15.0 and tvOS 16.0, the OpenIAP Swift package's minimums.
|
|
18
17
|
s.platforms = { :ios => '13.4', :tvos => '16.0' }
|
|
19
18
|
s.swift_version = '5.9'
|
|
20
19
|
s.source = { :path => '.' }
|
|
21
20
|
s.static_framework = true
|
|
22
21
|
|
|
23
22
|
s.dependency 'ExpoModulesCore'
|
|
24
|
-
|
|
23
|
+
# OpenIAP comes from its Swift package at the exact pinned version.
|
|
24
|
+
# OPENIAP_APPLE_PATH (absolute) builds a local packages/apple checkout instead.
|
|
25
|
+
local_openiap = ENV['OPENIAP_APPLE_PATH'].to_s.strip
|
|
26
|
+
unless local_openiap.empty? || File.exist?(File.join(local_openiap, 'Package.swift'))
|
|
27
|
+
raise "OPENIAP_APPLE_PATH #{local_openiap} has no Package.swift"
|
|
28
|
+
end
|
|
29
|
+
unless defined?(spm_dependency)
|
|
30
|
+
raise 'ExpoIap needs a React Native 0.75+ Podfile, which adds the OpenIAP Swift package'
|
|
31
|
+
end
|
|
32
|
+
spm_dependency(s,
|
|
33
|
+
url: local_openiap.empty? ? 'https://github.com/hyodotdev/openiap.git' : File.expand_path(local_openiap),
|
|
34
|
+
requirement: { kind: 'exactVersion', version: versions['apple'] },
|
|
35
|
+
products: ['OpenIAP']
|
|
36
|
+
)
|
|
25
37
|
|
|
26
38
|
# OnsideKit dependency is conditionally included via ENV var set by the Expo plugin.
|
|
27
39
|
# When modules.onside is enabled, the plugin prepends ENV['EXPO_IAP_ONSIDE']='1' to the
|
package/openiap-versions.json
CHANGED
package/osv-scanner.toml
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
[[IgnoredVulns]]
|
|
2
|
-
id = "GHSA-86w9-cpqp-85rv"
|
|
3
|
-
ignoreUntil = 2026-12-15
|
|
4
|
-
reason = "No patched node-forge release exists; 1.4.0 is npm latest and still affected. Reached only via @expo/cli and @expo/code-signing-certificates for development certificate tooling; the published SDK and config plugin do not import it. Development certificate verification remains affected: https://github.com/advisories/GHSA-86w9-cpqp-85rv."
|
|
5
|
-
|
|
6
1
|
[[IgnoredVulns]]
|
|
7
2
|
id = "GHSA-gcqx-w48m-w2h4"
|
|
8
3
|
ignoreUntil = 2026-11-04
|
|
9
4
|
reason = "No patched deepmerge release exists. This lock reaches it only through Jest, Expo configuration, or React Native/Kepler CLI configuration used for trusted local builds and tests; mobile entry points and the published SDK runtime do not import those tools. Do not load untrusted project configuration. Revisit the upstream fix before expiry: https://github.com/advisories/GHSA-gcqx-w48m-w2h4."
|
|
5
|
+
|
|
6
|
+
[[IgnoredVulns]]
|
|
7
|
+
id = "GHSA-3g3m-2fjr-6pw4"
|
|
8
|
+
ignoreUntil = 2026-11-04
|
|
9
|
+
reason = "The fix is glob 14.0.2, outside Expo 57 dependency ranges. Expo uses glob for trusted local project discovery and builds, including early cancellation after the first match; cancellation must not be treated as an enforced traversal budget. The published expo-iap mobile entry points do not import glob. Do not run this tooling on untrusted directory trees or expose it as a public request handler. Recheck compatible upstream upgrades before expiry: https://github.com/advisories/GHSA-3g3m-2fjr-6pw4."
|
|
10
|
+
|
|
11
|
+
[[IgnoredVulns]]
|
|
12
|
+
id = "GHSA-g3hv-75wg-m249"
|
|
13
|
+
ignoreUntil = 2026-11-04
|
|
14
|
+
reason = "The fix is minimatch 11.0.1, outside the Expo 57, Jest and ESLint dependency ranges. These copies serve local project discovery, fingerprinting, lint and coverage; the published expo-iap mobile entry points do not import them. Patterns come from the trusted checkout and build configuration; do not load untrusted project patterns or expose this tooling as a public request handler. Recheck compatible upstream upgrades before expiry: https://github.com/advisories/GHSA-g3hv-75wg-m249."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.2",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -103,7 +103,8 @@
|
|
|
103
103
|
"overrides": {
|
|
104
104
|
"zod": "4.6.7",
|
|
105
105
|
"uuid": "11.1.1",
|
|
106
|
-
"shell-quote": "1.12.0"
|
|
106
|
+
"shell-quote": "1.12.0",
|
|
107
|
+
"node-forge": "1.4.2"
|
|
107
108
|
},
|
|
108
109
|
"expo": {
|
|
109
110
|
"plugin": "./app.plugin.js"
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -466,11 +466,11 @@ const withIapIOS = (config, options) => {
|
|
|
466
466
|
content = `${cdnLine}\n\n${content}`;
|
|
467
467
|
(0, logOnce_1.logOnce)('📦 expo-iap: Added CocoaPods CDN source to Podfile');
|
|
468
468
|
}
|
|
469
|
-
// 2)
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
content =
|
|
473
|
-
(0, logOnce_1.logOnce)('🧹 expo-iap: Removed local OpenIAP
|
|
469
|
+
// 2) Build OpenIAP from the published Swift package
|
|
470
|
+
const published = (0, withLocalOpenIAP_1.removeLocalOpenIapPathIOS)(content);
|
|
471
|
+
if (published !== content) {
|
|
472
|
+
content = published;
|
|
473
|
+
(0, logOnce_1.logOnce)('🧹 expo-iap: Removed the local OpenIAP path from Podfile');
|
|
474
474
|
}
|
|
475
475
|
// 3) Optionally install OnsideKit when enabled in config
|
|
476
476
|
if (options?.enableOnside) {
|
|
@@ -480,6 +480,7 @@ const withIapIOS = (config, options) => {
|
|
|
480
480
|
}
|
|
481
481
|
content = updatedContent;
|
|
482
482
|
}
|
|
483
|
+
(0, withLocalOpenIAP_1.warnOnOpenIapPodIOS)(content);
|
|
483
484
|
config.modResults.contents = content;
|
|
484
485
|
return config;
|
|
485
486
|
});
|
|
@@ -713,14 +714,14 @@ const withIap = (config, options) => {
|
|
|
713
714
|
});
|
|
714
715
|
}
|
|
715
716
|
else {
|
|
716
|
-
//
|
|
717
|
+
// Published OpenIAP: the Swift package on iOS, Maven on Android
|
|
717
718
|
result = withIapIOS(result, {
|
|
718
719
|
enableOnside: includeOnside,
|
|
719
720
|
iosAlternativeBilling,
|
|
720
721
|
});
|
|
721
722
|
result = (0, withLocalOpenIAP_1.withoutLocalOpenIAPAndroid)(result);
|
|
722
723
|
if (includeExpoIap) {
|
|
723
|
-
(0, logOnce_1.logOnce)('📦 [expo-iap] Using OpenIAP
|
|
724
|
+
(0, logOnce_1.logOnce)('📦 [expo-iap] Using the published OpenIAP packages');
|
|
724
725
|
}
|
|
725
726
|
}
|
|
726
727
|
syncAutolinking(autolinkState);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
2
|
import { type IOSAlternativeBillingConfig } from './withIosAlternativeBilling';
|
|
3
|
-
/**
|
|
3
|
+
/** Local openiap-apple and openiap-google checkouts, for OpenIAP development only. */
|
|
4
4
|
export type LocalPathOption = string | {
|
|
5
5
|
ios?: string;
|
|
6
6
|
android?: string;
|
|
@@ -17,7 +17,9 @@ export declare const appStoreLines: (storeScriptPath: string, language: GradleLa
|
|
|
17
17
|
export declare const removeLocalOpenIapFlavorStrategy: (contents: string) => string;
|
|
18
18
|
export declare const removeLocalOpenIapSettings: (contents: string) => string;
|
|
19
19
|
export declare const removeLocalOpenIapAppWiring: (contents: string) => string;
|
|
20
|
-
export declare const
|
|
20
|
+
export declare const removeLocalOpenIapPathIOS: (podfile: string) => string;
|
|
21
|
+
export declare const setLocalOpenIapPathIOS: (podfile: string, relativePath: string) => string;
|
|
22
|
+
export declare const warnOnOpenIapPodIOS: (podfile: string) => void;
|
|
21
23
|
export declare const ensureLocalOpenIapFlavorStrategy: (contents: string, storeScriptPath: string, language?: GradleLanguage) => string;
|
|
22
24
|
declare const withLocalOpenIAP: ConfigPlugin<{
|
|
23
25
|
localPath?: LocalPathOption;
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.withoutLocalOpenIAPAndroid = exports.ensureLocalOpenIapFlavorStrategy = exports.
|
|
36
|
+
exports.withoutLocalOpenIAPAndroid = exports.ensureLocalOpenIapFlavorStrategy = exports.warnOnOpenIapPodIOS = exports.setLocalOpenIapPathIOS = exports.removeLocalOpenIapPathIOS = exports.removeLocalOpenIapAppWiring = exports.removeLocalOpenIapSettings = exports.removeLocalOpenIapFlavorStrategy = exports.appStoreLines = exports.LOCAL_STRATEGY_LINE_KOTLIN = exports.LOCAL_STRATEGY_LINE_GROOVY = exports.storeScriptPathFrom = exports.getAndroidLocalPathInput = void 0;
|
|
37
37
|
const config_plugins_1 = require("expo/config-plugins");
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
@@ -116,9 +116,28 @@ const removeLocalOpenIapAppWiring = (contents) => contents
|
|
|
116
116
|
.replace(/^[ \t]*val openIapStore = .*openIapResolveStore.*\n?(?:^[ \t]*\n)?/gm, '')
|
|
117
117
|
.replace(/^[ \t]*missingDimensionStrategy[\s(]{0,4}["']platform["'][^\n]*(openIapResolveStore|openIapStore)[^\n]*\n?/gm, '');
|
|
118
118
|
exports.removeLocalOpenIapAppWiring = removeLocalOpenIapAppWiring;
|
|
119
|
-
// A
|
|
120
|
-
const
|
|
121
|
-
|
|
119
|
+
// A :path pod beside the Swift package would define OpenIAP twice.
|
|
120
|
+
const LOCAL_OPENIAP_IOS = /^[ \t]*ENV\[\s*['"]OPENIAP_APPLE_PATH['"]\s*\][^\n]*\n?|^[ \t]*# Local OpenIAP pod for development \(added by expo-iap plugin\)\n|^[ \t]*pod\s+['"]openiap['"]\s*,\s*:path\s*=>[^\n]*\n?/gm;
|
|
121
|
+
// ExpoIap builds OpenIAP from the published Swift package.
|
|
122
|
+
const removeLocalOpenIapPathIOS = (podfile) => podfile.replace(LOCAL_OPENIAP_IOS, '');
|
|
123
|
+
exports.removeLocalOpenIapPathIOS = removeLocalOpenIapPathIOS;
|
|
124
|
+
// ExpoIap builds OpenIAP from localPath instead of the published Swift package.
|
|
125
|
+
const setLocalOpenIapPathIOS = (podfile, relativePath) => {
|
|
126
|
+
const quoted = relativePath.replace(/[\\']/g, '\\$&');
|
|
127
|
+
const line = `ENV['OPENIAP_APPLE_PATH'] = File.expand_path('${quoted}', __dir__)`;
|
|
128
|
+
const rest = (0, exports.removeLocalOpenIapPathIOS)(podfile);
|
|
129
|
+
// After any leading ENV lines, so the Onside flag keeps the first line.
|
|
130
|
+
const lead = rest.match(/^(?:ENV\[[^\n]*\n)*/)?.[0] ?? '';
|
|
131
|
+
return `${lead}${line}\n${rest.slice(lead.length)}`;
|
|
132
|
+
};
|
|
133
|
+
exports.setLocalOpenIapPathIOS = setLocalOpenIapPathIOS;
|
|
134
|
+
// ExpoIap already adds OpenIAP as a Swift package.
|
|
135
|
+
const warnOnOpenIapPodIOS = (podfile) => {
|
|
136
|
+
if (/^[ \t]*pod\s+['"]openiap['"]/m.test(podfile)) {
|
|
137
|
+
config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', "Remove pod 'openiap' from the Podfile: ExpoIap adds OpenIAP as a Swift package, and both together define OpenIAP twice.");
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
exports.warnOnOpenIapPodIOS = warnOnOpenIapPodIOS;
|
|
122
141
|
// Every Android module in a local build links the flavor the resolver picks.
|
|
123
142
|
const ensureLocalOpenIapFlavorStrategy = (contents, storeScriptPath, language = 'groovy') => {
|
|
124
143
|
const cleaned = (0, exports.removeLocalOpenIapFlavorStrategy)(contents).trimEnd();
|
|
@@ -194,7 +213,7 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
194
213
|
// The local openiap-google module: from localPath, else beside the app.
|
|
195
214
|
const localAndroidModule = (projectRoot) => resolveAndroidModulePath(androidInput) ??
|
|
196
215
|
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google'));
|
|
197
|
-
// iOS:
|
|
216
|
+
// iOS: point ExpoIap's OpenIAP Swift package at the local checkout
|
|
198
217
|
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
199
218
|
'ios',
|
|
200
219
|
async (config) => {
|
|
@@ -203,11 +222,6 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
203
222
|
const iosPath = (typeof raw === 'string' ? raw : raw?.ios) ||
|
|
204
223
|
path.resolve(projectRoot, 'openiap-apple');
|
|
205
224
|
const podfilePath = path.join(platformProjectRoot, 'Podfile');
|
|
206
|
-
if (!fs.existsSync(iosPath)) {
|
|
207
|
-
console.warn(`⚠️ Local openiap-apple path not found: ${iosPath}`);
|
|
208
|
-
console.warn(' Skipping local pod injection.');
|
|
209
|
-
return config;
|
|
210
|
-
}
|
|
211
225
|
let podfileContent;
|
|
212
226
|
try {
|
|
213
227
|
podfileContent = fs.readFileSync(podfilePath, 'utf8');
|
|
@@ -219,51 +233,28 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
219
233
|
console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
|
|
220
234
|
return config;
|
|
221
235
|
}
|
|
222
|
-
|
|
223
|
-
let podfileChanged = false;
|
|
236
|
+
let content = podfileContent;
|
|
224
237
|
if (props?.enableOnside) {
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
podfileContent = updatedContent;
|
|
228
|
-
podfileChanged = true;
|
|
238
|
+
content = (0, onsidePodfile_1.ensureOnsidePodIOS)(content);
|
|
239
|
+
if (content !== podfileContent) {
|
|
229
240
|
(0, logOnce_1.logOnce)('📦 expo-iap: Enabled OnsideKit');
|
|
230
241
|
}
|
|
231
242
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (updatedContent !== podfileContent) {
|
|
239
|
-
podfileContent = updatedContent;
|
|
240
|
-
podfileChanged = true;
|
|
241
|
-
(0, logOnce_1.logOnce)(`✅ Moved the local OpenIAP pod to: ${iosPath}`);
|
|
242
|
-
}
|
|
243
|
-
if (podfileChanged) {
|
|
244
|
-
fs.writeFileSync(podfilePath, podfileContent);
|
|
245
|
-
}
|
|
246
|
-
(0, logOnce_1.logOnce)('✅ Local OpenIAP pod already configured');
|
|
247
|
-
return config;
|
|
248
|
-
}
|
|
249
|
-
const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
|
|
250
|
-
if (targetRegex.test(podfileContent)) {
|
|
251
|
-
podfileContent = podfileContent.replace(targetRegex, (match) => {
|
|
252
|
-
return `${match}
|
|
253
|
-
|
|
254
|
-
# Local OpenIAP pod for development (added by expo-iap plugin)
|
|
255
|
-
pod 'openiap', :path => '${relativePath}'`;
|
|
256
|
-
});
|
|
257
|
-
podfileChanged = true;
|
|
258
|
-
}
|
|
259
|
-
if (podfileChanged) {
|
|
260
|
-
fs.writeFileSync(podfilePath, podfileContent);
|
|
261
|
-
}
|
|
262
|
-
if (podfileContent.includes("pod 'openiap',")) {
|
|
263
|
-
(0, logOnce_1.logOnce)(`✅ Added local OpenIAP pod at: ${iosPath}`);
|
|
243
|
+
if (fs.existsSync(iosPath)) {
|
|
244
|
+
(0, logOnce_1.logOnce)(`✅ Using local OpenIAP from: ${iosPath}`);
|
|
245
|
+
const relativePath = path
|
|
246
|
+
.relative(platformProjectRoot, iosPath)
|
|
247
|
+
.replace(/\\/g, '/');
|
|
248
|
+
content = (0, exports.setLocalOpenIapPathIOS)(content, relativePath);
|
|
264
249
|
}
|
|
265
250
|
else {
|
|
266
|
-
console.warn(
|
|
251
|
+
console.warn(`⚠️ Local openiap-apple path not found: ${iosPath}`);
|
|
252
|
+
console.warn(' Building OpenIAP from the published Swift package instead.');
|
|
253
|
+
content = (0, exports.removeLocalOpenIapPathIOS)(content);
|
|
254
|
+
}
|
|
255
|
+
(0, exports.warnOnOpenIapPodIOS)(content);
|
|
256
|
+
if (content !== podfileContent) {
|
|
257
|
+
fs.writeFileSync(podfilePath, content);
|
|
267
258
|
}
|
|
268
259
|
return config;
|
|
269
260
|
},
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
import type {ExpoConfig} from '@expo/config-types';
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import withLocalOpenIAP, {
|
|
16
|
+
import withLocalOpenIAP, {
|
|
17
|
+
removeLocalOpenIapPathIOS,
|
|
18
|
+
warnOnOpenIapPodIOS,
|
|
19
|
+
withoutLocalOpenIAPAndroid,
|
|
20
|
+
} from './withLocalOpenIAP';
|
|
17
21
|
import withVega, {type VegaProjectOptions} from './withVega';
|
|
18
22
|
import {
|
|
19
23
|
withIosAlternativeBilling,
|
|
@@ -646,12 +650,11 @@ const withIapIOS: ConfigPlugin<WithIapIosOptions | undefined> = (
|
|
|
646
650
|
logOnce('📦 expo-iap: Added CocoaPods CDN source to Podfile');
|
|
647
651
|
}
|
|
648
652
|
|
|
649
|
-
// 2)
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
logOnce('🧹 expo-iap: Removed local OpenIAP pod from Podfile');
|
|
653
|
+
// 2) Build OpenIAP from the published Swift package
|
|
654
|
+
const published = removeLocalOpenIapPathIOS(content);
|
|
655
|
+
if (published !== content) {
|
|
656
|
+
content = published;
|
|
657
|
+
logOnce('🧹 expo-iap: Removed the local OpenIAP path from Podfile');
|
|
655
658
|
}
|
|
656
659
|
|
|
657
660
|
// 3) Optionally install OnsideKit when enabled in config
|
|
@@ -662,6 +665,7 @@ const withIapIOS: ConfigPlugin<WithIapIosOptions | undefined> = (
|
|
|
662
665
|
}
|
|
663
666
|
content = updatedContent;
|
|
664
667
|
}
|
|
668
|
+
warnOnOpenIapPodIOS(content);
|
|
665
669
|
|
|
666
670
|
config.modResults.contents = content;
|
|
667
671
|
return config;
|
|
@@ -1015,14 +1019,14 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
1015
1019
|
enableOnside: includeOnside,
|
|
1016
1020
|
});
|
|
1017
1021
|
} else {
|
|
1018
|
-
//
|
|
1022
|
+
// Published OpenIAP: the Swift package on iOS, Maven on Android
|
|
1019
1023
|
result = withIapIOS(result, {
|
|
1020
1024
|
enableOnside: includeOnside,
|
|
1021
1025
|
iosAlternativeBilling,
|
|
1022
1026
|
});
|
|
1023
1027
|
result = withoutLocalOpenIAPAndroid(result);
|
|
1024
1028
|
if (includeExpoIap) {
|
|
1025
|
-
logOnce('📦 [expo-iap] Using OpenIAP
|
|
1029
|
+
logOnce('📦 [expo-iap] Using the published OpenIAP packages');
|
|
1026
1030
|
}
|
|
1027
1031
|
}
|
|
1028
1032
|
|