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

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

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

微信支付

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

  • 微信支付的流程相對簡單:
  1. 客戶端向業(yè)務(wù)后臺發(fā)起一個購買請求
  2. 業(yè)務(wù)后臺到微信服務(wù)端生成一個訂單
  3. 將微信訂單信息和自身系統(tǒng)所需的業(yè)務(wù)數(shù)據(jù)整合后返回給客戶端
  4. 客戶端拿到微信支付信息后旨怠,通過WeChatOpensdk調(diào)起支付
  5. 在頁面中訂閱支付回調(diào),接受支付信息并做業(yè)務(wù)流程處理(如:進(jìn)入支付結(jié)果頁等流程)
  6. 最后請求后臺蜈块,由后臺主動去微信系統(tǒng)中查詢最終支付狀態(tài)鉴腻,交回給前端顯示結(jié)果迷扇。
    (ps:后端在微信系統(tǒng)中主動查詢訂單轉(zhuǎn)態(tài)是同步的,可以馬上拿到支付結(jié)果)
  • 接下來講講開發(fā)爽哎,F(xiàn)lutter使用的是fluwx插件蜓席,簡單易用。在項目中课锌,我對微信支付進(jì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ù),即后臺返回的有關(guān)微信支付訂單的信息
  void wxPay(WxPayModel wxPayModel, {VoidCallback onWxPaying, VoidCallback onSuccess, Function(String data) onError}) async {
    // 跳轉(zhuǎn)微信支付前渺贤,告訴頁面進(jìn)入微信支付雏胃,頁面層可以做一些關(guān)閉加載框等的操作
    onWxPaying?.call();
    // 一些異常情況的處理
    if (!await fluwx.isWeChatInstalled) return onError?.call('請安裝微信完成支付或使用蘋果手機支付');
    if (wxPayModel.appId != Config.WX_APP_ID) return onError?.call('AppID不一致,請聯(lián)系管理員');
    // 此方法筆者沒有做單例志鞍,因此支付前嘗試注銷監(jiān)聽丑掺,避免重復(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)錯誤,請聯(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,
    );
  }
}

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

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

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


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

現(xiàn)在主流的做法是再支付頁面監(jiān)聽app的生命周期秘症,即由后臺切回前臺的時候,檢測下狀態(tài)讥珍,若還在支付中历极,直接進(jìn)入查詢結(jié)果頁面,由后臺去檢驗訂單衷佃,拿到結(jié)果顯示即可趟卸。(后臺主動查詢理論上還是存在微信服務(wù)端延時的問題,因此后臺進(jìn)行查詢的時候氏义,建議采取輪詢機制锄列,若是沒有支付成功的話,延時5秒后再確認(rè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)聽
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        {
          if (Platform.isAndroid && _isPaying) {
            _isPaying = false;
          // 監(jiān)聽到時安卓設(shè)備并且支付還在進(jìn)行中惯悠,程序員要根據(jù)業(yè)務(wù)做一下處理
            break;
        }
      default:
        break;
    }
    super.didChangeAppLifecycleState(state);
  }
}

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

可是肪获,iOS就不一樣了,簡直不要太惡心柒傻!

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

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

通過查看文檔和不斷調(diào)試,我們發(fā)現(xiàn):
① 支付錯誤的回調(diào)从撼,基本能馬上收到州弟;
② 上面流程說到IAP支付需要手動結(jié)束支付流程。同時iOS規(guī)定不能對同一個skuId重復(fù)發(fā)起多次支付的低零,只要當(dāng)前skuId有沒有final的支付婆翔,再次發(fā)起都會失敗掏婶;
② 無論支付成功或失敗啃奴,只要app沒有主動對當(dāng)前支付進(jìn)行final,每次啟動app后雄妥,app都會收到這個支付信息的通知最蕾;
③ 關(guān)于applicationUsername依溯,只有在支付完成馬上收到回調(diào)的情況下,回調(diào)信息才會有這個信息瘟则;到②中的情況黎炉,肯定不會返回applicationUsername;
④ 沒有applicationUsername就意味著訂單對不上醋拧,因此我們需要進(jìn)行湊單機制慷嗜。

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

③ 支付出錯可以final當(dāng)前支付,但是支付成功必須明確接收到iOS推送并且后臺核驗成功后盏袄,才能final忿峻,并刪除持久化存儲。


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

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

同時還需要做一些特殊處理:

  1. app剛啟動時已艰,若是持久化存儲不為空痊末,需要馬上啟動iOS支付訂閱監(jiān)聽,以接收iOS對未完成訂單的推送哩掺;
  2. 由于iOS限制了同一個skuId不能重復(fù)發(fā)起支付舌胶,因此持久化存儲中,一個skuId永遠(yuǎn)只會有一條記錄疮丛。因此當(dāng)app接收到的支付推送applicationUsername為null幔嫂,采取湊單機制辆它,原則是:通過skuId找到存儲記錄,拿到其對應(yīng)的OrderId履恩,發(fā)給后臺核驗锰茉。
  • 接下來進(jìn)入開發(fā),F(xiàn)utter采用的是in_app_purchase插件切心,官方提供的飒筑,支持google和IAP支付;而持久化存儲用的是flutter_secure_storage插件绽昏。
    依據(jù)上面的流程协屡,我同樣封裝了工具類。而且由于可能會在多個地方調(diào)用起監(jiān)聽全谤,所有必須是單例模式肤晓,代碼如下:
import 'dart:async';

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

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

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

  factory IOSPayment() {
    return _iosPayment;
  }

  IOSPayment.init();

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

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

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

  // 開始訂閱
  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ù)代碼略:有訂單開始支付,向外部發(fā)出通知认然,并記錄到緩存中补憾;  
          } else {
            if (purchaseDetails.status == PurchaseStatus.error) {
              print('>>> error');
               // 業(yè)務(wù)代碼略:有訂單支付錯誤,向外部發(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ù)單號,結(jié)束
                  finalTransaction(purchaseDetails);
                  BlocProvider.of<PaymentUtilsBloc>(Application.navigatorState.currentContext).add(IosPayFailureEvent(errorMessage: '支付出錯啦毕骡,請稍后再試~'));
                  return;
                }
                // 業(yè)務(wù)代碼略:支付成功削饵,向外部發(fā)出通知
                // 業(yè)務(wù)代碼略:開始核驗訂單,核驗結(jié)果由外部監(jiān)聽
                );
            }
          }
        });
      },
      onDone: () {
        stopListen();
      },
      onError: (error) {
        stopListen();
      },
    );
  }

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

  /// 啟動支付
  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ā)起,就寫入
        writeStorage(sku, orderId, 'pending');
      }
    }).catchError((err) {
      onError?.call('當(dāng)前商品您有未完成的交易橱赠,請等待iOS系統(tǒng)核驗后再次發(fā)起購買尤仍。');
      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();
    }
  }

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

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

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

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

頁面調(diào)用時箫津,建議啟用定時器狭姨,因為iOS回調(diào)不穩(wěn)定,所以監(jiān)聽到應(yīng)用回到前臺時開始30秒計時苏遥;30秒內(nèi)沒有收到支付回調(diào)饼拍,需要做對應(yīng)提示,這一塊也是存業(yè)務(wù)流程田炭,我這里不做代碼展示师抄。下面代碼是如何調(diào)用上面工具類的:

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

測試IAP中斷購買的測試

  • 這個測試是模擬用戶點擊購買協(xié)議的操作辆布,當(dāng)彈出系統(tǒng)協(xié)議彈框時,iOS會發(fā)出一個支付錯誤的消息茶鉴;這個時候我們的代碼會final這個支付,并且將持久化中對應(yīng)skuId的信息狀態(tài)改為cancel;
  • 然后用戶同意后娩缰,iOS會再發(fā)起一個同樣的不帶OdrerId(是的熙含,被弄丟了。割粮。盾碗。。)的訂單舀瓢,用戶支付成功后廷雅,我們的代碼就會收到支付成功的沒有OdrerId的推送,在持久化存儲中執(zhí)行湊單機制后氢伟,再發(fā)給后臺核銷榜轿。
    如何模擬這個流程呢?看看官方文檔描述,下面是譯文:
#### 設(shè)置測試

通過[登錄App Store Connect](https://help.apple.com/app-store-connect/#/devcd5016d31)啟用對Sandbox Apple ID的中斷購買朵锣,然后:

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

2.  選擇您要為其啟用中斷購買的Sandbox Apple ID。如果已啟用诬烹,則會在“中斷購買”列下看到一個復(fù)選標(biāo)記砸烦。

3.  在出現(xiàn)的對話框中,選擇“此測試儀的中斷購買”绞吁。

#### 開始測試

1.  在測試設(shè)備上幢痘,使用已中斷購買的沙盒Apple ID登錄。
2.  在您的應(yīng)用中家破,選擇“購買”或“訂閱”進(jìn)行應(yīng)用內(nèi)購買颜说。
3.  觀察到系統(tǒng)顯示付款單。
4.  在您的代碼中汰聋,驗證付款隊列在狀態(tài)下是否收到新交易门粪。
5.  在設(shè)備上,驗證付款單烹困。
6.  在您的代碼中玄妈,觀察到付款失敗。付款隊列在狀態(tài)中接收更新的交易。
7.  檢查您的代碼調(diào)用是否將其從隊列中刪除拟蜻。
8.  在設(shè)備上绎签,觀察到系統(tǒng)顯示“條款和條件”,從而中斷了購買(因為您已配置了沙盒環(huán)境)酝锅。
9.  在設(shè)備上辜御,點擊以同意條款和條件。
10.  在您的代碼中屈张,驗證付款隊列接收到的新交易處于與失敗交易相同且數(shù)量相同的狀態(tài).
11.  在您的代碼中擒权,驗證收據(jù)。檢查您的應(yīng)用是否提供了服務(wù)或產(chǎn)品阁谆,然后致電碳抄。
12.  在設(shè)備上,用戶應(yīng)觀察到購買成功场绿。

也就是說在Apple后臺把沙盒測試賬號設(shè)置為中斷即可剖效。但是無論我怎么同意,收到的還是支付失敗的訂閱焰盗。其實是因為文檔寫漏了璧尸,中斷后app彈出同意協(xié)議彈框,也就是上面第8步熬拒,這個時候必須在后臺把中斷測試關(guān)了爷光,然后再執(zhí)行第九步。(就是這么狗血澎粟,官方文檔不給力蛀序,網(wǎng)上也沒有任何資料,最后還是在官方論壇活烙,看到某個QA的評論才找到的靈感徐裸。。啸盏。這里也感謝公司大佬花了半天專門找這方面的資料重贺。)

寫在最后

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


但無論如何贫贝,這種方式對于開發(fā)者,確實是極度不友善的;另外稚晚,還有一種流程崇堵,app發(fā)起支付后,只要有回調(diào)就馬上final客燕,成功就發(fā)給后臺鸳劳,由后臺去執(zhí)行湊單機制,這種對于前端其實更合理也搓,畢竟數(shù)據(jù)存在客戶端永遠(yuǎn)是不夠安全赏廓,但是這樣app就有可能對同一個skuId瘋狂發(fā)起購買,后臺湊單時傍妒,就做不到一一對應(yīng)幔摸。有利有弊吧~~~

小弟班門弄斧,希望能一起學(xué)習(xí)進(jìn)步2贰<纫洹!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗦玖,一起剝皮案震驚了整個濱河市患雇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宇挫,老刑警劉巖苛吱,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異器瘪,居然都是意外死亡又谋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門娱局,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彰亥,“玉大人,你說我怎么就攤上這事衰齐∪握” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵耻涛,是天一觀的道長废酷。 經(jīng)常有香客問我,道長抹缕,這世上最難降的妖魔是什么澈蟆? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮卓研,結(jié)果婚禮上趴俘,老公的妹妹穿的比我還像新娘睹簇。我一直安慰自己,他們只是感情好寥闪,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布太惠。 她就那樣靜靜地躺著,像睡著了一般疲憋。 火紅的嫁衣襯著肌膚如雪凿渊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天缚柳,我揣著相機與錄音埃脏,去河邊找鬼。 笑死秋忙,一個胖子當(dāng)著我的面吹牛剂癌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翰绊,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼佩谷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了监嗜?” 一聲冷哼從身側(cè)響起谐檀,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎裁奇,沒想到半個月后桐猬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡刽肠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年溃肪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片音五。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡惫撰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出躺涝,到底是詐尸還是另有隱情厨钻,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布坚嗜,位于F島的核電站夯膀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏苍蔬。R本人自食惡果不足惜诱建,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碟绑。 院中可真熱鬧俺猿,春花似錦茎匠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抓狭。三九已至伯病,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間否过,已是汗流浹背午笛。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苗桂,地道東北人药磺。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像煤伟,于是被迫代替她去往敵國和親癌佩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容