Skip to content

Instantly share code, notes, and snippets.

@marcolabreu
Last active January 6, 2023 12:29
Show Gist options
  • Save marcolabreu/f9eb3bbf0fc9cb84701e1ae8bc6eb76b to your computer and use it in GitHub Desktop.
Save marcolabreu/f9eb3bbf0fc9cb84701e1ae8bc6eb76b to your computer and use it in GitHub Desktop.
Firebase function to validate Apple receipts
import {https} from 'firebase-functions';
import bent = require("bent");
/*
* Docs
* https://firebase.google.com/docs/functions/typescript
* https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
*/
/*
Apple validation status codes
[0]: { message: 'Active', valid: true, error: false },
[21000]: { message: 'App store could not read', valid: false, error: true },
[21002]: { message: 'Data was malformed', valid: false, error: true },
[21003]: { message: 'Receipt not authenticated', valid: false, error: true },
[21004]: { message: 'Shared secret does not match', valid: false, error: true },
[21005]: { message: 'Receipt server unavailable', valid: false, error: true },
[21006]: { message: 'Receipt valid but sub expired', valid: true, error: false },
[21007]: { message: 'Sandbox receipt sent to Production environment', valid: false, error: true, redirect: true },
[21008]: { message: 'Production receipt sent to Sandbox environment', valid: false, error: true },
*/
const sendToApple = async (data: string) => {
const body = JSON.stringify({ 'receipt-data': data })
const post = bent('POST', 'json', 200);
/* We try production following Apple recommendation */
try {
const productionResponse: bent.Json & { status?: number } = await post('https://buy.itunes.apple.com/verifyReceipt', body)
/* If status is 21007, we try sandbox */
if (productionResponse.status === 21007) {
return await post('https://sandbox.itunes.apple.com/verifyReceipt', body)
}
return productionResponse
} catch (e) {
console.error(e);
return { 'error': JSON.stringify(e) }
}
return productionResponse
}
/*
* Postman can be used to test the deployed function.
* Paste a receipt data encoded as base64, without quotes, to a POST request Body as raw Text.
* Check if Content-Type is set to text/plain in Header.
*
* * A sample receipt, working as of June 2020, can be obtained at
* https://stackoverflow.com/questions/33843281/apple-receipt-data-sample
*/
export const validateReceipt = https.onRequest(async (req, res) => {
const appleResponse = await sendToApple(req.body)
if (appleResponse.status === 0) {
console.info(`Receipt for app ${appleResponse.receipt.bundle_id} decrypted by Apple ${appleResponse.environment} server.`)
}
res.header("Content-Type", "application/json")
res.send(JSON.stringify(appleResponse))
})
{
"name": "functions",
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "tsc",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "10"
},
"main": "lib/index.js",
"dependencies": {
"@types/bent": "~7.0.2",
"bent": "~7.3.2",
"firebase-admin": "~8.10.0",
"firebase-functions": "~3.6.1"
},
"devDependencies": {
"tslint": "^5.12.0",
"typescript": "~3.8.0",
},
"private": true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment