Easy-to-use in-app purchases that can callback results and facilitate operational tracking
Features
- Configurable
- Callable function
Getting started
Install command
flutter pub add pp_purchase
Init method
//初始化所有内购对象
Future<void> initialize(List<String> productIds,{required String sharedSecret, bool showLog = false}) async
Load all products
//加载所有产品
SubPurchase.instance.loadProducts();
Load all purcharsed products
//加载所有已购买产品
SubPurchase.instance.loadPurchasedProducts();
Check product is purchased?
//判断产品是否已购买过
SubPurchase.instance.hasPurchased('vip_weekly');
Get latest purchase product
//获取最新购买的产品信息对象
SubPurchase.instance.latestPurchasedProducts;
Get latest purchase timestamp
//获取最后一次购买时间戳
SubPurchase.instance.lastPurchaseTimeMs;
Get all purchased products
//获取所有已购买的产品
SubPurchase.instance.allPurchasedProducts;
Usage
Step1: Create constants.dart
file
Config your subscription constants
// 订阅配置
static const String subs_weekly = 'vip_weekly';
static const String subs_weekly_inapp = 'vip_weekly_inapp';
static const String subs_annual_inapp = 'vip_annually_inapp';
static const String subs_lifelong_inapp = 'lifelong_vip';
static const List<String> subs_productIds = [
subs_weekly,
subs_weekly_inapp,
subs_annual_inapp,
subs_lifelong_inapp
];
static const List<String> subsOneTimeIds = [subs_lifelong_inapp];
static const String sharedSecret = 'your shared secret value';
static const bool showSubLogs = true;
Step2: First launch call init
You can create a global singleton file and save the code.
// 最后一次购买时间
int _lastPurchaseTime = 0;
int get lastPurchaseTime => _lastPurchaseTime;
Future<void> setLastPurchaseTime(int value) async {
if (_lastPurchaseTime != value) {
_lastPurchaseTime = value;
//Use Shared Preferences to save value
await getSp().setInt('lastPurchaseTime', value);
}
}
void initInAppPurchase() {
checkVip();
if (!isVip) {
Logger.trace('SubsPurchase 检测到不是会员,初始化订阅');
// init
SubsPurchase.instance.initialize(Consts.subs_productIds, Consts.subsOneTimeIds,
sharedSecret: Consts.sharedSecret, showLog: Consts.showSubLogs);
SubsPurchase.instance.lastPurchaseTimeMs = lastPurchaseTime;
// Request all purchased products detail infomation
SubsPurchase.instance.loadPurchasedProducts().then((value) {
// After loaded purchased logic code
//refreshVip();
Logger.trace('uploadsign 初始化首次订阅信息');
initFirstBuyInfo();
});
SubsPurchase.instance.loadProducts();
} else {
Logger.trace('SubsPurchase 检测到是会员,不初始化订阅');
}
}
Future<bool> initFirstBuyInfo() async {
//获取首次订阅信息
final allPurchasedProducts = SubsPurchase.instance.allPurchasedProducts;
int firstPurchaseTime = DateTime.now().millisecondsSinceEpoch;
String? firstOriginalTransactionId;
String? firstOriginalPurchaseDateMs;
if (allPurchasedProducts.isEmpty) {
Logger.trace('uploadsign 首次订阅信息为空');
return false;
}
for (var element in allPurchasedProducts) {
final purchaseTime = int.parse(element['purchase_date_ms'].toString());
if (purchaseTime < firstPurchaseTime) {
firstPurchaseTime = purchaseTime;
firstOriginalTransactionId = element['original_transaction_id'];
firstOriginalPurchaseDateMs = element['original_purchase_date_ms'];
await SharepUtil.setString(
'firstBuyOriginTransactionId', firstOriginalTransactionId!);
await SharepUtil.setString(
'firstBuyOriginalPurchaseDateMs', firstOriginalPurchaseDateMs!);
Logger.trace(
'uploadsign 首次订阅时间: ${DateTime.fromMillisecondsSinceEpoch(int.parse(firstOriginalPurchaseDateMs))}');
Logger.trace('uploadsign 首次订阅原始交易ID: $firstOriginalTransactionId');
}
}
return true;
}
Step3: Purchase product
SubsPurchase.instance.purchaseProduct(
productId,
callback: (result) {
//判断是否已经是会员,如果是,则不继续处理购买结果
if (app.isVip) {
Logger.trace('SubsPurchase 已经是会员,不处理购买结果');
return;
}
// 处理购买结果
String message;
switch (result.status) {
case IAPPurchaseStatus.purchasing:
message = '${result.message}...';
break;
case IAPPurchaseStatus.verifying:
message = '正在验证购买凭据...';
EasyLoading.show(status: 'IAP_Verifying'.localized);
break;
case IAPPurchaseStatus.verifyingFailed:
message = '验证失败(无统计价值): ${result.message}';
break;
case IAPPurchaseStatus.canceled:
message = '购买已取消';
global.logEvent('iap_buy_cancel');
EasyLoading.dismiss();
break;
case IAPPurchaseStatus.purchased:
final purchaseData = result.getDataAs<Map<String, dynamic>>();
if (purchaseData == null) {
return;
}
message = '购买成功\n交易日期: $purchaseData';
global.logEvent('iap_buy_ok');
app.refreshVip();
app.initFirstBuyInfo().then((value) {
if (value) {
Logger.trace('uploadsign 上传首次订阅数据 in subs purchase');
global.uploadFirstSubsData();
}
});
Get.back();
EasyLoading.dismiss();
break;
case IAPPurchaseStatus.purchaseFailed:
message = '购买失败: ${result.message}';
global.logEvent('iap_buy_failed');
EasyLoading.dismiss();
PPAlert.showSysAlert(
'IAP_PurchaseFailedTitle'.localized,
'IAP_PurchaseFailedMsg'.localized,
onOK: () {},
);
break;
case IAPPurchaseStatus.systemError:
message = '购买过程系统错误: ${result.message}';
global.logEvent('iap_buy_error');
EasyLoading.dismiss();
PPAlert.showSysConfirm(
title: 'IAP_PurchaseErrorTitle'.localized,
text: 'IAP_PurchaseErrorMsg'.localized,
cancelText: 'Close'.localized,
okText: 'Details'.localized,
onConfirm: () {
Get.toNamed(AppPages.subsReport, arguments: result);
},
);
break;
case IAPPurchaseStatus.crashes:
message = '购买过程崩溃: ${result.message}';
global.logEvent('iap_buy_crash');
EasyLoading.dismiss();
PPAlert.showSysConfirm(
title: 'IAP_PurchaseCrashTitle'.localized,
text: 'IAP_PurchaseCrashMsg'.localized,
cancelText: 'Close'.localized,
okText: 'Details'.localized,
onConfirm: () {
Get.toNamed(AppPages.subsReport, arguments: result);
},
);
break;
default:
message = '其他状态: ${result.status}';
EasyLoading.dismiss();
break;
}
Logger.trace('SubsPurchase inapp ${result.status.text} $message');
},
);
Step4: Restore product
SubsPurchase.instance.restorePurchases(callback: (result) {
//判断是否已经是会员,如果是,则不继续处理购买结果
if (app.isVip) {
Logger.trace('SubsPurchase 已经是会员,不处理购买结果');
return;
}
// 处理购买结果
String message;
switch (result.status) {
case IAPPurchaseStatus.verifying:
message = '正在验证购买凭据...';
EasyLoading.show(status: 'IAP_Verifying'.localized);
break;
case IAPPurchaseStatus.verifyingFailed:
message = '验证失败: ${result.message}';
break;
case IAPPurchaseStatus.restored:
final purchaseData = result.getDataAs<Map<String, dynamic>>();
if (purchaseData == null) {
return;
}
message = '恢复购买成功\n交易日期: $purchaseData';
global.logEvent('iap_restore_ok');
app.refreshVip();
app.initFirstBuyInfo().then((value) {
if (value) {
Logger.trace('uploadsign 上传首次订阅数据 in subs restore');
global.uploadFirstSubsData();
}
});
Get.back();
EasyLoading.dismiss();
break;
case IAPPurchaseStatus.restoreFailed:
message = '恢复购买失败: ${result.message}';
global.logEvent('iap_restore_failed');
EasyLoading.dismiss();
PPAlert.showSysAlert(
'IAP_RestoreFailedTitle'.localized,
'IAP_RestoreFailedMsg'.localized,
onOK: () {},
);
break;
case IAPPurchaseStatus.systemError:
message = '系统错误: ${result.message}';
global.logEvent('iap_restore_error');
EasyLoading.dismiss();
PPAlert.showSysConfirm(
title: 'IAP_PurchaseErrorTitle'.localized,
text: 'IAP_PurchaseErrorMsg'.localized,
cancelText: 'Close'.localized,
okText: 'Details'.localized,
onConfirm: () {
Get.toNamed(AppPages.subsReport, arguments: result);
},
);
break;
case IAPPurchaseStatus.crashes:
message = '购买或恢复崩溃: ${result.message}';
global.logEvent('iap_restore_crash');
EasyLoading.dismiss();
PPAlert.showSysConfirm(
title: 'IAP_PurchaseCrashTitle'.localized,
text: 'IAP_PurchaseCrashMsg'.localized,
cancelText: 'Close'.localized,
okText: 'Details'.localized,
onConfirm: () {
Get.toNamed(AppPages.subsReport, arguments: result);
},
);
break;
default:
message = '其他状态: ${result.status}';
EasyLoading.dismiss();
break;
}
Logger.trace('SubsPurchase inapp ${result.status.text} $message');
});
Simple Example
Include short and useful examples for package users. Add longer examples
to /example
folder.
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:pp_purchase/pp_purchase.dart';
class SubsDemoPage extends StatefulWidget {
const SubsDemoPage({super.key});
@override
State<SubsDemoPage> createState() => _SubsDemoPageState();
}
class _SubsDemoPageState extends State<SubsDemoPage> {
final _subsPurchase = SubsPurchase.instance;
List<ProductDetails> _products = [];
bool _isLoading = true;
String _status = '';
// 定义产品ID
static const List<String> _productIds = [
'weekly_vip',
'annually_vip',
];
@override
void initState() {
super.initState();
_initPurchase();
}
Future<void> _initPurchase() async {
// 可以在启动时初始化,也可以在需要时初始化,根据功能需求需要
await _subsPurchase.initialize(_productIds,
sharedSecret: 'testf80cba1241718168ddde807test', showLog: true);
final products = await _subsPurchase.loadProducts();
setState(() {
_products = products;
_isLoading = false;
});
}
void _purchase(String productId) {
setState(() => _status = '正在购买...');
_subsPurchase.purchaseProduct(
productId,
callback: (result) {
//判断是否已经是会员,如果是,则不继续处理购买结果
// 处理购买结果
String message;
switch (result.status) {
case IAPPurchaseStatus.purchasing:
message = '购买中...';
break;
case IAPPurchaseStatus.verifying:
message = '正在验证购买凭据...';
break;
case IAPPurchaseStatus.verifyingFailed:
message = '验证失败(无统计价值): ${result.message}';
break;
case IAPPurchaseStatus.canceled:
message = '购买已取消';
break;
case IAPPurchaseStatus.purchased:
final purchaseData = result.getDataAs<Map<String, dynamic>>();
message = '购买成功\n交易日期: $purchaseData';
break;
case IAPPurchaseStatus.purchaseFailed:
message = '购买失败: ${result.message}';
break;
case IAPPurchaseStatus.systemError:
message = '购买过程系统错误: ${result.message}';
break;
default:
message = '其他状态: ${result.status}';
break;
}
setState(() => _status = message);
print('subslog 1 ${result.status.text}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
);
}
void _restorePurchases() {
setState(() => _status = '正在恢复购买...');
_subsPurchase.restorePurchases(
callback: (result) {
//判断是否已经是会员,如果是,则不继续处理购买结果
// 处理购买结果
String message;
switch (result.status) {
case IAPPurchaseStatus.verifying:
message = '正在验证购买凭据...';
break;
case IAPPurchaseStatus.verifyingFailed:
message = '验证失败: ${result.message}';
break;
case IAPPurchaseStatus.restored:
final purchaseData = result.getDataAs<Map<String, dynamic>>();
message = '恢复购买成功\n交易日期: $purchaseData';
break;
case IAPPurchaseStatus.restoreFailed:
message = '恢复购买失败: ${result.message}';
break;
default:
message = '其他状态: ${result.status}';
break;
}
setState(() => _status = message);
print('subslog 2 ${result.status.text}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('订阅演示'),
actions: [
IconButton(
icon: const Icon(Icons.restore),
onPressed: _restorePurchases,
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_status,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(product.title),
subtitle: Text(product.description),
trailing: Text(
product.price,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
onTap: () => _purchase(product.id),
),
);
},
),
),
],
),
);
}
}