Flutter in_app_purchase StoreKit 製品IDが見つからないエラー問題
下記の問題で困っているのですが、同じ現象で解決できた方がいらっしゃいましたら、
教えていただけると嬉しいです😭
### 実現したいこと
Flutterのin_app_purchaseパッケージを使ってiOSアプリにサブスクリプション機能を実装したい。
サブスクリプション製品の一覧を表示させ、決済画面に移行させるためのコードの実装したい。
参考画像:リンク内容
### 前提
◯今回の機能実装のため見直し・変更したファイル
・subscription.dart(下記のコードを記述)
・pubspec.yaml(in_app_purchaseパッケージを定義)
・Podfile(platform:ios, '18.0'を設定)
・StoreKitConfigSample.storekit
### 発生している問題・エラーメッセージ
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Null check operator used on a null value
#0 SKPriceLocaleMessage.decode (package:in_app_purchase_storekit/src/messages.g.dart:405:29)
#1 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:579:37)
#2 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#3 StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:520:23)
#4 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:585:22)
#5 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#6 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:577:40)
#7 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#8 StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:520:23)
#9 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:585:22)
#10 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#11 StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:520:23)
#12 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:585:22)
#13 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#14 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:575:49)
#15 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#16 StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:520:23)
#17 _PigeonCodec.readValueOfType (package:in_app_purchase_storekit/src/messages.g.dart:585:22)
#18 StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:475:12)
#19 StandardMessageCodec.decodeMessage (package:flutter/src/services/message_codecs.dart:339:28)
#20 BasicMessageChannel.send (package:flutter/src/services/platform_channel.dart:218:18)
#21 InAppPurchaseAPI.startProductRequest (package:in_app_purchase_storekit/src/messages.g.dart:728:48)
#22 SKRequestMaker.startProductRequest (package:in_app_purchase_storekit/src/store_kit_wrappers/sk_request_maker.dart:32:9)
#23 InAppPurchaseStoreKitPlatform.queryProductDetails (package:in_app_purchase_storekit/src/in_app_purchase_storekit_platform.dart:207:18)
#24 _SubscriptionPageState.initStoreInfo (package:whatscook_flutter/subscription.dart:604:5)
### 該当のソースコード
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
// ----------------------------------------------------------
// <コード説明>
// 本クラスはサブスクリプションページの画面を構成し、iOS向けの
// InAppPurchaseStoreKitPlatformを利用して商品情報を取得、
// 購入・復元処理を行うためのUIとロジックを含みます。
// ----------------------------------------------------------
class SubscriptionPage extends StatefulWidget {
@override
State createState() => _SubscriptionPageState();
}
// ----------------------------------------------------------
// <コード説明>
// _SubscriptionPageState は、サブスク画面の実装(StatefulWidgetの状態保持)を担い、
// ここでストア接続、商品情報取得、購入ハンドリングを行います。
// ----------------------------------------------------------
class _SubscriptionPageState extends State {
// 商品ID例
static const String _kSilverSubscriptionId = 'com.monthly';
static const String _kGoldSubscriptionId = 'com.yearly';
static const List _kProductIds = [
_kSilverSubscriptionId,
_kGoldSubscriptionId,
];
// iOS向け InAppPurchaseStoreKitPlatform のインスタンス
late InAppPurchaseStoreKitPlatform _iapStoreKitPlatform;
late StreamSubscription> _subscription;
List _notFoundIds = [];
List _products = [];
List _purchases = [];
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
String? _queryProductError;
// ----------------------------------------------------------
// <コード説明>
// initState() はウィジェット生成時に最初に呼ばれるメソッドです。
// InAppPurchaseStoreKitPlatformを登録・取得してから、
// 購買ストリームの監視とストア情報の初期化を行います。
// ----------------------------------------------------------
@override
void initState() {
super.initState();
// InAppPurchaseStoreKitPlatformを使用可能にするための登録
InAppPurchaseStoreKitPlatform.registerPlatform();
// インスタンス取得
_iapStoreKitPlatform =
InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform;
// 購入状況更新ストリームを購読し、リスナーで購入の変化を監視
final Stream<List<PurchaseDetails>> purchaseUpdated =
_iapStoreKitPlatform.purchaseStream;
_subscription = purchaseUpdated.listen(
(List<PurchaseDetails> purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
},
onDone: () {
_subscription.cancel();
},
onError: (Object error) {
// 必要に応じてエラー処理を追加
},
);
// ストア情報を取得
initStoreInfo();
}
// ----------------------------------------------------------
// <コード説明>
// dispose() はウィジェットが破棄される時に呼び出されるメソッドです。
// リスナーのキャンセル等、後始末を行います。
// ----------------------------------------------------------
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
// ----------------------------------------------------------
// <コード説明>
// initStoreInfo() はストアが使用可能か確認した上で、
// 指定した商品IDリストを問い合わせ、ProductDetailsを取得します。
// ----------------------------------------------------------
Future initStoreInfo() async {
final bool isAvailable = await _iapStoreKitPlatform.isAvailable();
if (!isAvailable) {
setState(() {
_isAvailable = isAvailable;
_products = [];
_purchases = [];
_notFoundIds = [];
_purchasePending = false;
_loading = false;
});
return;
}
final ProductDetailsResponse productDetailResponse =
await _iapStoreKitPlatform.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
setState(() {
_queryProductError = productDetailResponse.error!.message;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = <PurchaseDetails>[];
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
return;
}
if (productDetailResponse.productDetails.isEmpty) {
setState(() {
_queryProductError = null;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = <PurchaseDetails>[];
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
return;
}
setState(() {
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_notFoundIds = productDetailResponse.notFoundIDs;
_purchasePending = false;
_loading = false;
});
}
// ----------------------------------------------------------
// <コード説明>
// build() はUI描画を行います。
// 取得した商品リストの表示や、購入状況に応じたローディング表示を行います。
// ----------------------------------------------------------
@override
Widget build(BuildContext context) {
final List stack = [];
if (_queryProductError == null) {
stack.add(
ListView(
children: <Widget>[
_buildConnectionCheckTile(),
_buildProductList(),
_buildRestoreButton(),
],
),
);
} else {
stack.add(Center(
child: Text(_queryProductError!),
));
}
if (_purchasePending) {
stack.add(
const Stack(
children: <Widget>[
Opacity(
opacity: 0.3,
child: ModalBarrier(dismissible: false, color: Colors.grey),
),
Center(
child: CircularProgressIndicator(),
),
],
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('IAP Example'),
),
body: Stack(
children: stack,
),
);
}
// ----------------------------------------------------------
// <コード説明>
// _buildConnectionCheckTile() は、ストア接続状況やエラーを表示します。
// ----------------------------------------------------------
Card _buildConnectionCheckTile() {
if (_loading) {
return const Card(child: ListTile(title: Text('Trying to connect...')));
}
final Widget storeHeader = ListTile(
leading: Icon(
_isAvailable ? Icons.check : Icons.block,
color: _isAvailable ? Colors.green : ThemeData.light().colorScheme.error,
),
title:
Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'),
);
final List children = [storeHeader];
if (!_isAvailable) {
children.addAll(<Widget>[
const Divider(),
ListTile(
title: Text(
'Not connected',
style: TextStyle(color: ThemeData.light().colorScheme.error),
),
subtitle: const Text('Unable to connect to the payments processor.'),
),
]);
}
return Card(child: Column(children: children));
}
// ----------------------------------------------------------
// <コード説明>
// _buildProductList() は取得済みの商品をリスト表示し、
// 未購入の場合は購入ボタンを表示します。
// ----------------------------------------------------------
Card _buildProductList() {
if (_loading) {
return const Card(
child: ListTile(
leading: CircularProgressIndicator(),
title: Text('Fetching products...'),
),
);
}
if (!_isAvailable) {
return const Card();
}
const ListTile productHeader = ListTile(title: Text('Products for Sale'));
final List productList = [];
if (_notFoundIds.isNotEmpty) {
productList.add(
ListTile(
title: Text(
'[${_notFoundIds.join(", ")}] not found',
style: TextStyle(color: ThemeData.light().colorScheme.error),
),
subtitle: const Text('Some products were not found.'),
),
);
}
// 購入済み情報をマップ化
final Map<String, PurchaseDetails> purchases =
Map<String, PurchaseDetails>.fromEntries(
_purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
_iapStoreKitPlatform.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
}),
);
// 商品ごとにListTileを生成
productList.addAll(
_products.map((ProductDetails productDetails) {
final PurchaseDetails? previousPurchase = purchases[productDetails.id];
return ListTile(
title: Text(productDetails.title),
subtitle: Text(productDetails.description),
trailing: previousPurchase != null
? const Icon(Icons.check, color: Colors.green)
: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.green[800],
foregroundColor: Colors.white,
),
onPressed: () {
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: productDetails,
);
_iapStoreKitPlatform.buyNonConsumable(
purchaseParam: purchaseParam,
);
},
child: Text(productDetails.price),
),
);
}),
);
return Card(
child: Column(
children: <Widget>[productHeader, const Divider()] + productList,
),
);
}
// ----------------------------------------------------------
// <コード説明>
// _buildRestoreButton() は、購入を復元するためのボタンを表示します。
// iOS審査では「Restore purchases」ボタンの実装が必須です。
// ----------------------------------------------------------
Widget _buildRestoreButton() {
if (_loading) {
return Container();
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
onPressed: () => _iapStoreKitPlatform.restorePurchases(),
child: const Text('Restore purchases'),
),
],
),
);
}
// ----------------------------------------------------------
// <コード説明>
// showPendingUI() は、購入処理中にプログレス表示を行います。
// ----------------------------------------------------------
void showPendingUI() {
setState(() {
_purchasePending = true;
});
}
// ----------------------------------------------------------
// <コード説明>
// deliverProduct() は、購入(または復元)が完了したときに、
// 実際にアイテムを付与する処理を行う想定のメソッドです。
// ----------------------------------------------------------
Future deliverProduct(PurchaseDetails purchaseDetails) async {
// 実際にはここでアイテム付与処理を行う
setState(() {
_purchases.add(purchaseDetails);
_purchasePending = false;
});
}
// ----------------------------------------------------------
// <コード説明>
// handleError() は購入中にエラーが発生した場合の処理です。
// ----------------------------------------------------------
void handleError(IAPError error) {
setState(() {
_purchasePending = false;
});
}
// ----------------------------------------------------------
// <コード説明>
// _verifyPurchase() は、サーバーなどでレシート検証を行う想定のメソッドです。
// 今回は常にtrueを返していますが、実際のアプリでは
// サーバー連携などで不正購入のチェックを行ってください。
// ----------------------------------------------------------
Future _verifyPurchase(PurchaseDetails purchaseDetails) async {
return true; // 仮実装
}
// ----------------------------------------------------------
// <コード説明>
// _handleInvalidPurchase() は購入検証に失敗した際に呼ばれるメソッドです。
// ----------------------------------------------------------
void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
// 不正な購入だった場合の処理を記述
}
// ----------------------------------------------------------
// <コード説明>
// _listenToPurchaseUpdated() は、ストリームで受け取った
// PurchaseDetailsリストを1件ずつ処理する役割を担います。
// ----------------------------------------------------------
void _listenToPurchaseUpdated(List purchaseDetailsList) {
for (final purchaseDetails in purchaseDetailsList) {
_handleReportedPurchaseState(purchaseDetails);
}
}
// ----------------------------------------------------------
// <コード説明>
// _handleReportedPurchaseState() は、個々のPurchaseDetailsのstatusに応じて
// UI表示変更やエラー処理、検証処理を実行します。
// ----------------------------------------------------------
Future _handleReportedPurchaseState(
PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
showPendingUI();
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
final bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
await deliverProduct(purchaseDetails);
} else {
_handleInvalidPurchase(purchaseDetails);
return;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await _iapStoreKitPlatform.completePurchase(purchaseDetails);
}
}
}
}
### .storekitファイルの設定手順
(1)
・Add Auto-Renewable Subscriptionを選択
↓
・Create New Group「サブスク」を命名
↓
・Reference Name「月間サブスクリプション利用」を設定 ※年間も同様に設定
・Product ID「com.monthly」を設定 ※年間も同様に設定(com.yearly)
(2)
・月間サブスクリプション利用のAuto-Renewable SubscriptionでPriceを200に設定。
・年間サブスクリプション利用のAuto-Renewable SubscriptionでPriceを2400に設定。
(3)
・Cibfugyratuib SettingsのDefault StorefrontにJapan(JPY)を設定
・Cibfugyratuib SettingsのDefault LocalizationにJapaneseを設定
### スキーム設定手順
・Project→Scheme→Edit Scheme→Run→Options
・Storekit ConfigurationでStoreKitConfigSample.storekitを設定。
### 試したこと
・Android Studioを使い、下記の順番で.xcworkspace を再生成。
% flutter clean
% rm -rf ios/Pods
% rm -rf ios/Podfile.lock
% flutter pub get
% cd ios
% pod install
・Xcodeアプリを再起動しXcodeでRun(変化なし)
・定期的にPC再起動(変化なし)
### 補足情報(FW/ツールのバージョンなど)
◯Flutter開発環境に関するバージョン
% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.4, on macOS 15.3 24D60 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.1)
[✓] VS Code (version 1.97.0)
[✓] Connected device (4 available)
[✓] Network resources
• No issues found!
% dart --version
Dart SDK version: 3.6.2 (stable) (Wed Jan 29 01:20:39 2025 -0800) on "macos_arm64"
◯パッケージ(pubspec.yaml)
dependencies:
flutter:
sdk: flutter
flutter_launcher_icons: ^0.10.0
animated_text_kit: ^4.2.2
image_picker: ^0.8.7+4
shared_preferences: ^2.3.5
url_launcher: ^6.1.10
cupertino_icons: ^1.0.8
google_mobile_ads: ^3.0.0
webview_flutter: ^4.9.0
flutter_stripe: ^11.4.0
pay: ^3.1.0
flutter_localizations:
sdk: flutter
intl: ^0.19.0
**in_app_purchase: ^3.2.1 **
in_app_purchase_storekit: ^0.3.21
purchases_flutter: ^8.4.5