Last active
October 13, 2023 04:16
-
-
Save d-sea/b3dee6d63cd68cd8eec88a246a500a92 to your computer and use it in GitHub Desktop.
flutter in_app_purchase products widget
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:io'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:in_app_purchase/in_app_purchase.dart'; | |
import 'package:cloud_functions/cloud_functions.dart'; | |
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | |
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; | |
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; | |
class PurchaseList extends StatefulWidget { | |
@override | |
State<PurchaseList> createState() => _PurchaseListState(); | |
} | |
class _PurchaseListState extends State<PurchaseList> { | |
final inAppPurchase = InAppPurchase.instance; | |
late StreamSubscription<List<PurchaseDetails>> _subscription; | |
List<ProductDetails> _products = <ProductDetails>[]; | |
bool _isPending = false; | |
@override | |
void initState() { | |
_isPending = true; | |
_getProducts(); | |
print('start initState()'); | |
final Stream<List<PurchaseDetails>> purchaseUpdated = inAppPurchase.purchaseStream; | |
_subscription = purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) { | |
_listenToPurchaseUpdated(purchaseDetailsList); | |
}, onDone: () { | |
_subscription.cancel(); | |
}, onError: (Object error) { | |
print('PurchaseList initState purchaseUpdated error: $error'); | |
return Container(); | |
}); | |
initStoreInfo(); | |
super.initState(); | |
} | |
Future<void> _getProducts() async { | |
final bool available = await inAppPurchase.isAvailable(); | |
if (!available) { | |
return []; | |
} | |
final _ids = <String>{ 'com.example.monthly.standard', 'com.example.yearly.standard' }; | |
final ProductDetailsResponse response = await inAppPurchase.queryProductDetails(_ids); | |
List<ProductDetails> products; | |
if (response.error != null) { | |
print('_getProducts response error : ${response.error!.message}'); | |
products = []; | |
} | |
if (response.notFoundIDs.isNotEmpty) { | |
print('_getProducts response.notFoundIDs.isNotEmpty: ${response.notFoundIDs}'); | |
products = []; | |
} else { | |
products = response.productDetails; | |
} | |
setState(() { | |
_products = products; | |
_isPending = false; | |
}); | |
} | |
Future<void> initStoreInfo() async { | |
if (Platform.isIOS) { | |
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = | |
inAppPurchase | |
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>(); | |
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); | |
} | |
} | |
@override | |
void dispose() { | |
if (Platform.isIOS) { | |
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = | |
inAppPurchase | |
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>(); | |
iosPlatformAddition.setDelegate(null); | |
} | |
_subscription.cancel(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: MediaQuery.of(context).size.height * 0.9, | |
child: SingleChildScrollView( | |
child: Column( | |
children: [ | |
Row( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
IconButton( | |
icon: Icon(Icons.close), | |
onPressed: () => Navigator.pop(context), | |
), | |
], | |
), | |
Text('有料プラン'), | |
Column(children: [ | |
Text('サービス説明文'), | |
_isPending | |
? Center(child: CircularProgressIndicator()) | |
: ListView.builder( | |
shrinkWrap: true, | |
physics: NeverScrollableScrollPhysics(), | |
padding: EdgeInsets.zero, | |
itemCount: _products.length, | |
itemBuilder: (_, i) { | |
ProductDetails product = _products[i]; | |
var _buttonView; | |
var _term; | |
if (product.id.contains('monthly')) { | |
_term = " / 月額"; | |
} else if (product.id.contains('yearly')) { | |
_term = " / 年額"; | |
} else { // 未使用 定期購入を非表示 | |
return Container(); | |
} | |
_buttonView = product.price + _term; | |
return Container( | |
child: Column( | |
children: [ | |
CupertinoButton.filled( | |
child: Text(_buttonView), | |
onPressed: () async { | |
PurchaseParam purchaseParam = PurchaseParam(productDetails: product); | |
try { | |
await inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam); | |
} catch(error) { | |
print('inAppPurchase.buyNonConsumable: $error'); | |
if (error.toString().contains('storekit_duplicate_product_object')) { | |
showDialog( | |
context: context, | |
builder: (context) { | |
return CupertinoAlertDialog( | |
title: Text('エラー'), | |
content: Text('すでに購入済みの商品です'), | |
actions: <Widget>[ | |
CupertinoDialogAction(child: Text("OK"), | |
onPressed: () => Navigator.pop(context),), | |
], | |
); | |
} | |
); | |
} | |
} | |
}), | |
Text(product.description), | |
], | |
), | |
); | |
} | |
), | |
],), | |
CupertinoButton( | |
child: Text('Close'), | |
onPressed: () => Navigator.pop(context), | |
), | |
], | |
), | |
), | |
); | |
} | |
Future<void> _listenToPurchaseUpdated( | |
List<PurchaseDetails> purchaseDetailsList) async { | |
print('start _listenToPurchaseUpdated'); | |
print('purchaseDetailsList.length: ${purchaseDetailsList.length}'); | |
if (purchaseDetailsList.isEmpty) { | |
print('purchaseDetailsList is empty'); | |
Navigator.pop(context); | |
showDialog( | |
context: context, | |
builder: (context) { | |
return CupertinoAlertDialog( | |
title: Text('エラー'), | |
content: Text( | |
'対象商品が見つかりませんでした\n' | |
'定期課金が停止されている可能性があります\n' | |
'購入処理をお願いします'), | |
actions: <Widget>[ | |
CupertinoDialogAction(child: Text("OK", key), | |
onPressed: () => Navigator.pop(context),), | |
], | |
); | |
} | |
); | |
} | |
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { | |
if (purchaseDetails.status == PurchaseStatus.pending) { | |
print('_listenToPurchaseUpdated purchaseDetails.status: pending'); | |
setState(() { | |
_isPending = false; | |
}); | |
} else { | |
if (purchaseDetails.status == PurchaseStatus.error) { | |
print("PurchaseStatus.error: ${purchaseDetails.error!.message}"); | |
setState(() { | |
_isPending = false; | |
}); | |
} else if (purchaseDetails.status == PurchaseStatus.purchased || | |
purchaseDetails.status == PurchaseStatus.restored) { | |
print('_listenToPurchaseUpdated purchaseDetails.status: purchased or restored'); | |
final result = await _verifyPurchase(purchaseDetails); | |
print('_verifyPurchase result: $result'); | |
if (result == BillingConst.SUCCESS) { | |
print('_listenToPurchaseUpdated _verifyPurchase: SUCCESS'); | |
deliverProduct(purchaseDetails); | |
} else { | |
print('_listenToPurchaseUpdated _verifyPurchase: ERROR'); | |
setState(() { | |
_isPending = false; | |
}); | |
return; | |
} | |
} | |
if (purchaseDetails.pendingCompletePurchase) { | |
print('completePurchase'); | |
await inAppPurchase.completePurchase(purchaseDetails); | |
} | |
} | |
} | |
} | |
Future<int> _verifyPurchase(PurchaseDetails purchaseDetails) async { | |
try { | |
if (Platform.isIOS) { | |
HttpsCallable verifyReceipt = FirebaseFunctions.instance.httpsCallable('verifyIos'); | |
final HttpsCallableResult result = await verifyReceipt.call( | |
{'data': purchaseDetails.verificationData.localVerificationData}); | |
print("Verify Purchase RESULT: " + result.data.toString()); | |
return result.data[BillingConst.result]; | |
} else if (Platform.isAndroid) { | |
HttpsCallable verifyReceipt = FirebaseFunctions.instance.httpsCallable('verifyAndroid'); | |
final HttpsCallableResult result = await verifyReceipt.call({ | |
'data': purchaseDetails.verificationData.localVerificationData}); | |
print("Verify Purchase RESULT: " + result.data.toString()); | |
return result.data[BillingConst.result]; | |
} | |
print("_verifyPurchase not matching OS"); | |
return BillingConst.UNEXPECTED_ERROR; | |
} catch (error) { | |
print('_verifyPurchase error: $error'); | |
return BillingConst.UNEXPECTED_ERROR; | |
} | |
} | |
Future<void> deliverProduct(PurchaseDetails purchaseDetails) async { | |
if (mounted) { | |
Navigator.pop(context); | |
} | |
} | |
void handleError(IAPError error) { | |
setState(() { | |
_isPending = false; | |
}); | |
} | |
} | |
class BillingConst { | |
static const String result = 'result'; | |
static const SUCCESS = 0; // 成功 (期限内) | |
static const EXPIRED = 1; // 期限切れ | |
static const DOCUMENT_NOT_FOUND = 2; // Firestoreにドキュメントなし | |
static const NO_AUTH = 3; // 認証情報なし | |
static const INVALID_RECEIPT = 4; // レシート情報が不正です | |
static const ALREADY_EXIST = 5; // 同じトランザクションが存在している | |
static const UNEXPECTED_ERROR = 99; // 不明なエラー | |
} | |
/// Example implementation of the | |
/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). | |
/// | |
/// The payment queue delegate can be implementated to provide information | |
/// needed to complete transactions. | |
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { | |
@override | |
bool shouldContinueTransaction( | |
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { | |
return true; | |
} | |
@override | |
bool shouldShowPriceConsent() { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment