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

React Native IAP未收到App Store通知及Webhook不触发问题排查

问题排查

你的代码存在以下核心问题,导致App被杀后支付信息无法同步、Webhook不触发:

  • 交易处理依赖前台监听:仅通过purchaseUpdatedListener处理支付完成事件,用户支付后立即杀死App时,前端无法执行后端同步逻辑(validateIosPayment),也无法完成交易(finishTransaction),App Store会认为交易未闭环,不会触发Webhook。
  • 缺少启动时的交易恢复逻辑:iOS平台要求App每次启动必须恢复未完成的交易,否则已支付但未处理的交易无法被重新触发,后端无法获取支付凭证。
  • 交易完成时机不合理:当前在获取收据后立即调用finishTransaction,若后端同步失败,会导致交易已标记完成但用户未获得服务,且无法重试。
解决方案

1. 添加入口级交易恢复逻辑

在useIAP Hook中添加启动时恢复未完成交易的逻辑,确保App重启后能自动处理遗留的支付:

import { useEffect, useState } from 'react';
import { 
  getSubscriptions, 
  initConnection, 
  requestSubscription, 
  finishTransaction, 
  restorePurchases,
  purchaseUpdatedListener,
  purchaseErrorListener
} from 'react-native-iap';
import { useDispatch, useSelector } from 'react-redux';
import { cancelPaymentOrderIdAction } from '../Actions/CommonActions';
import { Platform } from 'react-native';
import { subscriptionPlans, PAYMENT_STATUS } from '../Constants';
import { validateIosPayment } from '../path-to-your-graphql-api';
import { onPaymentSucessFull } from '../path-to-your-success-handler';

const useIAP = () => {
  const [products, setProducts] = useState([]);
  const userData = useSelector(state => state.userReducer.userData);
  const [error, setError] = useState(null);
  const dispatch = useDispatch();

  // 统一处理交易完成逻辑:先验证后端,再标记交易完成
  const handlePurchaseCompletion = async (purchase) => {
    if (!purchase.transactionReceipt || !userData?.me?.id) return;

    try {
      const response = await validateIosPayment({
        variables: {
          input: {
            transactionId: purchase.transactionId,
            receiptId: purchase.transactionReceipt,
            userId: userData.me.id, // 显式传递用户ID给后端
            id: purchase.transactionId // 无createdOrderId时用交易ID关联
          },
        },
      });

      if (response?.data?.validateIosPayment?.status === PAYMENT_STATUS.SUCCESS) {
        dispatch(cancelPaymentOrderIdAction(null));
        onPaymentSucessFull();
        // 验证成功后再完成交易,确保App Store触发Webhook
        await finishTransaction({ 
          purchase, 
          isConsumable: purchase.productType === 'consumable' 
        });
      } else {
        console.log('后端验证失败,交易暂不完成');
      }
    } catch (err) {
      console.log('交易处理失败:', err);
      // 异常情况不完成交易,留待下次启动恢复重试
    }
  };

  // 恢复未完成的交易
  const restoreUnfinishedTransactions = async () => {
    try {
      const restoredPurchases = await restorePurchases({
        appAccountToken: userData?.me?.id // 传递用户ID,恢复时关联用户
      });
      for (const purchase of restoredPurchases) {
        await handlePurchaseCompletion(purchase);
      }
    } catch (err) {
      console.log('恢复交易失败:', err);
    }
  };

  useEffect(() => {
    const initIAP = async () => {
      await initConnection();
      // 启动时强制恢复交易
      await restoreUnfinishedTransactions();

      // 获取订阅商品列表
      if (Platform.OS === 'ios') {
        try {
          const productList = await getSubscriptions({ skus: subscriptionPlans });
          setProducts(productList);
        } catch (fetchErr) {
          console.log('获取商品列表失败:', fetchErr);
        }
      }

      // 全局监听支付更新
      const updateListener = purchaseUpdatedListener(purchase => {
        handlePurchaseCompletion(purchase);
      });
      const errorListener = purchaseErrorListener(err => {
        console.log('支付错误:', err);
        dispatch(cancelPaymentOrderIdAction(null));
      });

      return () => {
        updateListener.remove();
        errorListener.remove();
        // endConnection(); // react-native-iap v12+ 可用此方法断开连接
      };
    };

    initIAP();
  }, [userData?.me?.id]);

  const purchaseProductIOSProduct = async (productId, setLoadingData) => {
    try {
      await requestSubscription({ 
        sku: productId,
        appAccountToken: userData?.me?.id // 传递用户ID,Webhook会包含此字段
      });
    } catch (err) {
      dispatch(cancelPaymentOrderIdAction(null));
      setLoadingData(false);
      console.log('支付请求失败:', err);
    }
  };

  return { products, error, purchaseProductIOSProduct };
};

export default useIAP;

2. 简化屏幕组件逻辑

移除屏幕组件中的重复监听和处理逻辑,直接使用useIAP提供的能力:

import useIAP from '../hooks/useIAP';

const YourSubscriptionScreen = () => {
  const { products, purchaseProductIOSProduct } = useIAP();
  // ... 你的其他状态(如loadingData)

  // 调用支付的方法
  const handleSubscribe = async (productId) => {
    setLoadingData(true);
    await purchaseProductIOSProduct(productId, setLoadingData);
  };

  // ... 渲染商品列表等其他代码
};

3. 确保Webhook触发的关键配置

  • 强制传递appAccountToken:在requestSubscription和restorePurchases中都传入appAccountToken: userData?.me?.id,App Store Server Notifications的payload会包含此字段,后端可直接通过它关联用户,即使前端未触发同步,Webhook也能推送完整的支付+用户信息。
  • 后端配置验证:确保后端在App Store Connect中配置了正确的Webhook URL,勾选所有需要的事件(如DID_FINISH_TRANSACTION、DID_RENEW等),并开启签名验证。
  • 交易完成时机:必须在后端验证支付成功后再调用finishTransaction,确保App Store认为交易已闭环,触发Webhook。

4. 消耗型商品适配

若处理消耗型商品,只需在requestSubscription替换为requestPurchase,并在handlePurchaseCompletion中根据商品类型判断isConsumable:

// 购买消耗型商品的方法
const purchaseConsumable = async (productId, setLoadingData) => {
  try {
    await requestPurchase({ 
      sku: productId,
      appAccountToken: userData?.me?.id
    });
  } catch (err) {
    dispatch(cancelPaymentOrderIdAction(null));
    setLoadingData(false);
    console.log('消耗型商品购买失败:', err);
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 11:54:57