一、有关Apple内购
1. SKStorefront
:包含App Store店面
位置和唯一标识符的对象。
您可以通过 App Store Connect
创建的应用内产品可在每个拥有App Store
的地区销售。您可以使用店面信息来确定客户所在的地区,并提供适合该地区的应用内产品。您必须维护自己的产品标识符列表以及要在其中提供它们的店面。
2.SKPaymentQueueDelegate
<1>shouldContinueTransaction
: 当设备的App Store店面
在交易期间发生更改时(比如:设备的App Store
原先登录的是中国地区的店面,在交易期间切换到了美国地区的店面),询问是否继续交易:
- 返回
true
:在更新后的店面中继续交易。 - 返回
false
:停止交易,此时会报出错误SKErrorStoreProductNotAvailable
。该情况下请考虑提示用户该产品在当前店面中不可用。
<2>shouldShowPriceConsent
:当App Store Connect
中的订阅价格已更改,且订阅者尚未采取任何行动时,询问是否立即显示价格变化同意书:
- 返回
true
:立即通知系统显示价格同意书。 - 返回
false
:系统不显示价格同意书。
此方法仅适用于需要客户同意的
自动续订订阅价格
上涨。当你提高自动续订订阅的价格并需要客户同意时,Apple会通过电子邮件、推送通知和应用内价格同意书通知受影响的订阅者,并要求他们同意新价格。如果客户不同意或不采取任何行动,他们的订阅将在当前计费周期结束时到期。
3.如何沙盒测试
<1>进入沙盒测试员,点击添加按钮,需要注意的是沙盒账号必须是没有注册过App Store和沙盒账号的邮箱
,邮箱可以随便填,记住邮箱密码就可以。
<2>打开手机的设置 --> 点击App Store --> 往下滑有个沙盒账户 --> 登录 --> 然后Apple ID安全,点击其他选项,选择不升级。
<3>如果你的App Store下面没有沙盒账户,说明你的手机之前没有做过沙盒测试,那就连接你的电脑运行一下,点击测试购买的商品,会弹出提示框让你登录沙盒账户。
4.充值成功后不走购买成功的回调
完成购买当用户付款成功后会弹出上面的弹窗,只有当用户点击“好”才会触发成功``的回调。如果这时候客户不点击"好",将app杀死或者卸载,那么服务器是不知道用户在IAP服务器上已经完成付款了,是不会给客户发货的。
5.漏单处理
https://blog.csdn.net/pengyuan_D/article/details/121335745
https://www.devfutao.com/archives/224/
- 利用苹果事务机制保存交易,重启App后自动补单。
- 网络异常重试,减少漏单。
- 使用applicationUsername传递
orderNo
时,orderNo
会有丢失的情况,此时需要服务端根据用户之前创建的订单重新获取。
处理漏单的关键点:在确认服务端收到
receipt
并验证成功之前不要结束订单,即不要调用finishTransaction
结束该笔订单。
处理漏单顺序:当客户端产生了多个漏单,向服务端验证时,服务端向苹果服务器获取的验证数据
in_app
会包含所有未处理的订单,最后产生的订单数据在最后,客户端处理漏单顺序和服务端验证顺序要保持一致。
6.票据
<1>如何使用cancellation_data
字段?
该字段仅适用于自动续期订阅、非续期订阅和非消耗型产品。当用户申请退款(Refund
)或撤销家庭共享(Family Sharing
)时,票据校验返回的JSON数据中才会有该字段。因此可以利用该字段监测用户退款,并及时收回已经发放的产品或服务。
<2>如何选择票据校验地址?
测试阶段使用沙盒地址:
https://sandbox.itunes.apple.com/verifyReceipt
在App Store
发布之后使用正式地址:
https://buy.itunes.apple.com/verifyReceipt
最佳实践:无论是测试阶段还是正式发布阶段,总是先去正式环境校验,如果返回
21007
状态码,再去沙盒环境校验,这样无需在测试、审核、发布等各个阶段频繁切换地址。
<3>如何处理appStoreReceiptURL
为空的情况?
[NSBundle mainBundle].appStoreReceiptURL
只是一个URL,当用户付款成功后,系统会把receipt
写入到这个位置。取receipt
时需要判空,如果文件不存在,就需要从苹果服务器重新刷新下载了。
SKReceiptRefreshRequest
刷新系统会弹窗让用户授权,而用户会取消授权,App必须要能正确处理取消授权的情况。若刷新成功,拿到票据走正常验证发货流程;若刷新失败时App应释放该请求,而不是尝试再次调用它。
出现这种情况的一种典型场景:从TestFlight
或Xcode
安装App。当从App Store
安装或从iCloud
恢复时,appStoreReceipt
将始终存在,但是在一些未知情况票据确实不存在。
<4>校验票据时,返回的结果中的in_app
是一个空数组,而不是预期的产品?
空in_app
数组表示App Store
尚未记录用户的任何交易,可能票据未更新导致的。
消耗性产品在购买成功后会添加进票据,在finishTransaction
成后后从交易队列移除,在下次更新票据时正式从票据中移除。而自动续期订阅、非续期订阅和非消耗型产品自购买成功后就会永久保留在票据中,如果应用只提供消耗型产品,那么在票据当前没有产品并且本次购买还未来得及更新时就拿去校验,就会出现空数组的情况。
出现这种情况,可以使用SKReceiptRefreshRequest
来显示刷新票据。
7.App Store Server API(重要)
<1>新特征:
2021年In App Purchase
迎来了重大变化,苹果推出了StoreKit2
、App Store Server API
、App Store Server Notifications V2
三大特征。
苹果将原先旧的内购变为Original API for in-app purchase
,引入了全新的内购In-App Purchase(即StoreKit2)
,StoreKit2
是基于Swift
的API,从iOS15
开始提供。
<2>appAccountToken:
其中最大的变化之一是新增了一个appAccountToken
字段,用于开发者将交易与自己服务器上的用户关联起来的UUID
。当开发者发起应用内购买时传入appAccountToken
,该值会永久的在交易信息里面保存。
而且苹果打通了applicationUsername
和appAccountToken
,当用Original StoreKit
创建订单时,applicationUsername
字段赋值使用 UUID
格式内容时,则可以在服务端通知或者解析receipt
票据时,可以获取这个UUID
值,也就是订单可以关联确认:
- 在
App Store Server API
中,JWSTransactionDecodedPayload对象会返回applicationUsername
在appAccountToken
字段中。 - 在
App Store Server Notifications
中,JWSTransactionDecodedPayload对象会返回applicationUsername
在appAccountToken
字段中。 - 当您调用verifyReceipt接口来验证票据时,
App Store
服务器会返回applicationUsername
在responseBody.Latest_receipt_info中的app_account_token
字段中。
注意:当使用
verifyReceipt
验证票据时,如果是消耗型产品,返回的responseBody
里面没有latest_receipt_info
字段。
注意:苹果不保证非
UUID
格式的applicationUsername
属性会在交易里面持续存在。
<3>使用新的验证方式:
由于使用票据receipt
有许多不合理的地方,我们可以放弃旧的读取本地receipt
传给服务端验证的方式,直接采用App Store Server API
中的Get Transaction Info API,将transaction_id
传递给苹果服务器进行验证票据。
在创建订单时,自己的服务器会返回一个UUID
与订单绑定,当发起交易时将该UUID
通过applicationUsername
传给App Store
。校验时,自己的服务器通过Get Transaction Info
API 获取到App Store
回传的UUID
,此时就可以与自己服务器的订单相关联从而进行充值。
二、有关Google内购
1.设定商品的定价
Google Play Console
--> 设定 --> 定价范本
2.建立产品
Google Play Console
--> 产品 --> 应用程式内产品 --> 建立产品
建立好产品后,一定需要启用,否则App会读取不到数据。
3.封闭测试
<1>Google Play Console
--> 测试 --> 封闭测试 --> 建立测试群组
<2> 管理测试群组 --> 版本 --> 建立封闭测试版本 --> 上传测试版本(aab文件)
上传测试版本(aab文件)安装在手机上的包一定要和上传的包的版本号、build号、签名一致。
<3>管理测试群组 --> 测试人数 --> 建立電子郵件名單
添加测试人员待审核通过后,复制链接发送给测试人员,让他接受邀请,否则测试人员会拉取不到订单数据。
4.授权测试
Google Play Console
--> 设定 --> 授权测试 --> 授权回应RESPOND_NORMALLY
5.Purchase
交易订单
-
getPurchaseState()
:交易状态。PENDING
:待处理,PURCHASED
:已购买,UNSPECIFIED_STATE
:未知状态。 -
getOrderId()
:交易唯一订单ID,PENDING
状态为null,PURCHASED
状态则填充该订单ID,例如GPA.3356-0813-8427-26633
。 -
getPackageName()
: App的包名,例如com.example.test
。 -
getPurchaseTime()
:购买产品的时间,单位是毫秒时间戳。对于订阅则是订阅的注册时间,例如1691131391062
。 -
getPurchaseToken()
:交易令牌,唯一标识用户和交易。 -
isAutoRenewing()
:订阅是否自动续订。如果为true
,则订阅处于有效状态,并在下一个计费日期自动续订。如果为false
,则表明用户已取消订阅,在下一个计费日订阅到期。 -
isAcknowledged()
:交易是否被确认,购买成功后需要在3天内向Google服务器
确认,否则Google Play
会自动退款。 -
getAccountIdentifiers()
:发起交易时自定义传值,用于与用户账户唯一关联的ID。Google Play
可以使用它来监测不规则活动,例如许多设备在短时间内使用同一账户进行购买。
6.PENDING
待处理状态的购买交易(一次性商品)
PENDING
状态并不是等待支付的状态,而是客户端完成支付操作后,Google
在成功扣款前,这笔购买交易将会处于待处理状态。可在测试时选择慢速测试卡,几分钟后批准,此时的状态会是PENDING
。
对于一次性商品,只有处于PENDING
状态的购买交易才可取消。如果处于PURCHASED
状态的一次性商品发生退款,需要通过Voided Purchases API获知。
-
PENDING --> PURCHASED
:当用户完成待处理的一次性商品购买交易时,Google Play
会发送一条类型为ONE_TIME_PRODUCT_PURCHASED
的OneTimeProductNotification
RTDN消息。当购买状态从PENDING
转换为PURCHASED
时,3天的确认期限才会开始。 -
PENDING --> CANCELED
:当用户取消待处理的一次性商品时,Google Play
会发送一条类型为ONE_TIME_PRODUCT_CANCELED
的OneTimeProductNotification
RTDN消息。例如,如果用户未在规定时间内完成付款,就可能发生这种情况。
注意:当选择慢速测试卡,几分钟后拒绝购买,客户端并没有收到取消的通知,即没有在
PurchasesUpdatedListener
里面监测到交易状态的取消更新。
7.在后端处理购买交易(一次性商品)
APIpurchases.products:get
返回的结果:
{
"kind": string,
/// 购买产品的时间戳
"purchaseTimeMillis": string,
/// 订单状态,0:已购买 1:已取消 2:待处理
"purchaseState": integer,
/// 消耗状态,0:未消耗 1:已消耗
"consumptionState": integer,
/// 开发人员自定义的字段
"developerPayload": string,
/// 订单ID
"orderId": string,
/// 购买类型,0:测试 1:促销 2:激励广告
"purchaseType": integer,
/// 确认状态,0:未确认 1:已确认
"acknowledgementState": integer,
/// 交易令牌,可能不存在
"purchaseToken": string,
/// 产品ID,可能不存在
"productId": string,
/// 产品数量
"quantity": integer,
/// 发起交易时自定义传值,用于与用户账户唯一关联的ID(适用于应用内商品的购买交易)
"obfuscatedExternalAccountId": string,
/// 发起交易时自定义传值,用于与用户个人资料唯一关联的ID(适用于服务器端的订阅)
"obfuscatedExternalProfileId": string,
/// 结算区域
"regionCode": string
}
消耗品返回的结果:
{
"purchaseTimeMillis": "1691131391062",
"purchaseState": 1,
"consumptionState": 0,
"developerPayload": "",
"orderId": "GPA.3356-0813-8427-26633",
"purchaseType": 0,
"acknowledgementState": 0,
"kind": "androidpublisher#productPurchase",
"obfuscatedExternalAccountId": "自己服務器上的訂單號",
"regionCode": "TW"
}
<1> 如果验证购买交易,必须先检查交易状态是否为PURCHASED
。
<2>purchaseToken
具有全局唯一性,所以应该使用purchaseToken
来作为是否已经发放产品的唯一值。
<3>验证当前购买交易的purchaseToken
是否有效,若有效才发放产品。
<4>发放产品后需要进行消耗或确认购买交易。
- 消耗型产品
对于消耗型产品首先需确保购买交易未被消耗,查看API
Purchases.products:get调用结果中的consumptionState
,若未被消耗必须调用API
Purchases.products:consume进行消耗。
- 非消耗型产品
对于非消耗型产品首先需确保购买交易未被确认,查看API
Purchases.products:get调用结果中的acknowledgementState
,若未被确认必须调用API
Purchases.products:acknowledge进行确认。
- 订阅产品
对于订阅产品首先需确保购买交易是否被确认,查看API
Purchases.subscriptions调用结果中的acknowledgementState
,若未被确认必须调用API
Purchases.subscriptions.acknowledge进行确认。所有初始化订阅购买交易都需要确认,订阅续订不需要确认。
注意:请务在购买交易状态为
PENDING
时发放产品。
注意:请务使用
orderId
检查是否存在重复的购买交易或将其作为数据库中的主键,因为不能保证所有购买交易都会生成orderId
。特别是,使用促销代码完成的购买交易不会生产orderId
。
三.上代码(仅供参考)
// 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();
}
/// 发起支付请求
/// @productId: 产品ID
/// @existedOrderNo:自己服务器产生的订单号,若有值则不重新创建订单,可用于补单时
/// @pointId: 选择的点数套餐,创建订单时必传
static Future<void> startPay({required String productId, String? existedOrderNo, int? pointId}) async {
YZToastUtil.showLoading(message: '正在創建訂單');
/// 检测内购是否可用
final available = await InAppPurchase.instance.isAvailable();
if (!available) {
YZToastUtil.showMessage('不支持內購功能');
return;
}
/// 查询产品id是否在服务器上注册了
final response = await InAppPurchase.instance.queryProductDetails({productId});
if (response.error != null) {
YZToastUtil.showMessage(response.error!.message);
return;
}
/// 未查询到产品
if (response.productDetails.isEmpty) {
YZToastUtil.showMessage('暫無產品');
return;
}
/// 創建訂單
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服务器不会保存
/// android: 自己服務器上生成的訂單號
/// iOS: 自己服务器上生成的与订单绑定的uuid字符串
applicationUserName: Platform.isIOS ? orderUuidNo : orderNo,
);
/// 发起支付请求
try {
await InAppPurchase.instance.buyConsumable(
purchaseParam: purchaseParam,
/// 安卓不自动消耗,会在后台服务器进行消耗
autoConsume: !Platform.isAndroid,
);
YZToastUtil.dismiss();
} on PlatformException catch(e) {
/// iOS重复订单: storekit_duplicate_product_object,可根据自己的业务做处理
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系统
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) {
/// 订单未付款,此时需要提醒用户完成购买,否则无法进行下一笔订单
_handleGooglePendingPurchase(purchaseDetails);
} else if (purchaseDetails.billingClientPurchase.purchaseState == PurchaseStateWrapper.purchased) {
/// 订单已付款,向服务器验证订单票据
await _verifyPurchase(purchaseDetails);
}
}
}
}
}
/// 监听内购更新
Future<void> _listenToPurchaseUpdated(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
/// 等待购买中
print('等待购买中');
/// iOS系统此时正处于拉起支付状态,需要比较长的时间,此处给一个提示
/// Android系统此时正处于支付操作完成,等待Google批准状态
if (Platform.isIOS) {
YZToastUtil.showLoading(message: '正在拉起支付');
}
} else {
if (Platform.isIOS) {
YZToastUtil.dismiss();
}
if (purchaseDetails.status == PurchaseStatus.error) {
/// 购买出错,提示错误信息
print('购买出错:${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) {
/// 购买成功或恢复购买,向服务器验证订单票据
print('购买成功或恢复购买:${purchaseDetails.status}');
await _verifyPurchase(purchaseDetails);
}
}
}
/// 发放产品
void _deliverProduct() {
context.read<CommonMemberViewModel>().updateLocalMemberInfo();
}
/// 将Apple订单标记为已完成
void _completeApplePurchase(PurchaseDetails purchaseDetails) {
/// 只需要iOS,Android会在后台进行消耗或确认操作
if (purchaseDetails is AppStorePurchaseDetails) {
if (purchaseDetails.pendingCompletePurchase) {
_inAppPurchase.completePurchase(purchaseDetails);
}
}
}
/// 将Google订单标记为已消耗
void _completeGooglePurchase(PurchaseDetails purchaseDetails) {
if (purchaseDetails is GooglePlayPurchaseDetails) {
/// 在Android系统,[_inAppPurchase.completePurchase(purchaseDetails)]方法调用的是确认API[acknowledge],
/// 所以对于消耗品,如果您需要在客户端进行消耗,您需要调用消耗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: '檢測到您有未完成訂單,是否繼續?',
onConfirm: () {
startPay(productId: productId, existedOrderNo: orderNo);
},
).show(context);
}
/// 向服务器验证订单票据
Future<void> _verifyPurchase(PurchaseDetails purchaseDetails) async {
YZToastUtil.showLoading(message: '正在驗證訂單');
if (purchaseDetails is AppStorePurchaseDetails) {
/// 向服务器验证票据
final params = <String, dynamic>{
'transaction_id': purchaseDetails.skPaymentTransaction.transactionIdentifier,
};
await PurchaseApi.verifyApplePay(params: params).then((value) {
/// 验证成功,发放产品
_deliverProduct();
_completeApplePurchase(purchaseDetails);
YZToastUtil.showMessage('儲值成功');
}).catchError((e) {
YZToastUtil.dismiss();
/// 4000006:无效的交易id 4040010:没有找到交易id 4290000:请求超出速率限制 5000000:服务器错误 5000001:服务器错误 42:没有找到订单
if (e is YZNetworkError) {
if (e.code == 4000006 || e.code == 4040010 || e.code == 42) { /// 结束订单
YZToastUtil.showMessage('無效訂單,請聯絡客服');
_completeApplePurchase(purchaseDetails);
} else { /// 重新验单
_retryVerifyPurchase(purchaseDetails);
}
}
});
} else if (purchaseDetails is GooglePlayPurchaseDetails) {
/// 向服务器验证票据
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) {
/// 验证成功,发放产品
_deliverProduct();
YZToastUtil.showMessage('儲值成功');
}).catchError((e) {
YZToastUtil.dismiss();
if (e is YZNetworkError) {
/// 40:验证失败 41:PENDING状态未支付 42:没有找到订单 43:订单不匹配
if (e.code == 41) { /// 提醒用户继续完成交易
_handleGooglePendingPurchase(purchaseDetails);
} else if (e.code == 42 || e.code == 43) { /// 结束订单
YZToastUtil.showMessage('無效訂單,請聯絡客服');
_completeGooglePurchase(purchaseDetails);
} else { /// 重新验单
_retryVerifyPurchase(purchaseDetails);
}
}
});
}
}
/// 验证订单票据失败不要将订单标记为已完成,会通过以下两种方法进行补单:
/// 1.以斐波那契数列为间隔时间尝试10次重新验证订单票据
/// 2.在App下次启动时,监听未完成订单
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 = () {
/// 重试成功,发放产品
_deliverProduct();
_completeApplePurchase(purchaseDetails);
YZToastUtil.showMessage('儲值成功');
};
/// 40:验证失败 41:PENDING状态未支付 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 = () {
/// 重试成功,发放产品
_deliverProduct();
YZToastUtil.showMessage('儲值成功');
};
/// 4000006:无效的交易id 4040010:没有找到交易id 4290000:请求超出速率限制 5000000:服务器错误 5000001:服务器错误 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;
}
}
}
}
/// 重新验证订单票据间隔时间, 以斐波那契数列为间隔时间尝试10次重新验证订单票据
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();
}
}
网友评论