UNPKG

iap-apple

Version:

Integration with Apples InAppPurchases in Typescript, available for NodeJS environments.

236 lines (232 loc) 9.43 kB
// src/constants/shared/index.ts var RECEIPT_STATUS_ENUM = /* @__PURE__ */ ((RECEIPT_STATUS_ENUM2) => { RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["SUCCESS"] = 0] = "SUCCESS"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["VALID_NO_PURCHASE"] = 2] = "VALID_NO_PURCHASE"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["CANNOT_READ_JSON"] = 21e3] = "CANNOT_READ_JSON"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["DATA_MALFORMED"] = 21002] = "DATA_MALFORMED"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["RECEIPT_NOT_AUTHENTICATED"] = 21003] = "RECEIPT_NOT_AUTHENTICATED"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["SHARED_SECRET_DOESNT_MATCH"] = 21004] = "SHARED_SECRET_DOESNT_MATCH"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["SERVER_NOT_AVAILABLE"] = 21005] = "SERVER_NOT_AVAILABLE"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["SUBSCRIPTION_EXPIRED"] = 21006] = "SUBSCRIPTION_EXPIRED"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["TEST_ENV_RECEIPT_DETECTED"] = 21007] = "TEST_ENV_RECEIPT_DETECTED"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["PRODUCTION_ENV_RECEIPT_DETECTED"] = 21008] = "PRODUCTION_ENV_RECEIPT_DETECTED"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["INTERNAL_DATA_ACCESS_ERROR"] = 21009] = "INTERNAL_DATA_ACCESS_ERROR"; RECEIPT_STATUS_ENUM2[RECEIPT_STATUS_ENUM2["USER_ACCOUNT_DELETED"] = 21010] = "USER_ACCOUNT_DELETED"; return RECEIPT_STATUS_ENUM2; })(RECEIPT_STATUS_ENUM || {}); // src/constants/internal/index.ts var STATUS_TO_MESSAGE_MAP = { 21e3: "The App Store could not read the JSON object you provided.", 21002: "The data in the receipt-data property was malformed.", 21003: "The receipt could not be authenticated.", 21004: "The shared secret you provided does not match the shared secret on file for your account.", 21005: "The receipt server is not currently available.", 21006: "This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.", 21007: "This receipt is a sandbox receipt, but it was sent to the production service for verification.", 21008: "This receipt is a production receipt, but it was sent to the sandbox service for verification.", 21009: "Internal data access error. Try again later", 21010: "The user account cannot be found or has been deleted", 2: "The receipt is valid, but purchased nothing.", 0: "No error." }; var PROD_PATH = "https://buy.itunes.apple.com/verifyReceipt"; var SANDBOX_PATH = "https://sandbox.itunes.apple.com/verifyReceipt"; // src/lib/internal/index.ts function prefixMessage(message) { return `[iap-apple] ${message}`; } function isSubscriptionExpired(responseData) { const expirationDates = (responseData.latest_receipt_info || []).filter((lri) => lri.expires_date_ms).map((lri) => parseInt(lri.expires_date_ms, 10)); if (expirationDates.length === 0) { return false; } const latestExpirationDate = Math.max(...expirationDates); return latestExpirationDate < Date.now(); } async function verifyReceiptApple(url, content) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(content) }); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } function getPurchaseItem(item, purchase) { return { quantity: parseInt(item.quantity, 10), productId: item.product_id, transactionId: item.transaction_id, originalTransactionId: item.original_transaction_id, bundleId: purchase.receipt.bundle_id, appItemId: item.app_item_id, originalPurchaseDateMS: parseInt(item.original_purchase_date_ms, 10), purchaseDateMS: parseInt(item.purchase_date_ms, 10), cancellationDateMS: item.cancellation_date_ms ? parseInt(item.cancellation_date_ms, 10) : void 0, isTrialPeriod: item.is_trial_period === "true", expirationDateMS: item.expires_date_ms ? parseInt(item.expires_date_ms, 10) : void 0 }; } var verifyReceipt = async function({ logger, validationEndpoint, receiptData, appSharedSecret, excludeOldTransactions }) { return new Promise(async (resolve, reject) => { const content = { "receipt-data": receiptData, password: appSharedSecret, "exclude-old-transactions": excludeOldTransactions }; logger?.log(prefixMessage(`Validating against: ${validationEndpoint} endpoint`)); logger?.log(prefixMessage(`Validation data: ${JSON.stringify(content, null, 2)}`)); try { const data = await verifyReceiptApple(validationEndpoint, content); logger?.log(prefixMessage(`Endpoint ${validationEndpoint} response: ${JSON.stringify(data, null, 2)}`)); if (data.status !== 0 /* SUCCESS */ && data.status !== 21007 /* TEST_ENV_RECEIPT_DETECTED */ && data.status !== 21002 /* DATA_MALFORMED */) { if (data.status === 21006 /* SUBSCRIPTION_EXPIRED */ && !isSubscriptionExpired(data)) { logger?.log(prefixMessage("Valid receipt, but has been cancelled (not expired yet)")); resolve({ ...data, status: 0 /* SUCCESS */ }); return; } logger?.error(prefixMessage(`Endpoint ${validationEndpoint} failed: ${JSON.stringify(data, null, 2)}`)); reject({ rejectionMessage: STATUS_TO_MESSAGE_MAP[data.status] || "Unknown", data }); return; } if (data.status === 21007 /* TEST_ENV_RECEIPT_DETECTED */) { resolve(null); return; } if (data.status === 21002 /* DATA_MALFORMED */) { reject({ rejectionMessage: STATUS_TO_MESSAGE_MAP[data.status] || "Unknown", data }); return; } logger?.log(prefixMessage(`Validation successful: ${JSON.stringify(data, null, 2)}`)); resolve(data); } catch (error) { logger?.error(prefixMessage(`Endpoint ${validationEndpoint} failed: ${error}`)); reject({ rejectionMessage: error?.message, data: null }); } }); }; // src/lib/shared/index.ts async function verify(receipt, config) { const { appleExcludeOldTransactions, logger, test, appSharedSecret } = config; return new Promise(async (resolve, reject) => { let verifyReceiptResponse = null; try { if (!test) { verifyReceiptResponse = await verifyReceipt({ logger, validationEndpoint: PROD_PATH, receiptData: receipt, appSharedSecret, excludeOldTransactions: Boolean(appleExcludeOldTransactions) }); } if (!verifyReceiptResponse) { verifyReceiptResponse = await verifyReceipt({ logger, validationEndpoint: SANDBOX_PATH, receiptData: receipt, appSharedSecret, excludeOldTransactions: Boolean(appleExcludeOldTransactions) }); } if (!verifyReceiptResponse) { reject({ rejectionMessage: "Unable to validate receipt using appstore endpoints.", data: null }); return; } } catch (err) { reject(err); return; } if (verifyReceiptResponse.status === 0 /* SUCCESS */) { if (verifyReceiptResponse.receipt?.in_app && verifyReceiptResponse.receipt?.in_app?.length === 0) { reject({ rejectionMessage: "Detected valid receipt, however purchase list is empty", data: verifyReceiptResponse }); } resolve(verifyReceiptResponse); return; } reject({ rejectionMessage: STATUS_TO_MESSAGE_MAP[verifyReceiptResponse.status], data: verifyReceiptResponse }); }); } var isVerifiedReceipt = function(verifyReceiptResponse) { return verifyReceiptResponse?.status === 0 /* SUCCESS */; }; var isPurchasedItemExpired = function(purchasedItem) { if (!purchasedItem?.transactionId) { throw new Error("Detected invalid purchased item! Make sure object is defined and it has transaction id."); } if (purchasedItem.cancellationDateMS) { return true; } if (!purchasedItem.expirationDateMS) { return false; } return Date.now().valueOf() - purchasedItem.expirationDateMS >= 0; }; var isPurchasedItemCanceled = function(purchasedItem) { if (!purchasedItem?.transactionId) { throw new Error("Detected invalid purchased item! Make sure object is defined and it has transaction id."); } return Boolean(purchasedItem.cancellationDateMS); }; var getPurchasedItems = function(verifyReceiptResponse) { if (!verifyReceiptResponse?.receipt) { return []; } const data = []; let purchases = verifyReceiptResponse.receipt.in_app || []; const lri = verifyReceiptResponse.latest_receipt_info || verifyReceiptResponse.receipt.latest_receipt_info; if (Array.isArray(lri)) { purchases = purchases.concat(lri); } purchases.sort((a, b) => parseInt(b.purchase_date_ms, 10) - parseInt(a.purchase_date_ms, 10)); const transactionIds = {}; for (let i = 0; i < purchases.length; i++) { const item = purchases[i]; const tid = item.original_transaction_id; if (transactionIds[tid]) { continue; } data.push(getPurchaseItem(item, verifyReceiptResponse)); transactionIds[tid] = true; } return data; }; export { RECEIPT_STATUS_ENUM, getPurchasedItems, isPurchasedItemCanceled, isPurchasedItemExpired, isVerifiedReceipt, verify };