一、有關(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)這種情況的一種典型場景:從TestFlight
或Xcode
安裝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
迎來了重大變化,蘋果推出了StoreKit2
、App Store Server API
、App 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ì)永久的在交易信息里面保存。
而且蘋果打通了applicationUsername
和appAccountToken
,當(dāng)用Original StoreKit
創(chuàng)建訂單時(shí),applicationUsername
字段賦值使用 UUID
格式內(nèi)容時(shí),則可以在服務(wù)端通知或者解析receipt
票據(jù)時(shí),可以獲取這個(gè)UUID
值,也就是訂單可以關(guān)聯(lián)確認(rèn):
- 在
App Store Server API
中,JWSTransactionDecodedPayload對象會(huì)返回applicationUsername
在appAccountToken
字段中。 - 在
App Store Server Notifications
中,JWSTransactionDecodedPayload對象會(huì)返回applicationUsername
在appAccountToken
字段中。 - 當(dāng)您調(diào)用verifyReceipt接口來驗(yàn)證票據(jù)時(shí),
App Store
服務(wù)器會(huì)返回applicationUsername
在responseBody.Latest_receipt_info中的app_account_token
字段中。
注意:當(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à)范本
2.建立產(chǎn)品
Google Play Console
--> 產(chǎn)品 --> 應(yīng)用程式內(nèi)產(chǎn)品 --> 建立產(chǎn)品
建立好產(chǎn)品后,一定需要啟用,否則App會(huì)讀取不到數(shù)據(jù)。
3.封閉測試
<1>Google Play Console
--> 測試 --> 封閉測試 --> 建立測試群組
<2> 管理測試群組 --> 版本 --> 建立封閉測試版本 --> 上傳測試版本(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
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_PURCHASED
的OneTimeProductNotification
RTDN消息。當(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_CANCELED
的OneTimeProductNotification
RTDN消息。例如,如果用戶未在規(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)品首先需確保購買交易未被消耗,查看API
Purchases.products:get調(diào)用結(jié)果中的consumptionState
,若未被消耗必須調(diào)用API
Purchases.products:consume進(jìn)行消耗。
- 非消耗型產(chǎn)品
對于非消耗型產(chǎn)品首先需確保購買交易未被確認(rèn),查看API
Purchases.products:get調(diào)用結(jié)果中的acknowledgementState
,若未被確認(rèn)必須調(diào)用API
Purchases.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();
}
}