因为之前写了in_app_purchase的使用与常遇到的问题,在里面说要把flutter_inapp_purchase的使用方法也写上没后来一直在忙,最近清闲下来了,现在补上之前所说的.
本文只介绍使用方法,支付集成常见问题请跳转查看另一篇帖子.
flutter in_app_purchase使用Apple pay和google pay内购订阅详解以及遇到的问题
开始
pubspec.yaml内导入
flutter_inapp_purchase: ^5.4.1
使用
class PayPageController extends GetxController {
late PayPlatform payPlatform = Platform.isAndroid ? PayAndroid() : PayIOS();
List<IAPItem> get products => payPlatform.list;
@override
void onClose() {
payPlatform.dispose();
super.onClose();
}
@override
Future<void> onInit() async {
super.onInit();
payPlatform.init();
}
pay(int index) async {
bool available = await Global.checkConnectivity();
if (!available) {
return;
}
if (!await FlutterInappPurchase.instance.isReady()) {
XXToast.toast(msg: 'Store unavailable'.tr);
return;
}
if (Platform.isAndroid && products.length != Purchase.productIds.length) {
XXToast.toast(msg: 'Google账户无法获取到订阅列表');
return;
}
payPlatform.pay(products[index]);
}
}
/// GPT 内购订阅 - 付费类型
enum GPTpurchaseType {
textWeek,
textYear;
bool get isTextVip =>
GPTpurchaseType.textWeek == this || GPTpurchaseType.textYear == this;
bool get isWeek => this == GPTpurchaseType.textWeek;
}
pay_platform.dart
文件
有可能安卓和iOS的产品名不一样,所以写了两个产品数组并做了判断
const List<String> iosProductIds = ['com.xxxx.week', 'com.xxxx.year'];
const List<String> androidProductIds = ['com.xxxx.week', 'com.xxxx.year'];
abstract class PayPlatform {
static final instance = FlutterInappPurchase.instance;
final completer = Completer<List<IAPItem>>();
List<IAPItem> list = [];
List<Purchase> planList = [Purchase.week(), Purchase.year()];
@mustCallSuper
init() {
debugPrint('[PayPlatform]: init()');
instance.initialize().then((value) async {
if (await instance.isReady()) {
await getItems();
}
});
}
getItems() async {
var value = await instance.getSubscriptions(Purchase.productIds);
debugPrint('[Purchase]: items - ${{value.length}}');
if (value.length == planList.length) {
debugPrint(
'[Purchase]: items - grand 1.${value.first} \n 2.${value.last}');
list = [
value.firstWhere((e) => e.productId == Purchase.productIds.first),
value.firstWhere((e) => e.productId == Purchase.productIds.last),
];
planList = [
Purchase.week()..update(list[0]),
Purchase.year()..update(list[1]),
];
completer.complete(list);
}
}
@mustCallSuper
dispose() {
debugPrint('[PayPlatform]: dispose()');
// instance.finalize();
}
@mustCallSuper
Future<void> pay(IAPItem item) {
debugPrint('[PayPlatform]: pay()');
return Future.value();
}
void reportTransaction(IAPItem iapItem, PurchasedItem purchasedItem) {
debugPrint(
'[PayPlatform]: paySuccess > productId: ${iapItem.productId}, transcationId: ${purchasedItem.transactionId})');
TenJin.inappPurchase(iapItem, purchasedItem: purchasedItem);
}
#验证订阅状态
Future<GPTpurchaseType?> checkPurchase() async {
var res = await MyRequest.post(Api.getMembersubScribeStatus,
data: {
'member_id': Profile.uid,
},
needToken: true);
try {
var data = json.decode(res.data);
debugPrint('[ScibeStatus]: 订阅状态 - $data');
bool isWeek = data['week'] as bool;
bool isYear = data['year'] as bool;
return isYear
? GPTpurchaseType.textYear
: (isWeek ? GPTpurchaseType.textWeek : null);
} catch (e) {
return Future.error(e);
}
}
}
/// 用于页面显示的封装类 - 订阅id & 价钱
class Purchase {
final String id;
final GPTpurchaseType type;
String price = '-';
String currency = r'$';
bool offer = false;
static final List<String> productIds =
Platform.isAndroid ? androidProductIds : iosProductIds;
Purchase.week()
: id = productIds.first,
type = GPTpurchaseType.textWeek;
Purchase.year()
: id = productIds.first,
type = GPTpurchaseType.textYear;
String get perDay {
// ignore: no_leading_underscores_for_local_identifiers
double? _price = double.tryParse(price);
if (_price == null) return '';
switch (type) {
case GPTpurchaseType.textWeek:
return (_price / 7).toStringAsFixed(2);
case GPTpurchaseType.textYear:
return (_price / 365).toStringAsFixed(2);
}
}
update(IAPItem item) {
if (Platform.isIOS) {
offer = item.discountsIOS?.isNotEmpty ?? false;
} else {
offer = (item.subscriptionOffersAndroid?.length ?? 0) > 1;
}
price = item.price ?? '-';
currency =
RegExp(r'([^a-zA-Z]{1})').firstMatch(item.localizedPrice!)?.group(0) ??
item.currency!;
}
}
pay_android.dart
文件
单独写的安卓文件,如果安卓有单独的业务,在这里面进行使用.
class PayAndroid extends PayPlatform {
static final _instance = FlutterInappPurchase.instance;
StreamSubscription? updateSub;
StreamSubscription? errorSub;
@override
dispose() {
updateSub?.cancel();
updateSub = null;
errorSub?.cancel();
errorSub = null;
super.dispose();
}
@override
Future<void> pay(IAPItem item) async {
super.pay(item);
if (list.length != Purchase.productIds.length) {
XXToast.toast(msg: 'Store unavailable'.tr);
return;
}
// 先获取之前的订阅
updateSub?.cancel();
errorSub?.cancel();
// const String userId = '';
final completer = Completer<void>();
showLoadingDialog();
updateSub =
FlutterInappPurchase.purchaseUpdated.listen((PurchasedItem? event) {
if (event == null) return;
// -- --- ----
_instance.finishTransaction(event);
// 订阅更新
debugPrint('$event');
if (item.productId == event.productId) {
switch (event.purchaseStateAndroid!) {
case PurchaseState.pending:
break;
case PurchaseState.purchased:
updateSub?.cancel();
androidPayInfo(event, onReport: () {
reportTransaction(item, event);
});
completer.complete();
break;
case PurchaseState.unspecified:
break;
}
}
});
errorSub =
FlutterInappPurchase.purchaseError.listen((PurchaseResult? event) {
// 出错
debugPrint('$event');
completer.completeError(event!);
hideLoadingDialog();
});
var oldSub = await _instance.getAvailablePurchases();
bool oldSubIsActive = oldSub != null && oldSub.isNotEmpty;
if (oldSubIsActive) {
// if (oldSub.any((e) => e.productId == item.productId)) {
// var match = oldSub.firstWhere((e) => e.productId == item.productId);
androidPayInfo(oldSub.first, onReport: () {});
// } else {
// // 旧订阅有效,但不是一个订阅,购买新的
// _instance.requestSubscription(
// item.productId!,
// prorationModeAndroid: 3,
// purchaseTokenAndroid: oldSub.first.purchaseToken,
// );
// }
} else {
_instance.requestSubscription(item.productId!);
}
}
/// 订阅成功以后,传递给后台 通过签名获取数据
androidPayInfo(PurchasedItem item, {required VoidCallback onReport}) {
var data = {
'deviceId': Global.deviceId,
'originalJson': item.transactionReceipt,
'signature': item.signatureAndroid,
};
MyRequest.post(Api.getPayInfo, data: data, needToken: false).then((value) {
// {status 1需要上报 0不需要上报
// environment 1正式 0测试}
var map = value.data['data'] as Map;
if (map['status'] == 1 && map['environment'] == 1) {
onReport();
}
_instance.finishTransaction(item);
Global.homeCon.login();
hideLoadingDialog();
Get.offAndToNamed(AppRoutes.Home);
}).catchError((p0) {
XXToast.toast(msg: '$p0');
});
}
}
pay_ios.dart
文件
单独写的iOS文件,如果安卓有单独的业务,在这里面进行使用.
class PayIOS extends PayPlatform {
static final _instance = FlutterInappPurchase.instance;
@override
Future<void> init() async {
super.init();
}
StreamSubscription? updateSub;
StreamSubscription? errorSub;
@override
dispose() {
updateSub?.cancel();
updateSub = null;
errorSub?.cancel();
errorSub = null;
_timer?.cancel();
super.dispose();
}
Timer? _timer;
@override
Future<void> pay(IAPItem item) async {
super.pay(item);
updateSub?.cancel();
errorSub?.cancel();
showLoadingDialog();
// const String userId = 'msmk';
await _instance.clearTransactionIOS();
// Completer _completer = Completer();
updateSub = FlutterInappPurchase.purchaseUpdated
.listen((PurchasedItem? event) async {
if (event == null) return;
// -- --- ----
_instance.finishTransaction(event);
// 订阅更新
debugPrint(
'[Purchase Update]:\n productId: ${event.productId} transactionId: ${event.transactionId} transactionState: ${event.transactionStateIOS} \n transactionDate: ${event.transactionDate}');
// if (event.transactionDate!.difference(dateTime).inSeconds < 0) {
// //
// return;
// }
if (item.productId == event.productId) {
_timer?.cancel();
_timer =
Timer.periodic(const Duration(milliseconds: 300), (timer) async {
timer.cancel();
switch (event.transactionStateIOS) {
case TransactionState.purchased:
case TransactionState.restored:
updateSub?.cancel();
hideLoadingDialog();
await applyVerify(event, onReport: () {
reportTransaction(item, event);
});
// _completer.complete();
break;
default:
hideLoadingDialog();
break;
}
});
}
});
errorSub =
FlutterInappPurchase.purchaseError.listen((PurchaseResult? event) {
// 出错
hideLoadingDialog();
_instance.clearTransactionIOS();
debugPrint('$event');
// _completer.completeError(event!);
});
// 检查是否还有有效的订阅
// GPTpurchaseType? purchase = await checkPurchase();
// List<DiscountIOS>? discounts = item.discountsIOS;
// if (discounts != null && discounts.isNotEmpty) {
// final offerSignature = await _appleSign(
// productId: item.productId!,
// offerId: discounts.first.identifier!,
// username: userId,
// );
// _instance
// .requestProductWithOfferIOS(item.productId!, userId, <String, Object>{
// "identifier": discounts.first.identifier!,
// "keyIdentifier": offerSignature.keyIdentifier,
// "nonce": offerSignature.nonce,
// "signature": offerSignature.signature,
// "timestamp": offerSignature.timestamp,
// });
// } else {
_instance.requestPurchase(item.productId!);
// }
}
// 苹果校验
Future<void> applyVerify(PurchasedItem item,
{required VoidCallback onReport}) async {
showLoadingDialog();
MyRequest.post(Api.appleVerify,
data: {
'deviceId': Global.deviceId,
//开发环境(沙盒:sandbox,正式:buy)
'env': 'sandbox',
'productID': item.productId,
'purchaseID': item.productId,
'transactionId': item.transactionId,
'originalTransactionId': item.originalTransactionIdentifierIOS,
'transactionDate': item.transactionDate?.millisecondsSinceEpoch,
// 'localVerificationData':
// appstoreDetail.verificationData.localVerificationData,
'serverVerificationData': item.transactionReceipt,
},
needToken: false)
.then((value) {
// {status 1需要上报 0不需要上报
// environment 1正式 0测试}
var map = value.data['data'] as Map;
if (map['status'] == 1 && map['environment'] == 1) {
onReport();
}
Global.homeCon.login();
Get.offAllNamed(AppRoutes.Home);
}, onError: (error) {
XXToast.toast(msg: '验证出错: $error');
}).whenComplete(() {
hideLoadingDialog();
_instance.finishTransaction(item);
});
hideLoadingDialog();
}
finishIAPTransaction() async {
if (Platform.isIOS) {
var transactions = await _instance.getPendingTransactionsIOS();
if (transactions != null) {
Get.log(
"iap _finishIAPTransaction transaction.length:${transactions.length}");
for (var transaction in transactions) {
Get.log(
"wztest _finishIAPTransaction transaction==>${transaction.transactionId}");
await _instance.finishTransaction(transaction);
Get.log("wztest _finishIAPTransaction transaction==>finished");
}
}
}
}
// ignore: unused_element
Future<_OfferSign> _appleSign(
{required String productId,
required String offerId,
required String username}) async {
// return Future.value(
// _OfferSign(
// keyIdentifier: 'R5DD89747P',
// nonce: '31eba0f6-ce40-48a4-bbab-fa5ddccaff42',
// signature:
// 'MEUCIBcLzYob+FT4nOtdopv8Q+v1r0bDhgZXZpP3XZbX+nPFAiEAskoXRnlqHgOBRyk99ICsSc7j5+FQxn1yw8RLwDjAiYs=',
// timestamp: 1679732536830,
// ),
// );
// 执行网络请求获取 签名
var res = await Dio().get('http://apple.linkwz.com/api/v1/getSign/msmk');
debugPrint('[Apple Sign]: - ${res.data["data"]}');
return Future.value(_OfferSign.fromJson(res.data as Map));
}
restore() async {
showLoadingDialog();
if (!await _instance.isReady()) {
await _instance.initialize();
}
List<PurchasedItem>? subList = await _instance.getAvailablePurchases();
if (subList != null && subList.isNotEmpty) {
applyVerify(subList.first, onReport: () {});
}
}
}
class _OfferSign {
final String keyIdentifier;
final String nonce;
final String signature;
final dynamic timestamp;
_OfferSign(
{required this.keyIdentifier,
required this.nonce,
required this.signature,
required this.timestamp});
static _OfferSign fromJson(Map json) {
var data = json['data'] as Map;
return _OfferSign(
keyIdentifier: data['keyIdentifier'],
nonce: data['nonce'],
signature: data['sign'],
timestamp: data['timestamp'],
);
}
}
大致使用就这样,其实拿过去就能用.有什么不懂得问题可以留言一起讨论.
喜欢的朋友点赞加关注给点鼓励哦!