Flutter實(shí)現(xiàn)微信支付和iOS IAP支付

公司近期將收費(fèi)的功能排期了,由于項(xiàng)目做的是線上教育,提供的服務(wù)屬于虛擬物品。根據(jù)iOS官方的規(guī)定,虛擬物品交易只能使用iOS應(yīng)用內(nèi)支付,其他類似微信、支付寶官方都是明文規(guī)定不允許存在的。(注:虛擬物品才有此規(guī)定,且iOS官方收稅30%;而有實(shí)體物品交易的,官方允許在提供應(yīng)用內(nèi)支付的前提下,提供其他支付方式供用戶選擇。)

結(jié)合相關(guān)平臺(tái)規(guī)定,我們最終確定支付方式為:Android端使用微信支付,iOS使用IAP應(yīng)用內(nèi)支付。

微信支付

不得不說(shuō)我們這一代程序員是幸運(yùn)的,得益于國(guó)內(nèi)移動(dòng)支付的迅猛發(fā)展,微信支付的流程閉環(huán)比iOS完善了N倍(iOS的槽點(diǎn)一篇文章都寫(xiě)不完,稍后我再來(lái)吐);同時(shí)微信官方所提供的服務(wù),至少在國(guó)內(nèi)網(wǎng)絡(luò)中,可以認(rèn)定為是百分百可靠的。

  • 微信支付的流程相對(duì)簡(jiǎn)單:
  1. 客戶端向業(yè)務(wù)后臺(tái)發(fā)起一個(gè)購(gòu)買(mǎi)請(qǐng)求
  2. 業(yè)務(wù)后臺(tái)到微信服務(wù)端生成一個(gè)訂單
  3. 將微信訂單信息和自身系統(tǒng)所需的業(yè)務(wù)數(shù)據(jù)整合后返回給客戶端
  4. 客戶端拿到微信支付信息后,通過(guò)WeChatOpensdk調(diào)起支付
  5. 在頁(yè)面中訂閱支付回調(diào),接受支付信息并做業(yè)務(wù)流程處理(如:進(jìn)入支付結(jié)果頁(yè)等流程)
  6. 最后請(qǐng)求后臺(tái),由后臺(tái)主動(dòng)去微信系統(tǒng)中查詢最終支付狀態(tài),交回給前端顯示結(jié)果。
    (ps:后端在微信系統(tǒng)中主動(dòng)查詢訂單轉(zhuǎn)態(tài)是同步的,可以馬上拿到支付結(jié)果)
  • 接下來(lái)講講開(kāi)發(fā),F(xiàn)lutter使用的是fluwx插件,簡(jiǎn)單易用。在項(xiàng)目中,我對(duì)微信支付進(jìn)行了封裝,代碼見(jiàn)下:
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:fluwx/fluwx.dart' as fluwx;

class WechatPayment {
  StreamSubscription _wxPay;

  /// 關(guān)閉微信消息訂閱
  void wxSubscriptionClose() => _wxPay?.cancel();

  /// 調(diào)起微信支付面板 
  /// 這里的WxPayModel是業(yè)務(wù)層的數(shù)據(jù),即后臺(tái)返回的有關(guān)微信支付訂單的信息
  void wxPay(WxPayModel wxPayModel, {VoidCallback onWxPaying, VoidCallback onSuccess, Function(String data) onError}) async {
    // 跳轉(zhuǎn)微信支付前,告訴頁(yè)面進(jìn)入微信支付,頁(yè)面層可以做一些關(guān)閉加載框等的操作
    onWxPaying?.call();
    // 一些異常情況的處理
    if (!await fluwx.isWeChatInstalled) return onError?.call('請(qǐng)安裝微信完成支付或使用蘋(píng)果手機(jī)支付');
    if (wxPayModel.appId != Config.WX_APP_ID) return onError?.call('AppID不一致,請(qǐng)聯(lián)系管理員');
    // 此方法筆者沒(méi)有做單例,因此支付前嘗試注銷監(jiān)聽(tīng),避免重復(fù)回調(diào)
    _wxPay?.cancel();
    // 支付回調(diào)
    _wxPay = fluwx.weChatResponseEventHandler.listen((event) {
      _wxPay?.cancel();
      if (event is fluwx.WeChatPaymentResponse) {
        if (event.isSuccessful) {
          return onSuccess?.call();
        } else {
          return onError?.call(event.errCode == -1 ? '系統(tǒng)錯(cuò)誤,請(qǐng)聯(lián)系管理員' : '您取消了支付');
        }
      }
    });

    // 發(fā)起支付
    fluwx.payWithWeChat(
      appId: wxPayModel.appId,
      partnerId: wxPayModel.partnerId,
      prepayId: wxPayModel.prepayId,
      packageValue: wxPayModel.packageValue,
      nonceStr: wxPayModel.nonceStr,
      timeStamp: wxPayModel.timeStamp,
      sign: wxPayModel.sign,
      signType: wxPayModel.signType,
      extData: wxPayModel.extData,
    );
  }
}

頁(yè)面端是這樣調(diào)用的

WechatPayment paymentUtils = new WechatPayment();
paymentUtils.wxPay(
    state.model.wxPayModel,
    onError: (String err) {
        if (!mounted) return;
        // 微信支付錯(cuò)誤,設(shè)置支付狀態(tài)為false,彈框即可
         _isPaying = false;
         SchedulerBinding.instance.addPostFrameCallback((_) {
           CommonUtils.showToast(err, backgroundColor: Theme.of(context).errorColor);
         });
      }, 
      onSuccess:(){ 
        _isPaying = true;
      },
      onWxPaying: () {
        // 啟動(dòng)微信支付,設(shè)置支付狀態(tài)為true,關(guān)閉加載框
        _isPaying = true;
        SchedulerBinding.instance.addPostFrameCallback((_) {
          Navigator.pop(context);
        });
   },
);

但是需要注意,微信的回調(diào)是異步的,并且有很多種情況是接收不到回調(diào)的,以下是確定收不到會(huì)調(diào)的情況。


微信調(diào)起支付頁(yè)面時(shí),其實(shí)是跳轉(zhuǎn)到新的應(yīng)用,對(duì)于我們的應(yīng)用而言是觸發(fā)了前后臺(tái)切換的生命周期。
因此在檢測(cè)到應(yīng)用返回前臺(tái),并且支付狀態(tài)還在進(jìn)行中時(shí),可以證明是收不到微信的支付狀態(tài)回調(diào),需要特殊處理下。
收不到的情況有:
// ① 彈出支付框后使用系統(tǒng)返回鍵關(guān)閉;
// ② 進(jìn)入微信支付密碼框后不輸入使用系統(tǒng)導(dǎo)航切回app或者系統(tǒng)返回鍵返回;
// ③ 進(jìn)入微信后直接返回桌面再回到應(yīng)用;
// ④ 彈出支付框后鎖屏再開(kāi)屏;
// ⑤ 彈出支付款后下拉任務(wù)欄;
// ⑥ 輸入密碼成功后,直接返回桌面或者使用系統(tǒng)導(dǎo)航或者使用返回鍵返回app
// ⑦ 退出微信登錄,進(jìn)行支付后直接登錄微信,在登錄過(guò)程中回到app
// ⑧ 在系統(tǒng)應(yīng)用管理中雙開(kāi)微信后,調(diào)起支付后不點(diǎn)擊任一個(gè)微信端,而是點(diǎn)擊取消

現(xiàn)在主流的做法是再支付頁(yè)面監(jiān)聽(tīng)app的生命周期,即由后臺(tái)切回前臺(tái)的時(shí)候,檢測(cè)下?tīng)顟B(tài),若還在支付中,直接進(jìn)入查詢結(jié)果頁(yè)面,由后臺(tái)去檢驗(yàn)訂單,拿到結(jié)果顯示即可。(后臺(tái)主動(dòng)查詢理論上還是存在微信服務(wù)端延時(shí)的問(wèn)題,因此后臺(tái)進(jìn)行查詢的時(shí)候,建議采取輪詢機(jī)制,若是沒(méi)有支付成功的話,延時(shí)5秒后再確認(rèn)下更保險(xiǎn))

class _XXXPageState extends State<XXXPage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this); //添加觀察者
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this); //銷毀觀察者
    super.dispose();
  }

  /// 應(yīng)用狀態(tài)監(jiān)聽(tīng)
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        {
          if (Platform.isAndroid && _isPaying) {
            _isPaying = false;
          // 監(jiān)聽(tīng)到時(shí)安卓設(shè)備并且支付還在進(jìn)行中,程序員要根據(jù)業(yè)務(wù)做一下處理
            break;
        }
      default:
        break;
    }
    super.didChangeAppLifecycleState(state);
  }
}

到此,微信支付很愉快的解決了,以上代碼是抽象出來(lái)的工具類,可以直接使用;但是不涉及任何業(yè)務(wù)流程的開(kāi)發(fā),這個(gè)需要使用者自己去補(bǔ)充。
綜上,微信支付流程主線可簡(jiǎn)單粗暴總結(jié)為:服務(wù)端生成訂單 → 客戶端調(diào)起支付 → 客戶端通知服務(wù)端核驗(yàn)訂單 → 客戶端拿到最終結(jié)果 → 客戶端final支付。
整個(gè)過(guò)程形成閉環(huán),有理有據(jù),數(shù)據(jù)都由后端去操作安全合理。(最重點(diǎn)是前端工作量簡(jiǎn)直不要太少)。

可是,iOS就不一樣了,簡(jiǎn)直不要太惡心!

iOS IAP應(yīng)用內(nèi)支付

  • IAP,即in-app Purchase,蘋(píng)果推出的App內(nèi)購(gòu)買(mǎi)虛擬商品的方式,基于AppStore賬戶的支付方式。由于iOS整個(gè)體系都是基于自己的一套系統(tǒng)的(不像上面的微信支付,是第三方支付平臺(tái)),因此在開(kāi)發(fā)之前,我們需要到Apple開(kāi)發(fā)者中心完成以下步驟:
    1. 簽署協(xié)議和銀行業(yè)務(wù)
    2. 在后臺(tái)創(chuàng)建App內(nèi)購(gòu)買(mǎi)項(xiàng)目,這里所有的價(jià)格都是Apple規(guī)定好的,我們只有選擇的資格,沒(méi)辦法自定價(jià)格。創(chuàng)建完成后,每個(gè)項(xiàng)目會(huì)有sku和productId
    3. 添加沙盒測(cè)試員Apple
    以上步驟參考內(nèi)容引自站內(nèi)大神:Geniune
  • 支付流程:應(yīng)用通過(guò)sku向服務(wù)端獲取商品列表 → 列表中取出對(duì)應(yīng)產(chǎn)品請(qǐng)求支付 → 進(jìn)入appStore支付 → 頁(yè)面監(jiān)聽(tīng)支付回調(diào)拿到驗(yàn)證票據(jù) → 業(yè)務(wù)后臺(tái)拿到應(yīng)用接收到的票據(jù)后去Apple官網(wǎng)進(jìn)行校驗(yàn)即可。
    流程很簡(jiǎn)單,簡(jiǎn)單到幾乎不用跟業(yè)務(wù)后臺(tái)打交代,但是坑卻隨之而來(lái):
① 支付數(shù)據(jù)完全依賴前端應(yīng)用,很難跟業(yè)務(wù)后臺(tái)的訂單系統(tǒng)一一對(duì)應(yīng);
② 針對(duì)①的問(wèn)題,IAP支付支持傳遞skPayment對(duì)象,里面的applicationUsername經(jīng)常用來(lái)保存系統(tǒng)的OrderId;
但是應(yīng)用支付成功后收到的回調(diào)中,applicationUsername卻偶爾會(huì)出現(xiàn)為null的情況,沒(méi)有了對(duì)應(yīng)關(guān)系,就沒(méi)辦法核銷業(yè)務(wù)系統(tǒng)中的訂單從而為用戶充值;
③ iOS支付回調(diào)非常不穩(wěn)定,有時(shí)延遲嚴(yán)重;且沒(méi)有任何注定查詢的方法;
④ iOS應(yīng)用內(nèi)支付有很多異常情況要處理,最常見(jiàn)的就是沒(méi)有登錄、沒(méi)有同意最新的iOS支付協(xié)議等,都會(huì)發(fā)送給app支付失敗的回調(diào);
但是當(dāng)用戶登錄或是同意后,iOS系統(tǒng)又會(huì)觸發(fā)新的支付,導(dǎo)致舊的附帶業(yè)務(wù)訂單號(hào)的支付無(wú)效,莫名又多出一個(gè)沒(méi)有訂單號(hào)的新支付;
⑤ 國(guó)內(nèi)網(wǎng)上資料極度缺乏,基本都是19年以前的,F(xiàn)lutter的文章更是少的可憐,可參考性不強(qiáng)。
⑥ 測(cè)試文檔對(duì)于中斷購(gòu)買(mǎi)的測(cè)試流程有巨坑,后面菜單一定不要錯(cuò)過(guò)~

通過(guò)查看文檔和不斷調(diào)試,我們發(fā)現(xiàn):
① 支付錯(cuò)誤的回調(diào),基本能馬上收到;
② 上面流程說(shuō)到IAP支付需要手動(dòng)結(jié)束支付流程。同時(shí)iOS規(guī)定不能對(duì)同一個(gè)skuId重復(fù)發(fā)起多次支付的,只要當(dāng)前skuId有沒(méi)有final的支付,再次發(fā)起都會(huì)失??;
② 無(wú)論支付成功或失敗,只要app沒(méi)有主動(dòng)對(duì)當(dāng)前支付進(jìn)行final,每次啟動(dòng)app后,app都會(huì)收到這個(gè)支付信息的通知;
③ 關(guān)于applicationUsername,只有在支付完成馬上收到回調(diào)的情況下,回調(diào)信息才會(huì)有這個(gè)信息;到②中的情況,肯定不會(huì)返回applicationUsername;
④ 沒(méi)有applicationUsername就意味著訂單對(duì)不上,因此我們需要進(jìn)行湊單機(jī)制。

綜上,我們對(duì)異常處理有了確定方案:
① app發(fā)起支付后,需要將業(yè)務(wù)OrderId和skuId進(jìn)行持久化存儲(chǔ)(即卸載應(yīng)用都不會(huì)刪除的數(shù)據(jù));
②只要持久化存儲(chǔ)不為空,啟動(dòng)app就需要馬上啟動(dòng)監(jiān)聽(tīng),以接收iOS系統(tǒng)的訂單推送;

③ 支付出錯(cuò)可以final當(dāng)前支付,但是支付成功必須明確接收到iOS推送并且后臺(tái)核驗(yàn)成功后,才能final,并刪除持久化存儲(chǔ)。


最終,結(jié)合到業(yè)務(wù)系統(tǒng)和特殊情況的處理后,支付流程應(yīng)該如下:

  1. 業(yè)務(wù)后臺(tái)返回商品列表時(shí),需要附加返回對(duì)應(yīng)的skuId
  2. app通過(guò)skuId請(qǐng)appStore請(qǐng)求商品信息
  3. app對(duì)商品發(fā)起支付,并將業(yè)務(wù)訂單號(hào)存儲(chǔ)在applicationUsername中,發(fā)起成功寫(xiě)入持久化存儲(chǔ),狀態(tài)為pending
  4. 接收iOS系統(tǒng)回調(diào),失敗馬上final支付,更改對(duì)應(yīng)持久化存儲(chǔ)狀態(tài)為cancle;成功拿到票據(jù)和業(yè)務(wù)OrderId發(fā)送給后臺(tái)
  5. 后臺(tái)調(diào)取Apple服務(wù)端接口,傳入票據(jù)(票據(jù)其實(shí)儲(chǔ)存著最新的時(shí)間,appStore用戶信息等)
  6. 后臺(tái)獲取到Apple返回的當(dāng)前appStore用戶所有支付的前100條記錄,拿到productId到數(shù)據(jù)庫(kù)有中匹配該用戶是否有未核銷的訂單,并對(duì)應(yīng)修改業(yè)務(wù)訂單狀態(tài)
  7. app確認(rèn)核銷成功,final支付,并且刪除持久化存儲(chǔ)

同時(shí)還需要做一些特殊處理:

  1. app剛啟動(dòng)時(shí),若是持久化存儲(chǔ)不為空,需要馬上啟動(dòng)iOS支付訂閱監(jiān)聽(tīng),以接收iOS對(duì)未完成訂單的推送;
  2. 由于iOS限制了同一個(gè)skuId不能重復(fù)發(fā)起支付,因此持久化存儲(chǔ)中,一個(gè)skuId永遠(yuǎn)只會(huì)有一條記錄。因此當(dāng)app接收到的支付推送applicationUsername為null,采取湊單機(jī)制,原則是:通過(guò)skuId找到存儲(chǔ)記錄,拿到其對(duì)應(yīng)的OrderId,發(fā)給后臺(tái)核驗(yàn)。
  • 接下來(lái)進(jìn)入開(kāi)發(fā),F(xiàn)utter采用的是in_app_purchase插件,官方提供的,支持google和IAP支付;而持久化存儲(chǔ)用的是flutter_secure_storage插件。
    依據(jù)上面的流程,我同樣封裝了工具類。而且由于可能會(huì)在多個(gè)地方調(diào)用起監(jiān)聽(tīng),所有必須是單例模式,代碼如下:
import 'dart:async';

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

// iOS支付單一實(shí)例
final iOSPayment = IOSPayment();

class IOSPayment {
  /// 單例模式
  static final IOSPayment _iosPayment = IOSPayment.init();

  factory IOSPayment() {
    return _iosPayment;
  }

  IOSPayment.init();

  // 應(yīng)用內(nèi)支付實(shí)例
  InAppPurchaseConnection purchaseConnection = InAppPurchaseConnection.instance;
  FlutterSecureStorage storage = new FlutterSecureStorage();

  // iOS訂閱監(jiān)聽(tīng)
  StreamSubscription<List<PurchaseDetails>> subscription;

  /// 判斷是否可以使用支付
  Future<bool> isAvailable() async => await purchaseConnection.isAvailable();

  // 開(kāi)始訂閱
  void startSubscription() async {
    if (subscription != null) return;
    print('>>> start subscription');
    // 支付消息訂閱
    Stream purchaseUpdates = purchaseConnection.purchaseUpdatedStream;
    subscription = purchaseUpdates.listen(
      (purchaseDetailsList) {
        purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
          if (purchaseDetails.status == PurchaseStatus.pending) {
            print('>>> pending');
            // 業(yè)務(wù)代碼略:有訂單開(kāi)始支付,向外部發(fā)出通知,并記錄到緩存中;  
          } else {
            if (purchaseDetails.status == PurchaseStatus.error) {
              print('>>> error');
               // 業(yè)務(wù)代碼略:有訂單支付錯(cuò)誤,向外部發(fā)出通知
              // 下面是刪除
              String value = await storage.read(key: purchaseDetails.productID);
              String orderId = value.split('¥')[0];
              writeStorage(purchaseDetails.productID, orderId, 'cancel');
              finalTransaction(purchaseDetails);
            } else if (purchaseDetails.status == PurchaseStatus.purchased) {
              print('>>> purchased');
              String orderId = purchaseDetails.skPaymentTransaction.payment.applicationUsername;
              if (orderId == null || orderId.isEmpty) {
                // 如果applicationUsername為空,執(zhí)行湊單
                orderId = await foundRecentOrder(purchaseDetails.productID);
              }
              if (orderId.isEmpty) {
                  // 湊單失敗,找不到業(yè)務(wù)單號(hào),結(jié)束
                  finalTransaction(purchaseDetails);
                  BlocProvider.of<PaymentUtilsBloc>(Application.navigatorState.currentContext).add(IosPayFailureEvent(errorMessage: '支付出錯(cuò)啦,請(qǐng)稍后再試~'));
                  return;
                }
                // 業(yè)務(wù)代碼略:支付成功,向外部發(fā)出通知
                // 業(yè)務(wù)代碼略:開(kāi)始核驗(yàn)訂單,核驗(yàn)結(jié)果由外部監(jiān)聽(tīng)
                );
            }
          }
        });
      },
      onDone: () {
        stopListen();
      },
      onError: (error) {
        stopListen();
      },
    );
  }

  /// 檢查sku是否有對(duì)應(yīng)商品
  Future<bool> checkProductBySku(String sku, {Function(String err) onError}) async {
    if (!await isAvailable()) {
      onError?.call('無(wú)法連接AppStore,請(qǐng)稍后再試');
      return false;
    }
    ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet());
    if (appStoreProducts.productDetails.length == 0) {
      onError?.call('沒(méi)有找到相關(guān)產(chǎn)品,請(qǐng)聯(lián)系管理員');
      return false;
    }
    return true;
  }

  /// 啟動(dòng)支付
  void iosPay(String sku, String orderId, {Function(String err) onError}) async {
    // 獲取商品列表
    ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet());
    // 發(fā)起支付
    purchaseConnection
        .buyNonConsumable(
      purchaseParam: PurchaseParam(
        productDetails: appStoreProducts.productDetails.first,
        applicationUserName: orderId,
      ),
    )
        .then((value) {
      if (value) {
        // 只要能發(fā)起,就寫(xiě)入
        writeStorage(sku, orderId, 'pending');
      }
    }).catchError((err) {
      onError?.call('當(dāng)前商品您有未完成的交易,請(qǐng)等待iOS系統(tǒng)核驗(yàn)后再次發(fā)起購(gòu)買(mǎi)。');
      print(err);
    });
  }

  writeStorage(String key, String value, String status) {
    storage.write(key: key, value: '$value¥$status');
  }

  // 關(guān)閉交易
  void finalTransaction(PurchaseDetails purchaseDetails) async {
    await purchaseConnection.completePurchase(purchaseDetails);
    // 每完成一張訂單進(jìn)行緩存的清除
    if (!await checkStorage()) {
      stopListen();
    }
  }

  // 湊單機(jī)制
  Future<String> foundRecentOrder(String sku) async {
    String orderId = '';
    String values = await storage.read(key: sku);

    if (values != null) {
      orderId = values.split('¥')[0];
    }
    return orderId;
  }

  // 校驗(yàn)是否還有緩存
  Future<bool> checkStorage() async {
    Map<String, String> remainingValues = await storage.readAll();
    return remainingValues.isNotEmpty;
  }

  // 關(guān)閉監(jiān)聽(tīng)
  stopListen() async {
    subscription?.cancel();
    subscription = null;
  }
}

頁(yè)面調(diào)用時(shí),建議啟用定時(shí)器,因?yàn)閕OS回調(diào)不穩(wěn)定,所以監(jiān)聽(tīng)到應(yīng)用回到前臺(tái)時(shí)開(kāi)始30秒計(jì)時(shí);30秒內(nèi)沒(méi)有收到支付回調(diào),需要做對(duì)應(yīng)提示,這一塊也是存業(yè)務(wù)流程,我這里不做代碼展示。下面代碼是如何調(diào)用上面工具類的:

iOSPayment.startSubscription();
iOSPayment.iosPay(
    state.skuId,
    state.model.orderId,
    onError: (String err) {
      if (!mounted) return;
      // 支付遇到錯(cuò)誤,馬上停止定時(shí)器,并且關(guān)掉彈框
    },
 );
// 應(yīng)用啟動(dòng)時(shí)
if (Platform.isIOS && await iOSPayment.checkStorage()) {
    // 啟動(dòng)訂閱:支付緩存未清除完畢、機(jī)型可使用應(yīng)用內(nèi)支付
    iOSPayment.startSubscription(needDelayed: true);
  }

測(cè)試IAP中斷購(gòu)買(mǎi)的測(cè)試

  • 這個(gè)測(cè)試是模擬用戶點(diǎn)擊購(gòu)買(mǎi)協(xié)議的操作,當(dāng)彈出系統(tǒng)協(xié)議彈框時(shí),iOS會(huì)發(fā)出一個(gè)支付錯(cuò)誤的消息;這個(gè)時(shí)候我們的代碼會(huì)final這個(gè)支付,并且將持久化中對(duì)應(yīng)skuId的信息狀態(tài)改為cancel;
  • 然后用戶同意后,iOS會(huì)再發(fā)起一個(gè)同樣的不帶OdrerId(是的,被弄丟了。。。。)的訂單,用戶支付成功后,我們的代碼就會(huì)收到支付成功的沒(méi)有OdrerId的推送,在持久化存儲(chǔ)中執(zhí)行湊單機(jī)制后,再發(fā)給后臺(tái)核銷。
    如何模擬這個(gè)流程呢?看看官方文檔描述,下面是譯文:
#### 設(shè)置測(cè)試

通過(guò)[登錄App Store Connect](https://help.apple.com/app-store-connect/#/devcd5016d31)啟用對(duì)Sandbox Apple ID的中斷購(gòu)買(mǎi),然后:

1.  在“用戶和訪問(wèn)”中,單擊邊欄中沙箱下的“測(cè)試器”。在右側(cè),您可以查看您的Sandbox Apple ID。

2.  選擇您要為其啟用中斷購(gòu)買(mǎi)的Sandbox Apple ID。如果已啟用,則會(huì)在“中斷購(gòu)買(mǎi)”列下看到一個(gè)復(fù)選標(biāo)記。

3.  在出現(xiàn)的對(duì)話框中,選擇“此測(cè)試儀的中斷購(gòu)買(mǎi)”。

#### 開(kāi)始測(cè)試

1.  在測(cè)試設(shè)備上,使用已中斷購(gòu)買(mǎi)的沙盒Apple ID登錄。
2.  在您的應(yīng)用中,選擇“購(gòu)買(mǎi)”或“訂閱”進(jìn)行應(yīng)用內(nèi)購(gòu)買(mǎi)。
3.  觀察到系統(tǒng)顯示付款單。
4.  在您的代碼中,驗(yàn)證付款隊(duì)列在狀態(tài)下是否收到新交易。
5.  在設(shè)備上,驗(yàn)證付款單。
6.  在您的代碼中,觀察到付款失敗。付款隊(duì)列在狀態(tài)中接收更新的交易。
7.  檢查您的代碼調(diào)用是否將其從隊(duì)列中刪除。
8.  在設(shè)備上,觀察到系統(tǒng)顯示“條款和條件”,從而中斷了購(gòu)買(mǎi)(因?yàn)槟雅渲昧松澈协h(huán)境)。
9.  在設(shè)備上,點(diǎn)擊以同意條款和條件。
10.  在您的代碼中,驗(yàn)證付款隊(duì)列接收到的新交易處于與失敗交易相同且數(shù)量相同的狀態(tài).
11.  在您的代碼中,驗(yàn)證收據(jù)。檢查您的應(yīng)用是否提供了服務(wù)或產(chǎn)品,然后致電。
12.  在設(shè)備上,用戶應(yīng)觀察到購(gòu)買(mǎi)成功。

也就是說(shuō)在Apple后臺(tái)把沙盒測(cè)試賬號(hào)設(shè)置為中斷即可。但是無(wú)論我怎么同意,收到的還是支付失敗的訂閱。其實(shí)是因?yàn)槲臋n寫(xiě)漏了,中斷后app彈出同意協(xié)議彈框,也就是上面第8步,這個(gè)時(shí)候必須在后臺(tái)把中斷測(cè)試關(guān)了,然后再執(zhí)行第九步。(就是這么狗血,官方文檔不給力,網(wǎng)上也沒(méi)有任何資料,最后還是在官方論壇,看到某個(gè)QA的評(píng)論才找到的靈感。。。這里也感謝公司大佬花了半天專門(mén)找這方面的資料。)

寫(xiě)在最后

感謝大家孜孜不倦看到最后,這篇長(zhǎng)文希望能幫助開(kāi)發(fā)支付的小伙伴少踩一些坑。
IAP的支付確實(shí)是很坑,但如果站在iOS開(kāi)發(fā)者的角度來(lái)看。其實(shí)也能理解:他們是做手機(jī)系統(tǒng)的,他們能保證系統(tǒng)內(nèi)部的所有支付流程,根本不care開(kāi)發(fā)者的業(yè)務(wù)邏輯。


但無(wú)論如何,這種方式對(duì)于開(kāi)發(fā)者,確實(shí)是極度不友善的;另外,還有一種流程,app發(fā)起支付后,只要有回調(diào)就馬上final,成功就發(fā)給后臺(tái),由后臺(tái)去執(zhí)行湊單機(jī)制,這種對(duì)于前端其實(shí)更合理,畢竟數(shù)據(jù)存在客戶端永遠(yuǎn)是不夠安全,但是這樣app就有可能對(duì)同一個(gè)skuId瘋狂發(fā)起購(gòu)買(mǎi),后臺(tái)湊單時(shí),就做不到一一對(duì)應(yīng)。有利有弊吧~~~

小弟班門(mén)弄斧,希望能一起學(xué)習(xí)進(jìn)步?。。?/strong>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容