Skip to content

Instantly share code, notes, and snippets.

@d-sea
Last active October 13, 2023 04:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d-sea/b3dee6d63cd68cd8eec88a246a500a92 to your computer and use it in GitHub Desktop.
Save d-sea/b3dee6d63cd68cd8eec88a246a500a92 to your computer and use it in GitHub Desktop.
flutter in_app_purchase products widget
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