@pxisuke

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

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

0 likes

1Answer

Comments

  1. @pxisuke

    Questioner

    回答ありがとうございます!情報を参考にしてみます。
    Xcodeにてstorekitファイルの設定「Default Localization」を"Japanese"から"English(U.S)"に変更したら、storekitファイルで設定した内容がアプリ内で表示できるようになりました。
    しかし、決済に関する記述は日本語に対応させたいため、完全な解決には至っておりません。。。

    スクリーンショット 2025-02-16 20.45.04.png

  2. @pxisuke

    Questioner

    段階的にダウングレードしてましたが、問題は解決しませんでした。
    また、XcodeのStore Kitではなく、実機にアプリをインストールしてApp Store ConnectのIAPを使うと、製品IDが見つからない状態になっています。

    スクリーンショット 2025-02-16 21.53.44.png

    ステータスは送信準備完了になっているため、実機でSandBoxアカウントにログインしていればアプリ側で製品情報を取得できるはずなのです。
    やはり、App Store ConnectのIAPでもXcodeのStore Kitと同じようにDefault Localizationが影響しているのでしょうか。

    pubspec.yamlには下記のパッケージを設定しています。
    in_app_purchase: ^3.1.13
    in_app_purchase_storekit: ^0.3.4

Your answer might help someone💌