in_app_purchase: ^3.1.13
用谷歌推出的內(nèi)購(gòu)插件,這個(gè)插件就是把ios內(nèi)購(gòu)api全部翻譯一遍
所以原生能實(shí)現(xiàn)的功能 這個(gè)插件完全可以實(shí)現(xiàn)
要在初始化時(shí)候先注冊(cè)下平臺(tái),因?yàn)榘沧渴枪雀柚Ц?我們只用ios的內(nèi)購(gòu)
我的下面代碼有很多異常處理,沒完成訂單查詢等, 連續(xù)出錯(cuò)幾次 直接finish等,確保丟單率最低,并且寫入本地文件日志,有問題可以排查
if (defaultTargetPlatform == TargetPlatform.iOS) {
InAppPurchaseStoreKitPlatform.registerPlatform();
}
Future.delayed(const Duration(seconds: 1), () {
delayAction();
});
delayAction() async {
if (LoginTool.isLogin()) {
if (defaultTargetPlatform == TargetPlatform.iOS) {
//內(nèi)購(gòu)監(jiān)聽
await IOSPayment.instance.init();
IOSPayment.instance.startSubscription();
// 查詢沒完成訂單 進(jìn)入支付頁(yè)面 調(diào)用一下
// TDIOSPayment.instance.checkUnfinishedPayment(() => null);
}
}
}
開始支付時(shí)候調(diào)用
IOSPayment.instance.isPaying = true;
IOSPayment.instance.iosStartPay('xxxxxx', orderId,
iapCallback: (isSuccess, errorMsg) {
Future.delayed(const Duration(seconds: 1), () {
TDIOSPayment.instance.isPaying = false;
});
});
isPaying設(shè)置 主要在支付期間 不讓里面進(jìn)入后臺(tái)調(diào)用檢查沒完成的訂單
typedef IapCallback = void Function(bool isSuccess, String errorMsg);
class IOSPayment with WidgetsBindingObserver {
listener() {
if (LoginTool.isLogin()) {
IOSPayment.instance.startSubscription();
} else {
IOSPayment.instance.stopIapListen();
}
}
/// 單例模式
IOSPayment._();
init() async {
//登錄狀態(tài)監(jiān)聽
kLoginChangeNotifier.addListener(listener);
}
static IOSPayment? _instance;
static IOSPayment get instance => _getOrCreateInstance();
static IOSPayment _getOrCreateInstance() {
if (_instance != null) {
return _instance!;
} else {
_instance = IOSPayment._();
return _instance!;
}
}
bool hasBindAppLife = false;
IapCache iapCache = IapCache();
// 應(yīng)用內(nèi)支付實(shí)例
final InAppPurchasePlatform iosPurchase = InAppPurchasePlatform.instance;
// iOS訂閱監(jiān)聽
StreamSubscription<List<PurchaseDetails>>? _subscription;
/// 判斷是否可以使用支付
Future<bool> isAvailable() async => await iosPurchase.isAvailable();
///監(jiān)聽?wèi)?yīng)用生命周期變化
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
jdLog(':didChangeAppLifecycleState:$state');
if (state == AppLifecycleState.resumed) {
//從后臺(tái)切換前臺(tái),界面可見
if (!isPaying) {
checkUnfinishedPayment(() => null);
}
}
}
// 開始訂閱 app一啟動(dòng)或者登錄就訂閱 看是否有支付完成的訂單
void startSubscription() async {
if (!LoginTool.isLogin()) {
return;
}
if (_subscription != null) {
return;
}
if (!hasBindAppLife) {
WidgetsBinding.instance.addObserver(this);
hasBindAppLife = true;
}
jdLog('iap 開始訂閱 -------->');
// 支付消息訂閱
Stream purchaseStream = iosPurchase.purchaseStream;
_subscription = purchaseStream.listen((purchaseDetailsList) {
purchaseDetailsList.forEach(_handleReportedPurchaseState);
}, onDone: () {
jdLog('iap 開始訂閱onDone -------->');
// _subscription?.cancel();
jdLog("onDone");
}, onError: (error) {
jdLog("error");
}) as StreamSubscription<List<PurchaseDetails>>?;
}
bool isPaying = false;
IapCallback? currentIapCallBack;
// 開始支付 productId : 商品在蘋果后臺(tái)設(shè)置的id orderId: 我們服務(wù)端生產(chǎn)的id
void iosStartPay(String productId, String orderId,
{IapCallback? iapCallback}) async {
if (!LoginTool.isLogin() ||
productId.isEmpty ||
orderId.isEmpty ||
LoginTool.instance.userId.isEmpty) {
FileWriteLog.log('iap iosStartPay 參數(shù)不對(duì) 或沒登錄 -------->');
iapCallback?.call(false, '參數(shù)不對(duì) 或沒登錄');
return;
}
if (!await isAvailable()) {
bool hasNet = await isNetWorkConnected();
if (hasNet) {
alertCanNotBuyDialog();
} else {
TDToast.showToast("沒有網(wǎng)絡(luò)");
}
iapCallback?.call(false, '無法支付');
return;
}
checkUnfinishedPayment(() async {
// 獲取商品列表
ProductDetailsResponse appStoreProducts =
await iosPurchase.queryProductDetails({productId});
if (appStoreProducts.productDetails.isNotEmpty) {
// 發(fā)起支付 消耗商品的支付
FileWriteLog.log('iap 發(fā)起支付productId --------> $productId');
iosPurchase
.buyConsumable(
purchaseParam: PurchaseParam(
productDetails: appStoreProducts.productDetails.first,
applicationUserName: LoginTool.instance.userId,
),
)
.then((value) {
if (value) {
currentIapCallBack = iapCallback;
// 只要能發(fā)起,就寫入
// status 0 開始支付 1 蘋果回調(diào)失敗 2 蘋果支付成功 3服務(wù)器校驗(yàn)失敗
Map dataMap = {
IapCache.kOrderId: orderId,
IapCache.kProductId: productId,
IapCache.kStatus: 0,
IapCache.kUserId: LoginTool.instance.userId,
};
FileWriteLog.log('iap 開始支付保存的dataMap-----> ${dataMap.toString()}');
iapCache.saveOrUpdateWithMap(dataMap);
}
}).catchError((err) {
FileWriteLog.log('當(dāng)前商品您有未完成的交易,請(qǐng)等待iOS系統(tǒng)核驗(yàn)后再次發(fā)起購(gòu)買。');
iapCallback?.call(false, '當(dāng)前商品您有未完成的交易,請(qǐng)等待iOS系統(tǒng)核驗(yàn)后再次發(fā)起購(gòu)買。');
jdLog(err);
});
} else {
iapCallback?.call(false, '沒有這個(gè)商品');
TDToast.showToast("查詢商品信息失敗");
}
});
}
//監(jiān)聽狀態(tài)回調(diào)
Future<void> _handleReportedPurchaseState(
AppStorePurchaseDetails purchaseDetail) async {
FileWriteLog.log(
'iap 監(jiān)聽狀態(tài)回調(diào) purchaseDetail.status --------> ${purchaseDetail.status}');
if (purchaseDetail.status == PurchaseStatus.pending) {
// 有訂單開始支付
FileWriteLog.log(
'iap 監(jiān)聽狀態(tài)回調(diào) 有訂單開始支付productID--------> ${purchaseDetail.productID}');
JDLoadingTool.showLoading();
} else {
if (purchaseDetail.status == PurchaseStatus.error) {
//錯(cuò)誤
FileWriteLog.log(
'iap 監(jiān)聽狀態(tài)回調(diào) 錯(cuò)誤1 productID--------> ${purchaseDetail.productID}');
finishTransaction(purchaseDetail);
currentIapCallBack?.call(false, '蘋果支付失敗');
} else if (purchaseDetail.status == PurchaseStatus.canceled) {
/// 取消訂單
FileWriteLog.log(
'iap 監(jiān)聽狀態(tài)回調(diào) 取消訂單 productID--------> ${purchaseDetail.productID}');
currentIapCallBack?.call(false, '取消訂單');
finishTransaction(purchaseDetail);
} else if (purchaseDetail.status == PurchaseStatus.purchased ||
purchaseDetail.status == PurchaseStatus.restored) {
FileWriteLog.log(
'iap 監(jiān)聽狀態(tài)回調(diào) 支付完成 productID--------> ${purchaseDetail.productID}');
Map? resultMap =
await iapCache.findWithProductID(purchaseDetail.productID);
if (resultMap != null) {
FileWriteLog.log(
'iap 緩存resultMap != null productID--------> ${purchaseDetail.productID}');
resultMap[IapCache.kServerVerificationData] =
purchaseDetail.verificationData.serverVerificationData;
resultMap[IapCache.kTransactionDate] = purchaseDetail.transactionDate;
resultMap[IapCache.kPurchaseID] = purchaseDetail.purchaseID ?? "";
resultMap[IapCache.kStatus] = 2;
iapCache.saveOrUpdateWithMap(resultMap);
FileWriteLog.log(
'iap 支付成功保存的resultMap-----> ${resultMap.toString()}');
//已經(jīng)購(gòu)買 purchaseID是蘋果服務(wù)器的訂單id transactionIdentifier
// if (purchaseDetail.applicationUsername.isEmptyNullAble) {
// FileWriteLog.log("applicationUsername null ");
// /*
// 如果某個(gè)transaction支付成功但是并沒有調(diào)用finishTransaction去完成這個(gè)交易的時(shí)候,
// 下次啟動(dòng)App重新監(jiān)聽支付隊(duì)列的時(shí)候會(huì)重新調(diào)用paymentQueue:updatedTransactions:重新獲取到未完成的交易,
// 這個(gè)時(shí)候獲取applicationUsername會(huì)出現(xiàn)nil的情況
//
// 目前可能場(chǎng)景是用戶只嘗試充值了一筆,收到蘋果兩次支付回調(diào)
// 比如:
// 1、不在常用區(qū)域網(wǎng)絡(luò)或長(zhǎng)時(shí)間未使用IAP, 則需要進(jìn)行短信校驗(yàn). 此時(shí)會(huì)有兩次支付回調(diào)
// 2、如果在支付進(jìn)行時(shí), 我們將App進(jìn)程銷毀, 支付完成再啟動(dòng)App, 也會(huì)有連續(xù)兩次回調(diào)
// 備注:第一次回調(diào)applicationUsername有值,第二次回調(diào)applicationUsername為空
// */
// }
String orderId = resultMap.getStringNotNull(IapCache.kOrderId) ?? '';
if (orderId.isNotEmpty) {
FileWriteLog.log('iap 調(diào)用后臺(tái)接口,發(fā)放商品orderId-----> $orderId');
/// 調(diào)用后臺(tái)接口,發(fā)放商品
bool success =
await requestVerifyToServer(purchaseDetail, orderId: orderId);
if (success) {
FileWriteLog.log('iap 服務(wù)器驗(yàn)證通過-----> ');
deliverProduct(purchaseDetail);
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
} else {
// 重試幾次強(qiáng)制刪除 重新請(qǐng)求 或者刪除
retryOrFinishPurchase(purchaseDetail, orderId, (success) {
if (success) {
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
deliverProduct(purchaseDetail);
} else {
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(purchaseDetail);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)聯(lián)系客服',
rightButtonFunction: (context) {
//上傳日志
},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
}
});
}
} else {
// 沒查到訂單id
//本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
noLocalDataSendServer(purchaseDetail, (success) {
if (success) {
currentIapCallBack?.call(true, '');
deliverProduct(purchaseDetail);
FileWriteLog.log(
'iap finishTransaction222 productID--------> ${purchaseDetail.productID}');
finishTransaction(purchaseDetail);
} else {
currentIapCallBack?.call(false, '');
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(purchaseDetail);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)聯(lián)系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
FileWriteLog.log(
'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗 productID--------> ${purchaseDetail.productID}');
}
});
}
} else {
//本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
FileWriteLog.log(
'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn) productID--------> ${purchaseDetail.productID}');
noLocalDataSendServer(purchaseDetail, (success) {
if (success) {
FileWriteLog.log(
'iap finishTransaction333 productID--------> ${purchaseDetail.productID}');
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
deliverProduct(purchaseDetail);
} else {
currentIapCallBack?.call(false, '服務(wù)端校驗(yàn)失敗');
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)聯(lián)系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
FileWriteLog.log(
'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗 productID--------> ${purchaseDetail.productID}');
}
});
}
}
}
}
// 支付成功 發(fā)送商品
deliverProduct(PurchaseDetails purchaseDetails) {
TDToast.showToast('支付成功');
}
//處理校驗(yàn)失敗邏輯 重試還是關(guān)閉
retryOrFinishPurchase(PurchaseDetails purchaseDetails, String? orderId,
Function(bool success) complete) async {
bool hasNet = await isNetWorkConnected();
if (hasNet) {
if (orderId.isNotEmptyNullAble) {
bool result = await requestVerifyToServer(purchaseDetails,
orderId: orderId ?? "");
complete(result);
} else {
noLocalDataSendServer(purchaseDetails, (success) {
complete(success);
});
}
} else {
TDToast.showToast('校驗(yàn)支付失敗');
complete(false);
}
}
// 本地沒數(shù)據(jù) 發(fā)送服務(wù)端校驗(yàn) 可以加個(gè)參數(shù)
noLocalDataSendServer(
PurchaseDetails purchaseDetail, Function(bool success)? complete) async {
bool success = await requestVerifyToServer(purchaseDetail);
complete?.call(success);
}
//向服務(wù)器請(qǐng)求校驗(yàn)
Future<bool> requestVerifyToServer(PurchaseDetails purchaseDetail,
{String? orderId}) async {
/*
orderId,
purchaseID //蘋果的訂單id
purchaseDetail.productID,
purchaseDetail.verificationData.serverVerificationData,
purchaseDetail.transactionDate ?? ""
*/
// 成功后刪除 已經(jīng)發(fā)送過商品 也是finish
// finalTransaction(purchaseDetail);
return Future<bool>.value(true);
}
//查詢支付完成 沒向服務(wù)端校驗(yàn)訂單
void checkUnfinishedPayment(Function() complete) async {
if (LoginTool.isNotLogin()) {
complete();
return;
}
SKPaymentQueueWrapper()
.transactions()
.then((List<SKPaymentTransactionWrapper> values) async {
if (values.isNotEmpty) {
for (var element in values) {
if (element.transactionState ==
SKPaymentTransactionStateWrapper.purchased) {
String productId = element.payment.productIdentifier;
String receiptData = await SKReceiptManager.retrieveReceiptData();
AppStorePurchaseDetails detail =
AppStorePurchaseDetails.fromSKTransaction(element, receiptData);
// detail.applicationUsername = element.payment.applicationUsername;
FileWriteLog.log('iap 有沒完成的交易productID---->${detail.productID}');
if (element.payment.applicationUsername?.isEmpty ?? false) {
FileWriteLog.log('iap---> applicationUsername會(huì)出現(xiàn)nil');
// SKPaymentQueueWrapper().finishTransaction(element);
// deleteWithProductID(productId);
// complete();
}
iapCache
.findWithProductID(element.payment.productIdentifier)
.then((Map? resultMap) {
if (resultMap != null && resultMap.keys.isNotEmpty) {
requestVerifyToServer(detail,
orderId: resultMap[IapCache.kOrderId])
.then((value) {
if (value) {
FileWriteLog.log(
'iap 重新校驗(yàn)成功 finishTransaction444 -------->');
finishTransaction(detail);
complete();
} else {
FileWriteLog.log('iap 重新校驗(yàn)失敗11 -------->');
retryOrFinishPurchase(detail, resultMap[IapCache.kOrderId],
(success) {
if (success) {
FileWriteLog.log(
'iap 重新校驗(yàn)成6666 inishTransaction-------->');
deliverProduct(detail);
finishTransaction(detail);
} else {
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(detail);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)聯(lián)系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服務(wù)器校驗(yàn)失敗',
content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
}
complete();
});
}
});
} else {
//本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
noLocalDataSendServer(detail, (success) {
if (success) {
deliverProduct(detail);
FileWriteLog.log('iap finishTransaction666 -------->');
finishTransaction(detail);
} else {
FileWriteLog.log('iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗 -------->');
}
complete();
});
}
});
}
}
} else {
complete();
}
});
}
// 關(guān)閉交易
void finishTransaction(PurchaseDetails purchaseDetails) async {
await iosPurchase.completePurchase(purchaseDetails);
iapCache.deleteWithProductID(purchaseDetails.productID);
JDLoadingTool.dismissLoading();
FileWriteLog.log('iap 關(guān)閉交易 finishTransaction -------->');
}
// 退出登錄 關(guān)閉監(jiān)聽
stopIapListen() async {
_subscription?.cancel();
_subscription = null;
if (hasBindAppLife) {
WidgetsBinding.instance.removeObserver(this);
hasBindAppLife = false;
}
}
void alertCanNotBuyDialog() {
showDialog(
barrierDismissible: false, //表示點(diǎn)擊灰色背景的時(shí)候是否消失彈出框
context: PreInit.currentContext,
builder: (context) {
return AlertDialog(
title: const Text("訪問受限"),
content: const Text(
"你的手機(jī)關(guān)閉了“應(yīng)用內(nèi)購(gòu)買”,請(qǐng)?jiān)凇霸O(shè)置-屏幕使用時(shí)間-內(nèi)容和因素訪問限制”里重新打開該選項(xiàng)后嘗試。"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop("ok"); //點(diǎn)擊按鈕讓AlertDialog消失
},
child: const Text("確定")),
],
);
});
}
/// 判斷網(wǎng)絡(luò)是否連接
Future<bool> isNetWorkConnected() async {
var connectResult = await (Connectivity().checkConnectivity());
return connectResult != ConnectivityResult.none;
}
}
demo地址在https://gitee.com/kuaipai/my_app.git
里,你可以下載下來參考