From fe93900dd97286391c83100fe758e4d139374c95 Mon Sep 17 00:00:00 2001 From: Hari07 <22373191+Hari-07@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:37:45 +0530 Subject: [PATCH] Add show manage subscriptions path --- .../in_app_purchase_storekit/CHANGELOG.md | 5 +++ .../InAppPurchasePlugin+StoreKit2.swift | 45 +++++++++++++++++++ .../StoreKit2/StoreKit2Messages.g.swift | 19 ++++++++ .../example/lib/main.dart | 24 ++++++++++ .../InAppPurchaseStoreKit2PluginTests.swift | 15 +++++++ ...p_purchase_storekit_platform_addition.dart | 10 +++++ .../lib/src/sk2_pigeon.g.dart | 14 ++++++ .../sk2_appstore_wrapper.dart | 7 +++ .../pigeons/sk2_pigeon.dart | 3 ++ .../in_app_purchase_storekit/pubspec.yaml | 2 +- .../test/fakes/fake_storekit_platform.dart | 7 +++ ...app_purchase_storekit_2_platform_test.dart | 7 +++ 12 files changed, 157 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index fa3b245ec325..e9521e6235e9 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.4.12 + +* Adds `showManageSubscriptions` to `InAppPurchaseStoreKitPlatformAddition`, which presents the + App Store sheet for managing subscriptions. Requires StoreKit 2 and iOS 15+ + ## 0.4.11+1 * Fixes StoreKit 2 restore transactions not grouping purchases into a single event. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift index c5418b97e3d9..521494074a06 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift @@ -401,6 +401,51 @@ extension InAppPurchasePlugin: InAppPurchase2API { #endif } + /// Wrapper method around StoreKit2's showManageSubscriptions(in:) method + /// https://developer.apple.com/documentation/storekit/appstore/showmanagesubscriptions(in:) + /// Presents the App Store sheet that lets the user manage their subscriptions. + func showManageSubscriptions(completion: @escaping (Result) -> Void) { + #if os(iOS) + if #available(iOS 15.0, *) { + guard let windowScene = self.registrar?.viewController?.view.window?.windowScene else { + let error = PigeonError( + code: "storekit2_missing_key_window_scene", + message: "Failed to fetch key window scene", + details: "registrar.viewController.view.window.windowScene returned nil." + ) + completion(.failure(error)) + return + } + Task { @MainActor in + do { + try await AppStore.showManageSubscriptions(in: windowScene) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } else { + completion( + .failure( + PigeonError( + code: "storekit2_unsupported_platform_version", + message: "Managing subscriptions requires iOS 15+", + details: nil + ))) + } + #elseif os(macOS) + // StoreKit does not provide showManageSubscriptions on macOS. Subscriptions are + // managed through the App Store app instead. + completion( + .failure( + PigeonError( + code: "storekit2_unsupported_platform", + message: "Managing subscriptions is not supported on macOS", + details: nil + ))) + #endif + } + /// Wrapper method around StoreKit2's sync() method /// https://developer.apple.com/documentation/storekit/appstore/sync() /// When called, a system prompt will ask users to enter their authentication details diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift index faeae556d8fe..ba896fb60879 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift @@ -760,6 +760,7 @@ protocol InAppPurchase2API { func countryCode(completion: @escaping (Result) -> Void) func sync(completion: @escaping (Result) -> Void) func presentOfferCodeRedeemSheet(completion: @escaping (Result) -> Void) + func showManageSubscriptions(completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -1027,6 +1028,24 @@ class InAppPurchase2APISetup { } else { presentOfferCodeRedeemSheetChannel.setMessageHandler(nil) } + let showManageSubscriptionsChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.showManageSubscriptions\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + showManageSubscriptionsChannel.setMessageHandler { _, reply in + api.showManageSubscriptions { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + showManageSubscriptionsChannel.setMessageHandler(nil) + } } } /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart index e97111ceccf2..0d3663bb6a43 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart @@ -147,6 +147,7 @@ class _MyAppState extends State<_MyApp> { _buildProductList(), _buildConsumableBox(), _buildCodeRedemptionButton(), + _buildShowManageSubscriptionsButton(), _buildRestoreButton(), ], ), @@ -419,6 +420,29 @@ class _MyAppState extends State<_MyApp> { ); } + Widget _buildShowManageSubscriptionsButton() { + if (_loading) { + return Container(); + } + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + ), + onPressed: () => _iapStoreKitPlatformAddition.showManageSubscriptions(), + child: const Text('Manage subscriptions'), + ), + ], + ), + ); + } + Widget _buildRestoreButton() { if (_loading) { return Container(); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift index 3b1147abe640..1aaaee9ef8f9 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift @@ -636,4 +636,19 @@ final class InAppPurchase2PluginTests: XCTestCase { waitForExpectations(timeout: 1.0) } + func testShowManageSubscriptionsFailsGracefullyWhenNoWindow() { + let expectation = self.expectation( + description: "Should fail gracefully when without key window") + + plugin.registrar = nil + + plugin.showManageSubscriptions { result in + if case .failure = result { + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1.0) + } + } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart index 9d4169a4cb0e..cbb628cf50b1 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart @@ -29,6 +29,16 @@ class InAppPurchaseStoreKitPlatformAddition extends InAppPurchasePlatformAdditio return SKPaymentQueueWrapper().presentCodeRedemptionSheet(); } + /// Presents the App Store sheet that lets the user manage their + /// subscriptions. + /// + /// Available on devices running iOS 15 and iPadOS 15 and later. + /// StoreKit 2 only; StoreKit 1 has no equivalent API. Not supported on macOS, + /// where subscriptions are managed through the App Store app instead. + Future showManageSubscriptions() { + return AppStore().showManageSubscriptions(); + } + /// Retry loading purchase data after an initial failure. /// /// If no results, a `null` value is returned. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart index 8d1423449abb..59b100007d83 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart @@ -1023,6 +1023,20 @@ class InAppPurchase2API { _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } + + Future showManageSubscriptions() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.showManageSubscriptions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } } abstract class InAppPurchase2CallbackAPI { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_appstore_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_appstore_wrapper.dart index cdd8bd9baa38..dfa2e9e896d5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_appstore_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_appstore_wrapper.dart @@ -26,4 +26,11 @@ final class AppStore { Future presentOfferCodeRedeemSheet() { return hostApi2.presentOfferCodeRedeemSheet(); } + + /// Dart wrapper for StoreKit2's showManageSubscriptions(in:) + /// Presents the App Store sheet that lets users manage their subscriptions. + /// https://developer.apple.com/documentation/storekit/appstore/showmanagesubscriptions(in:) + Future showManageSubscriptions() { + return hostApi2.showManageSubscriptions(); + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart index 1d0442412b66..c82164db9d5c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart @@ -261,6 +261,9 @@ abstract class InAppPurchase2API { @async void presentOfferCodeRedeemSheet(); + + @async + void showManageSubscriptions(); } @FlutterApi() diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 1d76f7cc7a36..4c930039f592 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.4.11+1 +version: 0.4.12 environment: sdk: ^3.10.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index 7092552dabd0..f2e96ba479eb 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -327,6 +327,7 @@ class FakeStoreKit2Platform implements InAppPurchase2API { SK2ProductPurchaseOptionsMessage? lastPurchaseOptions; Map> eligibleWinBackOffers = >{}; Map eligibleIntroductoryOffers = {}; + int showManageSubscriptionsCallCount = 0; /// Simulates purchase result for testing non-success scenarios. /// Set to userCancelled, pending, or unverified to test those cases. @@ -350,6 +351,7 @@ class FakeStoreKit2Platform implements InAppPurchase2API { eligibleWinBackOffers = >{}; eligibleIntroductoryOffers = {}; simulatedPurchaseResult = SK2ProductPurchaseResultMessage.success; + showManageSubscriptionsCallCount = 0; transactionsList = [ SK2TransactionMessage( id: 123, @@ -557,6 +559,11 @@ class FakeStoreKit2Platform implements InAppPurchase2API { @override Future presentOfferCodeRedeemSheet() async {} + + @override + Future showManageSubscriptions() async { + showManageSubscriptionsCallCount++; + } } SK2TransactionMessage createPendingTransaction(String id, {int quantity = 1}) { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart index 386c46505201..37fc5e42f5a8 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart @@ -403,6 +403,13 @@ void main() { }); }); + group('manage subscriptions', () { + test('forwards the call to the platform', () async { + await InAppPurchaseStoreKitPlatformAddition().showManageSubscriptions(); + expect(fakeStoreKit2Platform.showManageSubscriptionsCallCount, 1); + }); + }); + group('win back offers eligibility', () { late FakeStoreKit2Platform fakeStoreKit2Platform;