Apple和Google內(nèi)購之消耗型

一、有關(guān)Apple內(nèi)購

1. SKStorefront:包含App Store店面位置和唯一標(biāo)識(shí)符的對象。

您可以通過 App Store Connect創(chuàng)建的應(yīng)用內(nèi)產(chǎn)品可在每個(gè)擁有App Store的地區(qū)銷售。您可以使用店面信息來確定客戶所在的地區(qū),并提供適合該地區(qū)的應(yīng)用內(nèi)產(chǎn)品。您必須維護(hù)自己的產(chǎn)品標(biāo)識(shí)符列表以及要在其中提供它們的店面。

2.SKPaymentQueueDelegate

<1>shouldContinueTransaction: 當(dāng)設(shè)備的App Store店面在交易期間發(fā)生更改時(shí)(比如:設(shè)備的App Store原先登錄的是中國地區(qū)的店面,在交易期間切換到了美國地區(qū)的店面),詢問是否繼續(xù)交易:

  • 返回true:在更新后的店面中繼續(xù)交易。
  • 返回false:停止交易,此時(shí)會(huì)報(bào)出錯(cuò)誤SKErrorStoreProductNotAvailable。該情況下請考慮提示用戶該產(chǎn)品在當(dāng)前店面中不可用。

<2>shouldShowPriceConsent:當(dāng)App Store Connect中的訂閱價(jià)格已更改,且訂閱者尚未采取任何行動(dòng)時(shí),詢問是否立即顯示價(jià)格變化同意書:

  • 返回true:立即通知系統(tǒng)顯示價(jià)格同意書。
  • 返回false:系統(tǒng)不顯示價(jià)格同意書。

此方法僅適用于需要客戶同意的自動(dòng)續(xù)訂訂閱價(jià)格上漲。當(dāng)你提高自動(dòng)續(xù)訂訂閱的價(jià)格并需要客戶同意時(shí),Apple會(huì)通過電子郵件、推送通知和應(yīng)用內(nèi)價(jià)格同意書通知受影響的訂閱者,并要求他們同意新價(jià)格。如果客戶不同意或不采取任何行動(dòng),他們的訂閱將在當(dāng)前計(jì)費(fèi)周期結(jié)束時(shí)到期。

3.如何沙盒測試

<1>進(jìn)入沙盒測試員,點(diǎn)擊添加按鈕,需要注意的是沙盒賬號(hào)必須是沒有注冊過App Store和沙盒賬號(hào)的郵箱,郵箱可以隨便填,記住郵箱密碼就可以。

<2>打開手機(jī)的設(shè)置 --> 點(diǎn)擊App Store --> 往下滑有個(gè)沙盒賬戶 --> 登錄 --> 然后Apple ID安全,點(diǎn)擊其他選項(xiàng),選擇不升級(jí)。

<3>如果你的App Store下面沒有沙盒賬戶,說明你的手機(jī)之前沒有做過沙盒測試,那就連接你的電腦運(yùn)行一下,點(diǎn)擊測試購買的商品,會(huì)彈出提示框讓你登錄沙盒賬戶。

4.充值成功后不走購買成功的回調(diào)

完成購買

當(dāng)用戶付款成功后會(huì)彈出上面的彈窗,只有當(dāng)用戶點(diǎn)擊“好”才會(huì)觸發(fā)成功``的回調(diào)。如果這時(shí)候客戶不點(diǎn)擊"好",將app殺死或者卸載,那么服務(wù)器是不知道用戶在IAP服務(wù)器上已經(jīng)完成付款了,是不會(huì)給客戶發(fā)貨的。

5.票據(jù)(已過時(shí),不建議使用)

<1>如何使用cancellation_data字段?

該字段僅適用于自動(dòng)續(xù)期訂閱、非續(xù)期訂閱和非消耗型產(chǎn)品。當(dāng)用戶申請退款(Refund)或撤銷家庭共享(Family Sharing)時(shí),票據(jù)校驗(yàn)返回的JSON數(shù)據(jù)中才會(huì)有該字段。因此可以利用該字段監(jiān)測用戶退款,并及時(shí)收回已經(jīng)發(fā)放的產(chǎn)品或服務(wù)。

<2>如何選擇票據(jù)校驗(yàn)地址?

測試階段使用沙盒地址:

https://sandbox.itunes.apple.com/verifyReceipt

App Store發(fā)布之后使用正式地址:

https://buy.itunes.apple.com/verifyReceipt

最佳實(shí)踐:無論是測試階段還是正式發(fā)布階段,總是先去正式環(huán)境校驗(yàn),如果返回21007狀態(tài)碼,再去沙盒環(huán)境校驗(yàn),這樣無需在測試、審核、發(fā)布等各個(gè)階段頻繁切換地址。

<3>如何處理appStoreReceiptURL為空的情況?

[NSBundle mainBundle].appStoreReceiptURL只是一個(gè)URL,當(dāng)用戶付款成功后,系統(tǒng)會(huì)把receipt寫入到這個(gè)位置。取receipt時(shí)需要判空,如果文件不存在,就需要從蘋果服務(wù)器重新刷新下載了。

SKReceiptRefreshRequest刷新系統(tǒng)會(huì)彈窗讓用戶授權(quán),而用戶會(huì)取消授權(quán),App必須要能正確處理取消授權(quán)的情況。若刷新成功,拿到票據(jù)走正常驗(yàn)證發(fā)貨流程;若刷新失敗時(shí)App應(yīng)釋放該請求,而不是嘗試再次調(diào)用它。

出現(xiàn)這種情況的一種典型場景:從TestFlightXcode安裝App。當(dāng)從App Store安裝或從iCloud恢復(fù)時(shí),appStoreReceipt將始終存在,但是在一些未知情況票據(jù)確實(shí)不存在。

<4>校驗(yàn)票據(jù)時(shí),返回的結(jié)果中的in_app是一個(gè)空數(shù)組,而不是預(yù)期的產(chǎn)品?

in_app數(shù)組表示App Store尚未記錄用戶的任何交易,可能票據(jù)未更新導(dǎo)致的。

消耗性產(chǎn)品在購買成功后會(huì)添加進(jìn)票據(jù),在finishTransaction成后后從交易隊(duì)列移除,在下次更新票據(jù)時(shí)正式從票據(jù)中移除。而自動(dòng)續(xù)期訂閱、非續(xù)期訂閱和非消耗型產(chǎn)品自購買成功后就會(huì)永久保留在票據(jù)中,如果應(yīng)用只提供消耗型產(chǎn)品,那么在票據(jù)當(dāng)前沒有產(chǎn)品并且本次購買還未來得及更新時(shí)就拿去校驗(yàn),就會(huì)出現(xiàn)空數(shù)組的情況。

出現(xiàn)這種情況,可以使用SKReceiptRefreshRequest來顯示刷新票據(jù)。

6.App Store Server API(新版,建議使用)

<1>新特征

2021年In App Purchase迎來了重大變化,蘋果推出了StoreKit2App Store Server APIApp Store Server Notifications V2三大特征。

蘋果將原先舊的內(nèi)購變?yōu)?code>Original API for in-app purchase,引入了全新的內(nèi)購In-App Purchase(即StoreKit2)StoreKit2是基于Swift的API,從iOS15開始提供。

<2>appAccountToken:

其中最大的變化之一是新增了一個(gè)appAccountToken字段,用于開發(fā)者將交易與自己服務(wù)器上的用戶關(guān)聯(lián)起來的UUID。當(dāng)開發(fā)者發(fā)起應(yīng)用內(nèi)購買時(shí)傳入appAccountToken,該值會(huì)永久的在交易信息里面保存。

而且蘋果打通了applicationUsernameappAccountToken,當(dāng)用Original StoreKit創(chuàng)建訂單時(shí),applicationUsername字段賦值使用 UUID格式內(nèi)容時(shí),則可以在服務(wù)端通知或者解析receipt票據(jù)時(shí),可以獲取這個(gè)UUID值,也就是訂單可以關(guān)聯(lián)確認(rèn):

注意:當(dāng)使用verifyReceipt驗(yàn)證票據(jù)時(shí),如果是消耗型產(chǎn)品,返回的responseBody里面沒有latest_receipt_info字段。

注意:蘋果不保證非UUID格式的applicationUsername屬性會(huì)在交易里面持續(xù)存在。

<3>使用新的驗(yàn)證方式

由于使用票據(jù)receipt有許多不合理的地方,我們可以放棄舊的讀取本地receipt傳給服務(wù)端驗(yàn)證的方式,直接采用App Store Server API中的Get Transaction Info API,將transaction_id傳遞給蘋果服務(wù)器進(jìn)行驗(yàn)證票據(jù)。

在創(chuàng)建訂單時(shí),自己的服務(wù)器會(huì)返回一個(gè)UUID與訂單綁定,當(dāng)發(fā)起交易時(shí)將該UUID通過applicationUsername傳給App Store。校驗(yàn)時(shí),自己的服務(wù)器通過Get Transaction Info API 獲取到App Store回傳的UUID,此時(shí)就可以與自己服務(wù)器的訂單相關(guān)聯(lián)從而進(jìn)行充值。

7.在客戶端處理購買交易(舊版StoreKit)

  • 在iOS內(nèi)購中,蘋果提供了一個(gè)非常重要的概念,即事務(wù)記錄Transaction。當(dāng)用戶發(fā)起一筆訂單時(shí),會(huì)生成一筆事務(wù)記錄,直到開發(fā)者手動(dòng)調(diào)用finishTransaction才會(huì)結(jié)束訂單,即使用戶取消購買也需要手動(dòng)調(diào)用finishTransaction,否則該筆訂單會(huì)一直保存在事務(wù)記錄里面。

  • 利用蘋果的事務(wù)機(jī)制,每次重啟App時(shí)檢測未處理的訂單-[SKPaymentQueue addTransactionObserver:],若訂單已經(jīng)購買但是未發(fā)放產(chǎn)品(即漏單),需要重新向服務(wù)器驗(yàn)證,驗(yàn)證成功后調(diào)用finishTransaction結(jié)束訂單。對于消耗品,調(diào)用finishTransaction結(jié)束的訂單,不會(huì)再走蘋果事務(wù)機(jī)制。

  • 非特殊情況下無法對同一產(chǎn)品重復(fù)購買(即已經(jīng)發(fā)起一筆購買訂單,但是并沒有finishTransaction結(jié)束),所以每次購買前需要檢測是否有待處理的交易-[SKPaymentQueue transactions],若有則需要調(diào)用finishTransaction結(jié)束。

處理漏單的關(guān)鍵點(diǎn):在確認(rèn)服務(wù)端驗(yàn)證交易并發(fā)放產(chǎn)品前不要結(jié)束訂單,即不要調(diào)用finishTransaction結(jié)束該筆訂單。

二、有關(guān)Google內(nèi)購

1.設(shè)定商品的定價(jià)

Google Play Console --> 設(shè)定 --> 定價(jià)范本

設(shè)定價(jià)格

2.建立產(chǎn)品

Google Play Console --> 產(chǎn)品 --> 應(yīng)用程式內(nèi)產(chǎn)品 --> 建立產(chǎn)品

建立產(chǎn)品

建立好產(chǎn)品后,一定需要啟用,否則App會(huì)讀取不到數(shù)據(jù)。

3.封閉測試

<1>Google Play Console --> 測試 --> 封閉測試 --> 建立測試群組

建立測試群組

<2> 管理測試群組 --> 版本 --> 建立封閉測試版本 --> 上傳測試版本(aab文件)

上傳測試版本(aab文件)

安裝在手機(jī)上的包一定要和上傳的包的版本號(hào)、build號(hào)、簽名一致。

<3>管理測試群組 --> 測試人數(shù) --> 建立電子郵件名單

添加測試人員

待審核通過后,復(fù)制鏈接發(fā)送給測試人員,讓他接受邀請,否則測試人員會(huì)拉取不到訂單數(shù)據(jù)。

4.授權(quán)測試

Google Play Console --> 設(shè)定 --> 授權(quán)測試 --> 授權(quán)回應(yīng)RESPOND_NORMALLY

授權(quán)測試

5.Purchase交易訂單

  • getPurchaseState():交易狀態(tài)。PENDING:待處理,PURCHASED:已購買,UNSPECIFIED_STATE:未知狀態(tài)。
  • getOrderId():交易唯一訂單ID,PENDING狀態(tài)為null,PURCHASED狀態(tài)則填充該訂單ID,例如GPA.3356-0813-8427-26633。
  • getPackageName(): App的包名,例如com.example.test
  • getPurchaseTime():購買產(chǎn)品的時(shí)間,單位是毫秒時(shí)間戳。對于訂閱則是訂閱的注冊時(shí)間,例如1691131391062。
  • getPurchaseToken():交易令牌,唯一標(biāo)識(shí)用戶和交易。
  • isAutoRenewing():訂閱是否自動(dòng)續(xù)訂。如果為true,則訂閱處于有效狀態(tài),并在下一個(gè)計(jì)費(fèi)日期自動(dòng)續(xù)訂。如果為false,則表明用戶已取消訂閱,在下一個(gè)計(jì)費(fèi)日訂閱到期。
  • isAcknowledged():交易是否被確認(rèn),購買成功后需要在3天內(nèi)向Google服務(wù)器確認(rèn),否則Google Play會(huì)自動(dòng)退款。
  • getAccountIdentifiers():發(fā)起交易時(shí)自定義傳值,用于與用戶賬戶唯一關(guān)聯(lián)的ID。Google Play可以使用它來監(jiān)測不規(guī)則活動(dòng),例如許多設(shè)備在短時(shí)間內(nèi)使用同一賬戶進(jìn)行購買。

6.PENDING待處理狀態(tài)的購買交易(一次性商品)

PENDING狀態(tài)并不是等待支付的狀態(tài),而是客戶端完成支付操作后,Google在成功扣款前,這筆購買交易將會(huì)處于待處理狀態(tài)??稍跍y試時(shí)選擇慢速測試卡,幾分鐘后批準(zhǔn),此時(shí)的狀態(tài)會(huì)是PENDING。

對于一次性商品,只有處于PENDING狀態(tài)的購買交易才可取消。如果處于PURCHASED狀態(tài)的一次性商品發(fā)生退款,需要通過Voided Purchases API獲知。

  • PENDING --> PURCHASED:當(dāng)用戶完成待處理的一次性商品購買交易時(shí),Google Play會(huì)發(fā)送一條類型為ONE_TIME_PRODUCT_PURCHASEDOneTimeProductNotificationRTDN消息。當(dāng)購買狀態(tài)從PENDING轉(zhuǎn)換為PURCHASED時(shí),3天的確認(rèn)期限才會(huì)開始。

  • PENDING --> CANCELED:當(dāng)用戶取消待處理的一次性商品時(shí),Google Play會(huì)發(fā)送一條類型為ONE_TIME_PRODUCT_CANCELEDOneTimeProductNotificationRTDN消息。例如,如果用戶未在規(guī)定時(shí)間內(nèi)完成付款,就可能發(fā)生這種情況。

注意:當(dāng)選擇慢速測試卡,幾分鐘后拒絕購買,客戶端并沒有收到取消的通知,即沒有在PurchasesUpdatedListener里面監(jiān)測到交易狀態(tài)的取消更新。

7.在后端處理購買交易(一次性商品)

APIpurchases.products:get返回的結(jié)果:

{
  "kind": string,
  /// 購買產(chǎn)品的時(shí)間戳
  "purchaseTimeMillis": string,
  /// 訂單狀態(tài),0:已購買 1:已取消 2:待處理
  "purchaseState": integer,
  /// 消耗狀態(tài),0:未消耗 1:已消耗
  "consumptionState": integer,
  /// 開發(fā)人員自定義的字段
  "developerPayload": string,
  /// 訂單ID
  "orderId": string,
  /// 購買類型,0:測試  1:促銷  2:激勵(lì)廣告
  "purchaseType": integer,
  /// 確認(rèn)狀態(tài),0:未確認(rèn)  1:已確認(rèn)
  "acknowledgementState": integer,
  /// 交易令牌,可能不存在
  "purchaseToken": string,
  /// 產(chǎn)品ID,可能不存在
  "productId": string,
  /// 產(chǎn)品數(shù)量
  "quantity": integer,
  /// 發(fā)起交易時(shí)自定義傳值,用于與用戶賬戶唯一關(guān)聯(lián)的ID(適用于應(yīng)用內(nèi)商品的購買交易)
  "obfuscatedExternalAccountId": string,
  /// 發(fā)起交易時(shí)自定義傳值,用于與用戶個(gè)人資料唯一關(guān)聯(lián)的ID(適用于服務(wù)器端的訂閱)
  "obfuscatedExternalProfileId": string,
  /// 結(jié)算區(qū)域
  "regionCode": string
}

消耗品返回的結(jié)果:
{
    "purchaseTimeMillis": "1691131391062",
    "purchaseState": 1,
    "consumptionState": 0,
    "developerPayload": "",
    "orderId": "GPA.3356-0813-8427-26633",
    "purchaseType": 0,
    "acknowledgementState": 0,
    "kind": "androidpublisher#productPurchase",
    "obfuscatedExternalAccountId": "自己服務(wù)器上的訂單號(hào)",
    "regionCode": "TW"
}

<1> 如果驗(yàn)證購買交易,必須先檢查交易狀態(tài)是否為PURCHASED

<2>purchaseToken具有全局唯一性,所以應(yīng)該使用purchaseToken來作為是否已經(jīng)發(fā)放產(chǎn)品的唯一值。

<3>驗(yàn)證當(dāng)前購買交易的purchaseToken是否有效,若有效才發(fā)放產(chǎn)品。

<4>發(fā)放產(chǎn)品后需要進(jìn)行消耗或確認(rèn)購買交易。

  • 消耗型產(chǎn)品

對于消耗型產(chǎn)品首先需確保購買交易未被消耗,查看APIPurchases.products:get調(diào)用結(jié)果中的consumptionState,若未被消耗必須調(diào)用APIPurchases.products:consume進(jìn)行消耗。

  • 非消耗型產(chǎn)品

對于非消耗型產(chǎn)品首先需確保購買交易未被確認(rèn),查看APIPurchases.products:get調(diào)用結(jié)果中的acknowledgementState,若未被確認(rèn)必須調(diào)用APIPurchases.products:acknowledge進(jìn)行確認(rèn)。

注意:請務(wù)在購買交易狀態(tài)為PENDING時(shí)發(fā)放產(chǎn)品。

注意:請務(wù)使用orderId檢查是否存在重復(fù)的購買交易或?qū)⑵渥鳛閿?shù)據(jù)庫中的主鍵,因?yàn)椴荒鼙WC所有購買交易都會(huì)生成orderId。特別是,使用促銷代碼完成的購買交易不會(huì)生產(chǎn)orderId。

8. 在客戶端處理購買交易(一次性商品)

服務(wù)端必須在發(fā)送產(chǎn)品后進(jìn)行消耗,由于消耗請求偶爾會(huì)失敗,所以可能會(huì)出現(xiàn)漏單的情況。防止服務(wù)端出現(xiàn)驗(yàn)證錯(cuò)誤的情況,需要在App每次重啟的時(shí)候使用queryPurchasesAsync查詢未消耗的交易,若有未消耗的交易且交易處于purchased狀態(tài)時(shí),需要向服務(wù)器重新驗(yàn)證該筆購買交易。

由于queryPurchasesAsync方法不需要網(wǎng)絡(luò),且不會(huì)返回已進(jìn)行消耗標(biāo)記的交易,所以需要嚴(yán)格把控進(jìn)行消耗的時(shí)期。

三.上代碼(僅供參考)

// ignore_for_file: avoid_print
class CommonInAppPurchaseViewModel extends YZBaseViewModel {
  final BuildContext context;
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  late StreamSubscription<List<PurchaseDetails>> _purchaseSubscription;

  CommonInAppPurchaseViewModel(this.context) {
    /// InApp Purchase
    _purchaseSubscription = _inAppPurchase.purchaseStream.listen((purchaseDetailsList) {
      for (final purchaseDetails in purchaseDetailsList) {
        _listenToPurchaseUpdated(purchaseDetails);
      }
    }, onDone: () {
      _purchaseSubscription.cancel();
    });

    /// 處理未完成的訂單
    _handlePastPurchases();
  }

  /// 發(fā)起支付請求
  /// @productId: 產(chǎn)品ID
  /// @existedOrderNo:自己服務(wù)器產(chǎn)生的訂單號(hào),若有值則不重新創(chuàng)建訂單,可用于補(bǔ)單時(shí)
  /// @pointId: 選擇的點(diǎn)數(shù)套餐,創(chuàng)建訂單時(shí)必傳
  static Future<void> startPay({required String productId, String? existedOrderNo, int? pointId}) async {
    YZToastUtil.showLoading(message: '正在創(chuàng)建訂單');

    /// 檢測內(nèi)購是否可用
    final available = await InAppPurchase.instance.isAvailable();
    if (!available) {
      YZToastUtil.showMessage('不支持內(nèi)購功能');
      return;
    }

    /// 查詢產(chǎn)品id是否在服務(wù)器上注冊了
    final response = await InAppPurchase.instance.queryProductDetails({productId});
    if (response.error != null) {
      YZToastUtil.showMessage(response.error!.message);
      return;
    }

    /// 未查詢到產(chǎn)品
    if (response.productDetails.isEmpty) {
      YZToastUtil.showMessage('暫無產(chǎn)品');
      return;
    }

    /// 創(chuàng)建訂單
    var orderNo = existedOrderNo;
    String? orderUuidNo;
    if (orderNo == null) {
      final orderModel = await PurchaseApi.createOrder(pointId: pointId);
      orderNo = orderModel.number;
      orderUuidNo = orderModel.uuidNumber;
    }

    final purchaseParam = PurchaseParam(
      productDetails: response.productDetails.first,
      /// 注意:iOS的applicationUserName必須為uuid格式,否則App Store服務(wù)器不會(huì)保存
      /// android: 自己服務(wù)器上生成的訂單號(hào)
      /// iOS: 自己服務(wù)器上生成的與訂單綁定的uuid字符串
      applicationUserName: Platform.isIOS ? orderUuidNo : orderNo,
    );

    /// 發(fā)起支付請求
    try {
      await InAppPurchase.instance.buyConsumable(
        purchaseParam: purchaseParam,
        /// 安卓不自動(dòng)消耗,會(huì)在后臺(tái)服務(wù)器進(jìn)行消耗
        autoConsume: !Platform.isAndroid,
      );
      YZToastUtil.dismiss();
    } on PlatformException catch(e) {
      /// iOS重復(fù)訂單: storekit_duplicate_product_object,可根據(jù)自己的業(yè)務(wù)做處理
      YZToastUtil.showMessage(e.message ?? '');

      // if (Platform.isIOS && e.code == 'storekit_duplicate_product_object') {
      //   /// 查詢未處理的訂單
      //   final transactions = await SKPaymentQueueWrapper().transactions();
      //   for (final transaction in transactions) {
      //     await SKPaymentQueueWrapper().finishTransaction(transaction);
      //   }
      // }
    }
  }

  /// 處理未完成訂單
  Future<void> _handlePastPurchases() async {
    /// 如果是Android系統(tǒng)
    if (Platform.isAndroid) {
      /// 查詢未處理的訂單
      final androidPlatformAddition = _inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
      final response = await androidPlatformAddition.queryPastPurchases();
      if (response.error == null) {
        final purchaseDetailsList = response.pastPurchases;
        for (final purchaseDetails in purchaseDetailsList) {
          if (purchaseDetails.billingClientPurchase.purchaseState == PurchaseStateWrapper.pending) {
            /// 訂單未付款,此時(shí)需要提醒用戶完成購買,否則無法進(jìn)行下一筆訂單
            _handleGooglePendingPurchase(purchaseDetails);
          } else if (purchaseDetails.billingClientPurchase.purchaseState == PurchaseStateWrapper.purchased) {
            /// 訂單已付款,向服務(wù)器驗(yàn)證訂單票據(jù)
            await _verifyPurchase(purchaseDetails);
          }
        }
      }
    }
  }

  /// 監(jiān)聽內(nèi)購更新
  Future<void> _listenToPurchaseUpdated(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.pending) {
      /// 等待購買中
      print('等待購買中');
      /// iOS系統(tǒng)此時(shí)正處于拉起支付狀態(tài),需要比較長的時(shí)間,此處給一個(gè)提示
      /// Android系統(tǒng)此時(shí)正處于支付操作完成,等待Google批準(zhǔn)狀態(tài)
      if (Platform.isIOS) {
        YZToastUtil.showLoading(message: '正在拉起支付');
      }
    } else {
      if (Platform.isIOS) {
        YZToastUtil.dismiss();
      }
      if (purchaseDetails.status == PurchaseStatus.error) {
        /// 購買出錯(cuò),提示錯(cuò)誤信息
        print('購買出錯(cuò):${purchaseDetails.error?.message}');
        final error = purchaseDetails.error;
        if (error != null) {
          YZToastUtil.showMessage(error.message);
        }
        _completeApplePurchase(purchaseDetails);
      }  else if (purchaseDetails.status == PurchaseStatus.canceled) {
        /// 購買取消
        print('購買取消');
        _completeApplePurchase(purchaseDetails);
      } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) {
        /// 購買成功或恢復(fù)購買,向服務(wù)器驗(yàn)證訂單票據(jù)
        print('購買成功或恢復(fù)購買:${purchaseDetails.status}');
        await _verifyPurchase(purchaseDetails);
      }
    }
  }

  /// 發(fā)放產(chǎn)品
  void _deliverProduct() {
    context.read<CommonMemberViewModel>().updateLocalMemberInfo();
  }

  /// 將Apple訂單標(biāo)記為已完成
  void _completeApplePurchase(PurchaseDetails purchaseDetails) {
    /// 只需要iOS,Android會(huì)在后臺(tái)進(jìn)行消耗或確認(rèn)操作
    if (purchaseDetails is AppStorePurchaseDetails) {
      if (purchaseDetails.pendingCompletePurchase) {
        _inAppPurchase.completePurchase(purchaseDetails);
      }
    }
  }

  /// 將Google訂單標(biāo)記為已消耗
  void _completeGooglePurchase(PurchaseDetails purchaseDetails) {
    if (purchaseDetails is GooglePlayPurchaseDetails) {
      /// 在Android系統(tǒng),[_inAppPurchase.completePurchase(purchaseDetails)]方法調(diào)用的是確認(rèn)API[acknowledge],
      /// 所以對于消耗品,如果您需要在客戶端進(jìn)行消耗,您需要調(diào)用消耗API[consume]
      _inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>().consumePurchase(purchaseDetails);
    }
  }

  /// 處理Google的PENDING待處理購買交易
  void _handleGooglePendingPurchase(GooglePlayPurchaseDetails purchaseDetails) {
    final productId = purchaseDetails.productID;
    final orderNo = purchaseDetails.billingClientPurchase.obfuscatedAccountId;
    MyMessageDialog(
      width: 305.wRate,
      icon: 'dialog/publish',
      title: '未完成訂單',
      message: '檢測到您有未完成訂單,是否繼續(xù)?',
      onConfirm: () {
        startPay(productId: productId, existedOrderNo: orderNo);
      },
    ).show(context);
  }

  /// 向服務(wù)器驗(yàn)證訂單票據(jù)
  Future<void> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    YZToastUtil.showLoading(message: '正在驗(yàn)證訂單');

    if (purchaseDetails is AppStorePurchaseDetails) {
      /// 向服務(wù)器驗(yàn)證票據(jù)
      final params = <String, dynamic>{
        'transaction_id': purchaseDetails.skPaymentTransaction.transactionIdentifier,
      };

      await PurchaseApi.verifyApplePay(params: params).then((value) {
        /// 驗(yàn)證成功,發(fā)放產(chǎn)品
        _deliverProduct();
        _completeApplePurchase(purchaseDetails);
        YZToastUtil.showMessage('儲(chǔ)值成功');
      }).catchError((e) {
        YZToastUtil.dismiss();
        /// 4000006:無效的交易id 4040010:沒有找到交易id 4290000:請求超出速率限制 5000000:服務(wù)器錯(cuò)誤 5000001:服務(wù)器錯(cuò)誤 42:沒有找到訂單
        if (e is YZNetworkError) {
          if (e.code == 4000006 || e.code == 4040010 || e.code == 42) { /// 結(jié)束訂單
            YZToastUtil.showMessage('無效訂單,請聯(lián)絡(luò)客服');
            _completeApplePurchase(purchaseDetails);
          } else { /// 重新驗(yàn)單
            _retryVerifyPurchase(purchaseDetails);
          }
        }
      });
    } else if (purchaseDetails is GooglePlayPurchaseDetails) {
      /// 向服務(wù)器驗(yàn)證票據(jù)
      final params = <String, dynamic>{
        'product_id': purchaseDetails.productID,
        'purchase_token': purchaseDetails.billingClientPurchase.purchaseToken,
        'order_no': purchaseDetails.billingClientPurchase.obfuscatedAccountId,
      };

      await PurchaseApi.verifyGooglePay(params: params).then((value) {
        /// 驗(yàn)證成功,發(fā)放產(chǎn)品
        _deliverProduct();
        YZToastUtil.showMessage('儲(chǔ)值成功');
      }).catchError((e) {
        YZToastUtil.dismiss();
        if (e is YZNetworkError) {
          /// 40:驗(yàn)證失敗 41:PENDING狀態(tài)未支付 42:沒有找到訂單 43:訂單不匹配
          if (e.code == 41) { /// 提醒用戶繼續(xù)完成交易
            _handleGooglePendingPurchase(purchaseDetails);
          } else if (e.code == 42 || e.code == 43) {  /// 結(jié)束訂單
            YZToastUtil.showMessage('無效訂單,請聯(lián)絡(luò)客服');
            _completeGooglePurchase(purchaseDetails);
          } else {  /// 重新驗(yàn)單
            _retryVerifyPurchase(purchaseDetails);
          }
        }
      });
    }
  }


  /// 驗(yàn)證訂單票據(jù)失敗不要將訂單標(biāo)記為已完成,會(huì)通過以下兩種方法進(jìn)行補(bǔ)單:
  /// 1.以斐波那契數(shù)列為間隔時(shí)間嘗試10次重新驗(yàn)證訂單票據(jù)
  /// 2.在App下次啟動(dòng)時(shí),監(jiān)聽未完成訂單
  Future<void> _retryVerifyPurchase(PurchaseDetails purchaseDetails) async {
    Future Function()? retryFn;
    void Function()? retryComplete;
    bool Function(YZNetworkError error)? retryIf;
    if (purchaseDetails is GooglePlayPurchaseDetails) {  /// 安卓訂單
      retryFn = () {
        final params = <String, dynamic>{
          'product_id': purchaseDetails.productID,
          'purchase_token': purchaseDetails.billingClientPurchase.purchaseToken,
          'order_no': purchaseDetails.billingClientPurchase.obfuscatedAccountId,
        };
        return PurchaseApi.verifyGooglePay(params: params);
      };

      retryComplete = () {
        /// 重試成功,發(fā)放產(chǎn)品
        _deliverProduct();
        _completeApplePurchase(purchaseDetails);
        YZToastUtil.showMessage('儲(chǔ)值成功');
      };

      /// 40:驗(yàn)證失敗 41:PENDING狀態(tài)未支付 42:沒有找到訂單 43:訂單不匹配
      retryIf = (e) => e.code != 41 && e.code != 42 && e.code != 43;
    }

    if (purchaseDetails is AppStorePurchaseDetails) { /// 蘋果訂單
      retryFn = () {
        final params = <String, dynamic>{
          'transaction_id': purchaseDetails.skPaymentTransaction.transactionIdentifier,
        };
        return PurchaseApi.verifyApplePay(params: params);
      };

      retryComplete = () {
        /// 重試成功,發(fā)放產(chǎn)品
        _deliverProduct();
        YZToastUtil.showMessage('儲(chǔ)值成功');
      };

      /// 4000006:無效的交易id 4040010:沒有找到交易id 4290000:請求超出速率限制 5000000:服務(wù)器錯(cuò)誤 5000001:服務(wù)器錯(cuò)誤 42:沒有找到訂單
      retryIf = (e) => e.code != 4000006 && e.code != 4040010 && e.code != 42;
    }

    if (retryFn == null) return;

    var attempt = 0;
    while (true) {
      attempt++;
      await Future.delayed(_retryVerifyInterval(attempt));

      try {
        await retryFn();
        retryComplete?.call();
        return;
      } on YZNetworkError catch (e) {
        if (attempt >= 10 || (retryIf != null && !retryIf(e))) {
          rethrow;
        }
      }
    }
  }

  /// 重新驗(yàn)證訂單票據(jù)間隔時(shí)間, 以斐波那契數(shù)列為間隔時(shí)間嘗試10次重新驗(yàn)證訂單票據(jù)
  Duration _retryVerifyInterval(int attempt) {
    if (attempt < 2) {
      return Duration(seconds: attempt);
    }
    const mod = 1000000007;
    var p = 0;
    var q = 0;
    var r = 1;
    for (var i = 2; i <= attempt; i++) {
      p = q;
      q = r;
      r = (p + q) % mod;
    }
    return Duration(seconds: r);
  }

  @override
  void dispose() {
    _purchaseSubscription.cancel();
    super.dispose();
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

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