in typescript/src/services/appleValidateReceipts.ts [163:287]
export function toSensiblePayloadFormat(
response: AppleValidationServerResponse,
receipt: string,
): AppleValidationResponse[] {
function expiryDate(
receiptServerInfo: AppleValidatedReceiptServerInfo,
): number {
if (receiptServerInfo.expires_date_ms) {
return Number.parseInt(receiptServerInfo.expires_date_ms);
} else if (receiptServerInfo.expires_date) {
return Number.parseInt(receiptServerInfo.expires_date);
} else {
throw new ProcessingError(
'Receipt has no expiry, this should have been filtered by now',
false,
);
}
}
function getReceiptInfo(): AppleValidatedReceiptServerInfo[] {
if (response.latest_receipt_info) {
if (Array.isArray(response.latest_receipt_info)) {
const latestReceipt = response.latest_receipt_info;
if (latestReceipt.length == 0) {
console.error(
`Invalid validation response, empty receipt info array`,
);
throw new ProcessingError(
`Invalid validation response, empty receipt info array`,
);
}
// only keep receipts that have an expiry date, those who don't aren't subscriptions or are pre 2011
const filteredLatestReceipt = latestReceipt.filter(
(receipt) => receipt.expires_date || receipt.expires_date_ms,
);
const deDupedReceipts = filteredLatestReceipt
.sort((r1, r2) => expiryDate(r1) - expiryDate(r2)) // most recent last
.reduce(
(acc: Record<string, AppleValidatedReceiptServerInfo>, current) => {
acc[current.original_transaction_id] = current;
return acc;
},
{},
);
return Object.values(deDupedReceipts);
} else {
return [response.latest_receipt_info];
}
} else {
if (response.latest_expired_receipt_info) {
return [response.latest_expired_receipt_info];
} else {
// should be impossible as this will be caught by checkResponseStatus
console.error(`No receipt info`);
throw new ProcessingError(
`Invalid validation response, no receipt info`,
);
}
}
}
type PendingRenewalInfoById = Record<string, PendingRenewalInfo>;
const pendingRenewalInfoArray = response.pending_renewal_info ?? [];
const pendingRenewalInfoById: PendingRenewalInfoById =
pendingRenewalInfoArray.reduce<PendingRenewalInfoById>((agg, value) => {
agg[value.original_transaction_id] = value;
return agg;
}, {});
return getReceiptInfo().map((receiptInfo) => {
const pendingRenewalInfo: PendingRenewalInfo =
pendingRenewalInfoById[receiptInfo.original_transaction_id];
const autoRenewStatus = pendingRenewalInfo
? pendingRenewalInfo.auto_renew_status === '1'
: response.auto_renew_status === 1;
// bundle_id is the documented field, however it's sometimes not valued, but instead there's the field bid
// which is valued but undocumented. Oh and also sometimes it's on the latest_receipt object, but sometimes
// it's on the receipt object. Hopefully this covers all the corner cases? who knows!
const bundleId =
receiptInfo.bundle_id ??
receiptInfo.bid ??
response.receipt?.bundle_id ??
response.receipt?.bid;
if (!bundleId) {
console.warn(
`Unable to identify the bundle id for the original transaction id ${receiptInfo.original_transaction_id}`,
);
}
const originalPurchaseDate = optionalMsToDate(
receiptInfo.original_purchase_date_ms,
);
if (originalPurchaseDate === null) {
console.error(
`Unable to parse the original purchase date ${receiptInfo.original_purchase_date_ms}`,
);
throw new ProcessingError(
`Unable to parse the original purchase date`,
false,
);
}
return {
isRetryable: response['is-retryable'] === true,
latestReceipt: response.latest_receipt ?? receipt,
latestReceiptInfo: {
bundleId: bundleId,
autoRenewStatus: autoRenewStatus,
cancellationDate: optionalMsToDate(receiptInfo.cancellation_date_ms),
expiresDate: new Date(expiryDate(receiptInfo)),
originalPurchaseDate: originalPurchaseDate,
originalTransactionId: receiptInfo.original_transaction_id,
productId: receiptInfo.product_id,
trialPeriod: receiptInfo.is_trial_period === 'true',
inIntroOfferPeriod: receiptInfo.is_in_intro_offer_period === 'true',
appAccountToken: receiptInfo.app_account_token,
},
originalResponse: response,
};
});
}