Flutter iOS内购verifyPurchase验证问题求助
iOS内购Firebase云函数验证修复方案
问题代码中的核心错误
- onCall参数顺序错误:
functions.https.onCall的第一个参数是请求数据data,第二个才是上下文context,你把context放在第一个位置,导致无法获取前端传入的订单凭证,反而硬编码使用常量PURCHASE_TOKEN,这是调用失败的关键原因。 - JSON语法错误:
JSON.stringify的对象中,"password": PURCHASE_PASSWORD后缺少逗号,会导致JSON序列化失败。 - 响应处理错误:
response.body是可读流对象,不能直接返回给客户端,需要先解析为JSON或文本。 - 环境接口混用:测试阶段应该使用苹果沙箱验证接口
https://sandbox.itunes.apple.com/verifyReceipt,生产环境再切换到正式接口。 - 错误处理不完善:仅判断
response.ok不够,需要解析苹果返回的错误码,针对性处理。
修正后的云函数代码
import * as admin from "firebase-admin"; import * as functions from "firebase-functions"; import { CLOUD_REGION } from "./constants"; import fetch from 'node-fetch'; const functionsInstance = functions.region(CLOUD_REGION); admin.initializeApp(); // 替换为你的共享密钥(在App Store Connect的内购设置中获取) const APP_SHARED_SECRET = "你的共享密钥"; export const verifyPurchase = functionsInstance.https.onCall( async (data, context): Promise<any> => { // 验证用户身份(可选但推荐,防止恶意调用) if (!context.auth) { throw new functions.https.HttpsError( "unauthenticated", "用户未登录,无法验证订单" ); } const { receiptData } = data; if (!receiptData) { throw new functions.https.HttpsError( "invalid-argument", "缺少订单凭证数据" ); } // 根据环境选择验证接口,测试用沙箱,生产用正式 const verifyUrl = process.env.NODE_ENV === 'production' ? "https://buy.itunes.apple.com/verifyReceipt" : "https://sandbox.itunes.apple.com/verifyReceipt"; try { const response = await fetch(verifyUrl, { method: 'POST', body: JSON.stringify({ "receipt-data": receiptData, "password": APP_SHARED_SECRET, "exclude-old-transactions": true, }), headers: { 'Content-Type': 'application/json', }, }); const result = await response.json(); // 处理苹果返回的验证结果 switch (result.status) { case 0: // 验证成功,可在此添加用户权益发放逻辑 return { success: true, data: result }; case 21007: // 沙箱收据用了正式接口,自动重试沙箱接口 const sandboxResponse = await fetch("https://sandbox.itunes.apple.com/verifyReceipt", { method: 'POST', body: JSON.stringify({ "receipt-data": receiptData, "password": APP_SHARED_SECRET, "exclude-old-transactions": true, }), headers: { 'Content-Type': 'application/json' }, }); const sandboxResult = await sandboxResponse.json(); return sandboxResult.status === 0 ? { success: true, data: sandboxResult } : { success: false, error: sandboxResult }; default: return { success: false, error: result }; } } catch (error) { throw new functions.https.HttpsError( "internal", "验证请求失败", error instanceof Error ? error.message : String(error) ); } } );
关键优化点说明
- 参数传递:前端调用时需传入
receiptData(即iOS内购返回的收据字符串),云函数通过data.receiptData获取。 - 环境适配:自动根据Node环境切换验证接口,避免测试和生产环境混用导致的验证失败。
- 错误重试:针对状态码21007(沙箱收据用正式接口)自动重试沙箱接口,提升兼容性。
- 身份验证:添加用户登录验证,确保只有合法用户能调用验证接口。
- 结构化返回:统一返回
success标识和对应数据/错误信息,方便Flutter端处理。
Flutter端调用示例
final HttpsCallable callable = FirebaseFunctions.instance.httpsCallable('verifyPurchase'); try { final result = await callable.call({ 'receiptData': '你的iOS内购收据字符串', }); bool isSuccess = result.data['success'] as bool; if (isSuccess) { // 验证成功,处理用户权益 } else { // 验证失败,提示用户 } } catch (e) { // 处理调用错误 }
测试注意事项
- Postman测试
onCall函数时,需遵循Firebase调用规范:- 请求URL为云函数部署地址
- 请求体格式为
{"data": {"receiptData": "测试收据字符串"}} - 请求头携带Firebase ID Token(格式:
Authorization: Bearer <ID_TOKEN>),否则会触发身份验证错误。
- 测试用收据必须是苹果沙箱环境生成的,不能使用生产环境收据。
参考实现思路
- 优先阅读Firebase官方
onCall函数文档,明确参数结构和调用规范。 - 苹果官方收据验证文档详细说明了返回状态码的含义,需根据状态码做对应处理。
- 可在云函数中添加日志(
functions.logger.info()),方便排查部署后的调用问题。
内容的提问来源于stack exchange,提问作者Farhan Tariq
相关产品推荐
相关产品推荐

