import { requireNativeModule, UnavailabilityError, type EventSubscription, } from 'expo-modules-core'; import {installedFromOnside} from './onside'; import type { BillingProgramAndroid, BillingProgramReportingDetailsAndroid, DeveloperBillingTypeAndroid, Mutation, MutationField, ProductQueryType, PurchaseInput, PurchaseOptions, PurchaseUpdatedListenerOptions, Query, QueryField, SubscriptionStatusIOS, } from './types'; import {getVegaIapModule, isVegaOS} from './vega'; type NativeIapModuleName = 'ExpoIapVega' | 'ExpoIapOnside' | 'ExpoIap'; const ONSIDE_MARKETPLACE_ID = 'com.onside.marketplace-app'; /** Members read from the raw module; every store module provides them. */ type NativeEventModule = { ERROR_CODES?: Record; HAS_EXPLICIT_STORE_PROVIDER?: boolean; addListener( eventName: string, listener: (payload: T) => void, ): EventSubscription | undefined; removeListener?(eventName: string, listener: (payload: T) => void): void; setPurchaseUpdatedListenerOptions?( options?: PurchaseUpdatedListenerOptions | null, ): Promise; }; type QueryFields = {[P in K]: QueryField

}; type MutationFields = {[P in K]: MutationField

}; /** * Native surface behind the default export. Results the wrappers decode stay * `unknown`; the rest match the generated operation signatures. */ export type ExpoIapNativeModule = NativeEventModule & QueryFields< | 'canPresentExternalPurchaseNoticeIOS' | 'currentEntitlementIOS' | 'getActiveSubscriptions' | 'getAppTransactionIOS' | 'verifyAppOwnership' | 'getBillingChoiceInfoAndroid' | 'getExternalPurchaseCustomLinkTokenIOS' | 'getPromotedProductIOS' | 'getReceiptDataIOS' | 'getTransactionJwsIOS' | 'hasActiveSubscriptions' | 'isEligibleForExternalPurchaseCustomLinkIOS' | 'isEligibleForIntroOfferIOS' | 'isTransactionVerifiedIOS' | 'latestTransactionIOS' > & MutationFields< | 'beginRefundRequestIOS' | 'clearTransactionIOS' | 'endConnection' | 'initConnection' | 'isBillingProgramAvailableAndroid' | 'launchExternalLinkAndroid' | 'openRedeemOfferCode' | 'presentExternalPurchaseLinkIOS' | 'presentExternalPurchaseNoticeSheetIOS' | 'showBillingProgramInformationDialogAndroid' | 'showExternalPurchaseCustomLinkNoticeIOS' | 'showInAppMessagesAndroid' | 'syncIOS' | 'verifyPurchase' | 'verifyPurchaseWithProvider' > & { USING_ONSIDE_SDK: boolean; USING_VEGA_SDK: boolean; fetchProducts(request: { skus: string[]; type: ProductQueryType; }): Promise; fetchProducts(type: ProductQueryType, skus: string[]): Promise; getAvailableItems( alsoPublishToEventListenerIOS: boolean, onlyIncludeActiveItemsIOS: boolean, ): Promise; getAvailableItems(options: PurchaseOptions): Promise; getAllTransactionsIOS(): Promise; getPendingTransactionsIOS(): Promise; showManageSubscriptionsIOS(): Promise; deepLinkToSubscriptionsIOS(): Promise; requestPurchase(request: object): Promise; finishTransaction( purchase: PurchaseInput, isConsumable: boolean | null, ): Promise; /** Internal: the once-per-install flag behind the first-purchase notice. */ claimFirstPurchaseNotice(): Promise; acknowledgePurchaseAndroid(purchaseToken: string): Promise; consumePurchaseAndroid(purchaseToken: string): Promise; createBillingProgramReportingDetailsAndroid( program: BillingProgramAndroid, developerBillingType: DeveloperBillingTypeAndroid | null, ): Promise; subscriptionStatusIOS(sku: string): Promise; requestReceiptRefreshIOS(): Promise; deepLinkToSubscriptionsAndroid?(options: { skuAndroid?: string; packageNameAndroid?: string; }): Promise | void; getStorefront?(): Promise | string; restorePurchases?(): Promise; }; type ResolvedNativeModule = { module: NativeEventModule; name: NativeIapModuleName; }; let cached: ResolvedNativeModule | null = null; let onsideModuleUnavailable = false; let explicitStoreProvider: boolean | undefined; function getResolved(): ResolvedNativeModule { function shouldUseOnsideModule(): boolean { if ( installedFromOnside === true || typeof installedFromOnside === 'string' ) { if (explicitStoreProvider === undefined) { try { const module = cached?.name === 'ExpoIap' ? cached.module : requireNativeModule('ExpoIap'); explicitStoreProvider = module.HAS_EXPLICIT_STORE_PROVIDER === true; } catch (error) { if (!isMissingModuleError(error, 'ExpoIap')) throw error; explicitStoreProvider = false; } } if (explicitStoreProvider) return false; } if (installedFromOnside === true) { return true; } if (typeof installedFromOnside !== 'string') { return false; } const normalized = installedFromOnside.trim().toLowerCase(); return normalized === 'true' || normalized === ONSIDE_MARKETPLACE_ID; } function getExpectedModuleName(): NativeIapModuleName { if (isVegaOS()) { return 'ExpoIapVega'; } return shouldUseOnsideModule() ? 'ExpoIapOnside' : 'ExpoIap'; } function resolveNativeModule(): ResolvedNativeModule { if (isVegaOS()) { const vegaModule = getVegaIapModule(); if (!vegaModule) { throw new UnavailabilityError( 'expo-iap', 'Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.', ); } return {module: vegaModule, name: 'ExpoIapVega'}; } if (shouldUseOnsideModule()) { if (onsideModuleUnavailable) { throw new UnavailabilityError( 'expo-iap', 'The Onside marketplace build does not contain ExpoIapOnside. Rebuild with ios.onside.enabled instead of routing purchases through Apple StoreKit.', ); } try { return { module: requireNativeModule('ExpoIapOnside'), name: 'ExpoIapOnside', }; } catch (error) { if (!isMissingModuleError(error, 'ExpoIapOnside')) { throw error; } onsideModuleUnavailable = true; throw new UnavailabilityError( 'expo-iap', 'The Onside marketplace build does not contain ExpoIapOnside. Rebuild with ios.onside.enabled instead of routing purchases through Apple StoreKit.', ); } } return { module: requireNativeModule('ExpoIap'), name: 'ExpoIap', }; } const expectedName = getExpectedModuleName(); if (!cached || cached.name !== expectedName) { cached = resolveNativeModule(); } return cached; } function isMissingModuleError(error: unknown, moduleName: string): boolean { if (error instanceof UnavailabilityError) { return true; } if (error instanceof Error) { return error.message.includes(`Cannot find native module '${moduleName}'`); } return false; } export const NATIVE_ERROR_CODES: Record = new Proxy( {} as Record, { get(target, prop) { if (typeof prop === 'symbol') return Reflect.get(target, prop); const errorCodes: Record = getResolved().module.ERROR_CODES || {}; return errorCodes[prop]; }, }, ); /** * Returns the raw native module, not the Proxy. Use it for addListener: a JSI * HostObject needs the real module as `this`, and a Proxy throws "native state * unsupported on Proxy" on New Architecture / Hermes. */ export function getNativeModule(): NativeEventModule { return getResolved().module; } // The Proxy forwards every member to the lazily resolved store module. export default new Proxy({} as ExpoIapNativeModule, { get(target, prop) { if (typeof prop === 'symbol') return Reflect.get(target, prop); const resolved = getResolved(); if (prop === 'USING_ONSIDE_SDK') { return resolved.name === 'ExpoIapOnside'; } if (prop === 'USING_VEGA_SDK') { return resolved.name === 'ExpoIapVega'; } const value: unknown = Reflect.get(resolved.module, prop); if (value !== undefined || resolved.name !== 'ExpoIapOnside') { return value; } return () => { throw new UnavailabilityError( 'expo-iap', `The Onside marketplace does not support ${String( prop, )}. The call was not routed through Apple StoreKit.`, ); }; }, });