You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Firebase订阅自动续费如何通知后端更新过期时间?

解决自动续订时更新Firebase过期日期的方案

一、客户端层面:监听订阅状态+恢复购买(覆盖用户打开APP的场景)

你的现有代码仅处理了首次购买逻辑,自动续订时若用户打开APP,purchaseStream会触发订阅更新事件,但需优化逻辑以适配续订场景:

  1. 不要手动计算过期时间,从订阅凭证解析官方日期
    手动加30天易出错(如不同订阅时长、时区差异),应从PurchaseDetails的交易凭证中提取平台提供的准确过期时间。
  2. APP启动时主动恢复购买状态
    用户未打开APP期间完成续订的,下次启动APP时调用restorePurchases拉取最新订阅状态,同步更新Firebase。

优化后的客户端代码:

@override
void initState() {
  super.initState();
  loadPlans();
  _listenToPurchaseUpdates();
  _restorePurchases(); // APP启动时恢复订阅状态
}

void _listenToPurchaseUpdates() {
  InAppPurchase.instance.purchaseStream.listen((purchaseDetailsList) {
    _handlePurchaseUpdates(purchaseDetailsList);
  });
}

Future<void> _restorePurchases() async {
  final bool isAvailable = await InAppPurchase.instance.isAvailable();
  if (isAvailable) {
    await InAppPurchase.instance.restorePurchases();
  }
}

void _handlePurchaseUpdates(List<PurchaseDetails> purchaseDetailsList) {
  for (var purchase in purchaseDetailsList) {
    if (purchase.status == PurchaseStatus.purchased || 
        purchase.status == PurchaseStatus.restored) {
      final receipt = purchase.transactionReceipt;
      if (receipt != null) {
        // 解析凭证获取过期时间(实际开发建议将凭证发送到Cloud Function验证后解析,避免客户端篡改)
        DateTime? expirationDate = _parseExpirationDateFromReceipt(receipt);
        if (expirationDate != null) {
          widget.userModel.expirationDate = expirationDate;
          // 同时存储交易ID/Token,方便服务器通知关联用户
          widget.userModel.subscriptionTransactionId = purchase.transactionId;
          FirebaseFirestore.instance
              .collection('users')
              .doc(widget.firebaseUser.uid)
              .update(widget.userModel.toMap());
        }
      }
    }
  }
}

// 示例:解析苹果订阅收据(Android需解析Google Play的JSON格式凭证)
DateTime? _parseExpirationDateFromReceipt(String receipt) {
  try {
    final decoded = base64Decode(receipt);
    final receiptJson = jsonDecode(utf8.decode(decoded));
    final latestReceiptInfo = receiptJson['latest_receipt_info']?.last;
    if (latestReceiptInfo != null) {
      final expiresDateMs = int.parse(latestReceiptInfo['expires_date_ms']);
      return DateTime.fromMillisecondsSinceEpoch(expiresDateMs);
    }
  } catch (e) {
    print('解析收据失败: $e');
  }
  return null;
}

二、服务器层面:接收应用商店实时通知(覆盖用户未打开APP的场景)

客户端方案存在局限性(用户不打开APP则无法更新),必须结合服务器端监听应用商店的续订通知,这是最可靠的自动更新方式:

1. 配置Firebase Cloud Function接收通知

创建Cloud Function,分别处理苹果App Store和谷歌Google Play的订阅事件:

处理苹果App Store服务器通知的Node.js示例

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

exports.handleAppleSubscriptionNotification = functions.https.onRequest(async (req, res) => {
  const notification = req.body;
  const { latest_receipt_info, notification_type } = notification;

  // 仅处理订阅成功或续订事件
  if (['SUBSCRIBED', 'RENEWED'].includes(notification_type) && latest_receipt_info) {
    const latestReceipt = latest_receipt_info[latest_receipt_info.length - 1];
    const transactionId = latestReceipt.transaction_id;
    
    // 通过交易ID关联到对应的用户文档
    const userSnapshot = await admin.firestore()
      .collection('users')
      .where('subscriptionTransactionId', '==', transactionId)
      .get();
    
    if (!userSnapshot.empty) {
      const userDoc = userSnapshot.docs[0];
      const expiresDateMs = parseInt(latestReceipt.expires_date_ms);
      const expirationDate = new Date(expiresDateMs);
      
      await userDoc.ref.update({ expirationDate: expirationDate.toISOString() });
    }
  }

  res.status(200).send('OK');
});

处理谷歌Google Play实时通知的Node.js示例

exports.handleGoogleSubscriptionNotification = functions.https.onRequest(async (req, res) => {
  const notification = req.body;
  const { subscriptionNotification, purchaseToken } = notification;

  // 4代表订阅续订事件
  if (subscriptionNotification && subscriptionNotification.notificationType === 4) {
    const { google } = require('googleapis');
    const androidpublisher = google.androidpublisher('v3');
    
    const auth = new google.auth.GoogleAuth({
      keyFile: 'service-account-key.json', // 你的Google Play服务账号密钥文件
      scopes: ['https://www.googleapis.com/auth/androidpublisher'],
    });
    
    const client = await auth.getClient();
    const packageName = 'com.your.app.package'; // 你的APP包名
    const subscriptionId = subscriptionNotification.subscriptionId;
    
    try {
      const response = await androidpublisher.purchases.subscriptions.get({
        auth: client,
        packageName: packageName,
        subscriptionId: subscriptionId,
        token: purchaseToken,
      });
      
      const { expiryTimeMillis } = response.data;
      const expirationDate = new Date(parseInt(expiryTimeMillis));
      
      // 通过purchaseToken关联到对应的用户文档
      const userSnapshot = await admin.firestore()
        .collection('users')
        .where('subscriptionPurchaseToken', '==', purchaseToken)
        .get();
      
      if (!userSnapshot.empty) {
        const userDoc = userSnapshot.docs[0];
        await userDoc.ref.update({ expirationDate: expirationDate.toISOString() });
      }
    } catch (e) {
      functions.logger.error('验证谷歌订阅失败:', e);
    }
  }

  res.status(200).send('OK');
});

2. 配置应用商店的通知URL

  • iOS: 在App Store Connect进入APP的「订阅」设置,配置「服务器通知URL」为Cloud Function的HTTPS地址,选择发送「所有事件」。
  • Android: 在Google Play Console进入「开发工具」>「API访问」,启用「实时开发者通知」,配置通知URL为对应Cloud Function的HTTPS地址。

三、关键注意事项

  • 客户端数据不可信: 所有订阅状态的最终验证必须在服务器端完成,避免客户端篡改数据。
  • 关联用户与交易: 客户端购买时需将transactionId(iOS)或purchaseToken(Android)存储到用户的Firestore文档中,方便服务器通知时匹配用户。
  • 处理异常事件: 需同时监听应用商店的订阅过期、取消等通知,及时更新Firebase中的用户状态。

内容的提问来源于stack exchange,提问作者shakky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 17:55:30