react-native-iap
Version:
React Native In-App Purchases module for iOS and Android using Nitro
417 lines (404 loc) • 15.6 kB
JavaScript
"use strict";
/**
* Type Bridge Utilities
*
* Converts the loose Nitro shapes coming from native into the strongly typed
* structures that our generated TypeScript types expect.
*/
import { RnIapConsole } from "./debug.js";
const PLATFORM_IOS = 'ios';
const PLATFORM_ANDROID = 'android';
const STORE_UNKNOWN = 'unknown';
const STORE_APPLE = 'apple';
const STORE_GOOGLE = 'google';
const STORE_HORIZON = 'horizon';
const PRODUCT_TYPE_SUBS = 'subs';
const PRODUCT_TYPE_IN_APP = 'in-app';
const PURCHASE_STATE_PENDING = 'pending';
const PURCHASE_STATE_PURCHASED = 'purchased';
const PURCHASE_STATE_UNKNOWN = 'unknown';
const PAYMENT_MODE_EMPTY = 'empty';
const PAYMENT_MODE_FREE_TRIAL = 'free-trial';
const PAYMENT_MODE_PAY_AS_YOU_GO = 'pay-as-you-go';
const PAYMENT_MODE_PAY_UP_FRONT = 'pay-up-front';
const SUBSCRIPTION_PERIOD_DAY = 'day';
const SUBSCRIPTION_PERIOD_WEEK = 'week';
const SUBSCRIPTION_PERIOD_MONTH = 'month';
const SUBSCRIPTION_PERIOD_YEAR = 'year';
const SUBSCRIPTION_PERIOD_EMPTY = 'empty';
const DEFAULT_JSON_REPR = '{}';
function normalizePlatform(value) {
return value?.toLowerCase() === PLATFORM_IOS ? PLATFORM_IOS : PLATFORM_ANDROID;
}
function normalizeStore(value) {
switch (value?.toLowerCase()) {
case 'apple':
return STORE_APPLE;
case 'google':
return STORE_GOOGLE;
case 'horizon':
return STORE_HORIZON;
default:
return STORE_UNKNOWN;
}
}
function normalizeProductType(value) {
return value?.toLowerCase() === PRODUCT_TYPE_SUBS ? PRODUCT_TYPE_SUBS : PRODUCT_TYPE_IN_APP;
}
function normalizeProductTypeIOS(value) {
switch ((value ?? '').toLowerCase()) {
case 'consumable':
return 'consumable';
case 'nonconsumable':
case 'non_consumable':
case 'non-consumable':
return 'non-consumable';
case 'autorenewablesubscription':
case 'auto_renewable_subscription':
case 'auto-renewable-subscription':
case 'autorenewable':
return 'auto-renewable-subscription';
case 'nonrenewingsubscription':
case 'non_renewing_subscription':
return 'non-renewing-subscription';
default:
if (value) {
RnIapConsole.warn(`[react-native-iap] Unknown iOS product type "${value}", defaulting to NonConsumable.`);
}
return 'non-consumable';
}
}
function normalizePaymentMode(value) {
switch ((value ?? '').toUpperCase()) {
case 'FREE_TRIAL':
case 'FREETRIAL':
case 'FREE-TRIAL':
return PAYMENT_MODE_FREE_TRIAL;
case 'PAY_AS_YOU_GO':
case 'PAYASYOUGO':
case 'PAY-AS-YOU-GO':
return PAYMENT_MODE_PAY_AS_YOU_GO;
case 'PAY_UP_FRONT':
case 'PAYUPFRONT':
case 'PAY-UP-FRONT':
return PAYMENT_MODE_PAY_UP_FRONT;
default:
return PAYMENT_MODE_EMPTY;
}
}
function normalizeSubscriptionPeriod(value) {
switch ((value ?? '').toUpperCase()) {
case 'DAY':
return SUBSCRIPTION_PERIOD_DAY;
case 'WEEK':
return SUBSCRIPTION_PERIOD_WEEK;
case 'MONTH':
return SUBSCRIPTION_PERIOD_MONTH;
case 'YEAR':
return SUBSCRIPTION_PERIOD_YEAR;
default:
return SUBSCRIPTION_PERIOD_EMPTY;
}
}
function normalizePurchaseState(state) {
if (typeof state === 'string') {
switch (state.toLowerCase()) {
case PURCHASE_STATE_PURCHASED:
case 'restored':
// Restored purchases are treated as purchased
return PURCHASE_STATE_PURCHASED;
case PURCHASE_STATE_PENDING:
case 'deferred':
// Deferred is treated as pending
return PURCHASE_STATE_PENDING;
default:
return PURCHASE_STATE_UNKNOWN;
}
}
if (typeof state === 'number') {
switch (state) {
case 1:
return PURCHASE_STATE_PURCHASED;
case 2:
return PURCHASE_STATE_PENDING;
default:
return PURCHASE_STATE_UNKNOWN;
}
}
return PURCHASE_STATE_UNKNOWN;
}
function toNullableString(value) {
if (value == null) return null;
return String(value);
}
function toNullableNumber(value) {
if (value == null) return null;
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
function toNullableBoolean(value) {
if (value == null) return null;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'string') return value.toLowerCase() === 'true';
return null;
}
function parseSubscriptionOffers(value) {
if (!value) return undefined;
// If it's already an array (from mocks), return it as-is
if (Array.isArray(value)) {
return value;
}
// Otherwise, try to parse it as JSON string
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
RnIapConsole.warn('Failed to parse subscriptionOfferDetailsAndroid:', error);
}
return undefined;
}
/**
* Convert NitroProduct (from native) to generated Product type
*/
export function convertNitroProductToProduct(nitroProduct) {
const platform = normalizePlatform(nitroProduct.platform);
const type = normalizeProductType(nitroProduct.type);
const base = {
id: nitroProduct.id,
title: nitroProduct.title,
description: nitroProduct.description,
type,
displayName: nitroProduct.displayName ?? null,
displayPrice: nitroProduct.displayPrice ?? '',
currency: nitroProduct.currency ?? '',
price: toNullableNumber(nitroProduct.price),
debugDescription: null,
platform
};
if (platform === PLATFORM_IOS) {
const iosProduct = {
...base,
displayNameIOS: nitroProduct.displayName ?? nitroProduct.title,
isFamilyShareableIOS: Boolean(nitroProduct.isFamilyShareableIOS ?? false),
jsonRepresentationIOS: nitroProduct.jsonRepresentationIOS ?? DEFAULT_JSON_REPR,
typeIOS: normalizeProductTypeIOS(nitroProduct.typeIOS),
subscriptionInfoIOS: undefined
};
iosProduct.introductoryPriceAsAmountIOS = toNullableString(nitroProduct.introductoryPriceAsAmountIOS);
iosProduct.introductoryPriceIOS = toNullableString(nitroProduct.introductoryPriceIOS);
iosProduct.introductoryPriceNumberOfPeriodsIOS = toNullableString(nitroProduct.introductoryPriceNumberOfPeriodsIOS);
iosProduct.introductoryPricePaymentModeIOS = normalizePaymentMode(nitroProduct.introductoryPricePaymentModeIOS);
iosProduct.introductoryPriceSubscriptionPeriodIOS = normalizeSubscriptionPeriod(nitroProduct.introductoryPriceSubscriptionPeriodIOS);
iosProduct.subscriptionPeriodNumberIOS = toNullableString(nitroProduct.subscriptionPeriodNumberIOS);
iosProduct.subscriptionPeriodUnitIOS = normalizeSubscriptionPeriod(nitroProduct.subscriptionPeriodUnitIOS);
// Parse discountsIOS from JSON string if present
if (nitroProduct.discountsIOS) {
try {
iosProduct.discountsIOS = JSON.parse(nitroProduct.discountsIOS);
} catch {
iosProduct.discountsIOS = null;
}
} else {
iosProduct.discountsIOS = null;
}
return iosProduct;
}
const androidProduct = {
...base,
nameAndroid: nitroProduct.nameAndroid ?? nitroProduct.title,
oneTimePurchaseOfferDetailsAndroid: nitroProduct.oneTimePurchaseOfferDetailsAndroid ?? null,
subscriptionOfferDetailsAndroid: parseSubscriptionOffers(nitroProduct.subscriptionOfferDetailsAndroid)
};
if (type === PRODUCT_TYPE_SUBS) {
if (!Array.isArray(androidProduct.subscriptionOfferDetailsAndroid)) {
androidProduct.subscriptionOfferDetailsAndroid = [];
}
}
return androidProduct;
}
/**
* Convert Product to ProductSubscription (type-safe casting helper)
*/
export function convertProductToProductSubscription(product) {
if (product.type !== PRODUCT_TYPE_SUBS) {
RnIapConsole.warn('Converting non-subscription product to ProductSubscription:', product.id);
}
const output = {
...product
};
if (output.platform === PLATFORM_ANDROID) {
if (!Array.isArray(output.subscriptionOfferDetailsAndroid)) {
output.subscriptionOfferDetailsAndroid = [];
}
}
return output;
}
/**
* Convert NitroPurchase (from native) to generated Purchase type
*/
export function convertNitroPurchaseToPurchase(nitroPurchase) {
const platform = normalizePlatform(nitroPurchase.platform);
let purchaseState = normalizePurchaseState(nitroPurchase.purchaseState ?? nitroPurchase.purchaseStateAndroid);
// Fallback for unknown purchase state
if (purchaseState === PURCHASE_STATE_UNKNOWN && nitroPurchase.purchaseStateAndroid != null) {
purchaseState = normalizePurchaseState(nitroPurchase.purchaseStateAndroid);
}
const store = normalizeStore(nitroPurchase.store);
if (platform === PLATFORM_IOS) {
const iosPurchase = {
id: nitroPurchase.id,
productId: nitroPurchase.productId,
transactionDate: nitroPurchase.transactionDate ?? Date.now(),
purchaseToken: nitroPurchase.purchaseToken ?? null,
platform,
store,
quantity: nitroPurchase.quantity ?? 1,
purchaseState,
isAutoRenewing: Boolean(nitroPurchase.isAutoRenewing),
// PurchaseIOS requires both id and transactionId (they are the same value)
transactionId: nitroPurchase.id,
quantityIOS: toNullableNumber(nitroPurchase.quantityIOS),
originalTransactionDateIOS: toNullableNumber(nitroPurchase.originalTransactionDateIOS),
originalTransactionIdentifierIOS: toNullableString(nitroPurchase.originalTransactionIdentifierIOS),
appAccountToken: toNullableString(nitroPurchase.appAccountToken),
appBundleIdIOS: toNullableString(nitroPurchase.appBundleIdIOS),
countryCodeIOS: toNullableString(nitroPurchase.countryCodeIOS),
currencyCodeIOS: toNullableString(nitroPurchase.currencyCodeIOS),
currencySymbolIOS: toNullableString(nitroPurchase.currencySymbolIOS),
environmentIOS: toNullableString(nitroPurchase.environmentIOS),
expirationDateIOS: toNullableNumber(nitroPurchase.expirationDateIOS),
isUpgradedIOS: toNullableBoolean(nitroPurchase.isUpgradedIOS),
offerIOS: nitroPurchase.offerIOS ? (() => {
try {
return JSON.parse(nitroPurchase.offerIOS);
} catch {
return null;
}
})() : null,
ownershipTypeIOS: toNullableString(nitroPurchase.ownershipTypeIOS),
reasonIOS: toNullableString(nitroPurchase.reasonIOS),
reasonStringRepresentationIOS: toNullableString(nitroPurchase.reasonStringRepresentationIOS),
revocationDateIOS: toNullableNumber(nitroPurchase.revocationDateIOS),
revocationReasonIOS: toNullableString(nitroPurchase.revocationReasonIOS),
storefrontCountryCodeIOS: toNullableString(nitroPurchase.storefrontCountryCodeIOS),
subscriptionGroupIdIOS: toNullableString(nitroPurchase.subscriptionGroupIdIOS),
transactionReasonIOS: toNullableString(nitroPurchase.transactionReasonIOS),
webOrderLineItemIdIOS: toNullableString(nitroPurchase.webOrderLineItemIdIOS),
renewalInfoIOS: nitroPurchase.renewalInfoIOS ? {
autoRenewPreference: toNullableString(nitroPurchase.renewalInfoIOS.autoRenewPreference),
expirationReason: toNullableString(nitroPurchase.renewalInfoIOS.expirationReason),
gracePeriodExpirationDate: toNullableNumber(nitroPurchase.renewalInfoIOS.gracePeriodExpirationDate),
isInBillingRetry: toNullableBoolean(nitroPurchase.renewalInfoIOS.isInBillingRetry),
jsonRepresentation: toNullableString(nitroPurchase.renewalInfoIOS.jsonRepresentation),
pendingUpgradeProductId: toNullableString(nitroPurchase.renewalInfoIOS.pendingUpgradeProductId),
priceIncreaseStatus: toNullableString(nitroPurchase.renewalInfoIOS.priceIncreaseStatus),
renewalDate: toNullableNumber(nitroPurchase.renewalInfoIOS.renewalDate),
renewalOfferId: toNullableString(nitroPurchase.renewalInfoIOS.renewalOfferId),
renewalOfferType: toNullableString(nitroPurchase.renewalInfoIOS.renewalOfferType),
willAutoRenew: nitroPurchase.renewalInfoIOS.willAutoRenew ?? false
} : null
};
return iosPurchase;
}
const androidPurchase = {
id: nitroPurchase.id,
productId: nitroPurchase.productId,
transactionDate: nitroPurchase.transactionDate ?? Date.now(),
purchaseToken: nitroPurchase.purchaseToken ?? nitroPurchase.purchaseTokenAndroid ?? null,
platform,
store,
quantity: nitroPurchase.quantity ?? 1,
purchaseState,
isAutoRenewing: Boolean(nitroPurchase.isAutoRenewing),
// PurchaseAndroid has optional transactionId (may differ from id/orderId)
transactionId: toNullableString(nitroPurchase.id),
autoRenewingAndroid: toNullableBoolean(nitroPurchase.autoRenewingAndroid ?? nitroPurchase.isAutoRenewing),
dataAndroid: toNullableString(nitroPurchase.dataAndroid),
signatureAndroid: toNullableString(nitroPurchase.signatureAndroid),
isAcknowledgedAndroid: toNullableBoolean(nitroPurchase.isAcknowledgedAndroid),
packageNameAndroid: toNullableString(nitroPurchase.packageNameAndroid),
obfuscatedAccountIdAndroid: toNullableString(nitroPurchase.obfuscatedAccountIdAndroid),
obfuscatedProfileIdAndroid: toNullableString(nitroPurchase.obfuscatedProfileIdAndroid),
developerPayloadAndroid: toNullableString(nitroPurchase.developerPayloadAndroid),
isSuspendedAndroid: toNullableBoolean(nitroPurchase.isSuspendedAndroid)
};
return androidPurchase;
}
/**
* Convert Nitro subscription status (iOS) to generated type
*/
export function convertNitroSubscriptionStatusToSubscriptionStatusIOS(nitro) {
return {
state: String(nitro.state ?? ''),
renewalInfo: nitro.renewalInfo ? {
autoRenewPreference: toNullableString(nitro.renewalInfo.autoRenewPreference),
jsonRepresentation: JSON.stringify(nitro.renewalInfo),
willAutoRenew: Boolean(nitro.renewalInfo.autoRenewStatus)
} : undefined
};
}
/**
* Validate that a NitroProduct has the expected shape
*/
export function validateNitroProduct(nitroProduct) {
if (!nitroProduct || typeof nitroProduct !== 'object') {
return false;
}
const required = ['id', 'title', 'description', 'type', 'platform'];
for (const field of required) {
if (!(field in nitroProduct) || nitroProduct[field] == null) {
RnIapConsole.error(`NitroProduct missing required field: ${field}`, nitroProduct);
return false;
}
}
return true;
}
/**
* Validate that a NitroPurchase has the expected shape
*/
export function validateNitroPurchase(nitroPurchase) {
if (!nitroPurchase || typeof nitroPurchase !== 'object') {
return false;
}
const required = ['id', 'productId', 'transactionDate', 'platform'];
for (const field of required) {
if (!(field in nitroPurchase) || nitroPurchase[field] == null) {
RnIapConsole.error(`NitroPurchase missing required field: ${field}`, nitroPurchase);
return false;
}
}
return true;
}
/**
* Development helper to check that type conversions stay valid
*/
export function checkTypeSynchronization() {
const issues = [];
try {
const testNitroProduct = {
id: 'test',
title: 'Test',
description: 'Test product',
type: 'inapp',
platform: PLATFORM_IOS,
displayPrice: '$1.00',
currency: 'USD',
price: 1,
introductoryPricePaymentModeIOS: PAYMENT_MODE_EMPTY
};
const converted = convertNitroProductToProduct(testNitroProduct);
if (!converted.id || !converted.title) {
issues.push('Type conversion failed');
}
} catch (error) {
issues.push(`Type conversion error: ${String(error)}`);
}
return {
isSync: issues.length === 0,
issues
};
}
//# sourceMappingURL=type-bridge.js.map