From 097ed3c0ae9fda40275d0ea8165bfb6388b90bd8 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:42:24 +0530 Subject: [PATCH 01/11] fix: add ios update check,remove boilerplate platform interface,rewrite test sto use methodchanel Closes #16,#17 --- example/pubspec.lock | 143 ++++++-- lib/in_app_update_flutter.dart | 83 +++-- lib/src/ios_update_check.dart | 57 ++++ .../in_app_update_flutter_method_channel.dart | 52 ++- lib/src/models/app_update_info.dart | 63 ++++ lib/src/models/app_update_info_ios.dart | 26 ++ lib/src/models/models.dart | 2 + ...app_update_flutter_platform_interface.dart | 105 ------ pubspec.yaml | 5 +- ...pp_update_flutter_method_channel_test.dart | 222 ------------ test/in_app_update_flutter_test.dart | 318 ++++++++++-------- 11 files changed, 522 insertions(+), 554 deletions(-) create mode 100644 lib/src/ios_update_check.dart create mode 100644 lib/src/models/app_update_info.dart create mode 100644 lib/src/models/app_update_info_ios.dart delete mode 100644 lib/src/platform_interface/in_app_update_flutter_platform_interface.dart delete mode 100644 test/in_app_update_flutter_method_channel_test.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index 423f0dd..ec9c1e5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" fake_async: dependency: transitive description: @@ -57,14 +57,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -88,18 +104,39 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" in_app_update_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.0.0" + version: "2.0.3" integration_test: dependency: "direct dev" description: flutter @@ -157,10 +194,26 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + url: "https://pub.dev" + source: hosted + version: "10.1.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 + url: "https://pub.dev" + source: hosted + version: "4.1.0" path: dependency: transitive description: @@ -173,10 +226,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -189,10 +242,18 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -202,10 +263,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" stack_trace: dependency: transitive description: @@ -226,10 +287,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -242,18 +303,26 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -266,18 +335,34 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "6.3.0" sdks: - dart: ">=3.9.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.1" diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index 9ffdb14..6328db9 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -1,17 +1,30 @@ +import 'package:in_app_update_flutter/src/method_channel/in_app_update_flutter_method_channel.dart'; import 'package:in_app_update_flutter/src/models/models.dart'; -import 'package:in_app_update_flutter/src/platform_interface/in_app_update_flutter_platform_interface.dart'; export 'package:in_app_update_flutter/src/models/models.dart'; /// A Flutter plugin for in-app updates. /// -/// On iOS, use [showUpdateForIos] to present the App Store product page -/// using StoreKit. -/// -/// On Android, use [checkUpdateAndroid] to check for updates via Google Play's -/// In-App Updates API, then [startImmediateUpdateAndroid] or -/// [startFlexibleUpdateAndroid] to start the update flow. +/// Use [checkUpdate] and [startUpdate] for a cross-platform experience, +/// or call platform-specific methods directly for more control. class InAppUpdateFlutter { + final _impl = MethodChannelInAppUpdateFlutter(); + + /// Cross-platform: Checks whether an update is available. + /// + /// On iOS, queries the iTunes Lookup API. On Android, uses Play Core. + /// Returns a unified [AppUpdateInfo] with platform-specific details. + Future checkUpdate({String? iosAppStoreRegion}) => + _impl.checkUpdate(iosAppStoreRegion: iosAppStoreRegion); + + /// Cross-platform: Starts the update flow. + /// + /// On iOS, presents the App Store product page via StoreKit + /// ([appStoreId] is required). + /// On Android, starts the immediate (blocking) update flow. + Future startUpdate({String? appStoreId}) => + _impl.startUpdate(appStoreId: appStoreId); + /// Shows the platform-specific in-app update UI. /// /// [appStoreId] is the numeric App Store ID of your app @@ -20,28 +33,33 @@ class InAppUpdateFlutter { 'Use showUpdateForIos() on iOS or checkUpdateAndroid() + ' 'startImmediateUpdateAndroid()/startFlexibleUpdateAndroid() on Android', ) - Future showUpdate({required String appStoreId}) { - // ignore: deprecated_member_use_from_same_package - return InAppUpdateFlutterPlatform.instance - .showUpdate(appStoreId: appStoreId); - } + Future showUpdate({required String appStoreId}) => + // ignore: deprecated_member_use_from_same_package + _impl.showUpdate(appStoreId: appStoreId); /// iOS: Shows the App Store product page overlay via StoreKit. /// /// [appStoreId] is the numeric App Store ID of your app /// (found in your App Store Connect URL). - Future showUpdateForIos({required String appStoreId}) { - return InAppUpdateFlutterPlatform.instance - .showUpdateForIos(appStoreId: appStoreId); - } + Future showUpdateForIos({required String appStoreId}) => + _impl.showUpdateForIos(appStoreId: appStoreId); + + /// iOS: Checks whether an update is available via the iTunes Lookup API. + /// + /// Queries the App Store for the latest published version and compares it + /// against the currently installed version using semantic versioning. + /// + /// An optional [iosAppStoreRegion] can be provided to check a specific + /// App Store region (e.g., `"us"`, `"gb"`). + Future checkUpdateIos({String? iosAppStoreRegion}) => + _impl.checkUpdateIos(iosAppStoreRegion: iosAppStoreRegion); /// Android: Checks whether an in-app update is available via Play Core. /// /// Returns an [AppUpdateInfoAndroid] containing update metadata such as /// availability, version code, priority, staleness, and allowed update types. - Future checkUpdateAndroid() { - return InAppUpdateFlutterPlatform.instance.checkUpdateAndroid(); - } + Future checkUpdateAndroid() => + _impl.checkUpdateAndroid(); /// Android: Starts the immediate (full-screen, blocking) update flow. /// @@ -52,11 +70,10 @@ class InAppUpdateFlutter { /// to free up storage for the update. Future startImmediateUpdateAndroid({ bool allowAssetPackDeletion = false, - }) { - return InAppUpdateFlutterPlatform.instance.startImmediateUpdateAndroid( - allowAssetPackDeletion: allowAssetPackDeletion, - ); - } + }) => + _impl.startImmediateUpdateAndroid( + allowAssetPackDeletion: allowAssetPackDeletion, + ); /// Android: Starts the flexible (background download) update flow. /// @@ -68,24 +85,20 @@ class InAppUpdateFlutter { /// to free up storage for the update. Future startFlexibleUpdateAndroid({ bool allowAssetPackDeletion = false, - }) { - return InAppUpdateFlutterPlatform.instance.startFlexibleUpdateAndroid( - allowAssetPackDeletion: allowAssetPackDeletion, - ); - } + }) => + _impl.startFlexibleUpdateAndroid( + allowAssetPackDeletion: allowAssetPackDeletion, + ); /// Android: Completes a flexible update by triggering an app restart. /// /// Call this after [installStateStreamAndroid] reports /// [InstallStatusAndroid.downloaded]. - Future completeUpdateAndroid() { - return InAppUpdateFlutterPlatform.instance.completeUpdateAndroid(); - } + Future completeUpdateAndroid() => _impl.completeUpdateAndroid(); /// Android: A stream of install state changes during a flexible update. /// /// Emits [InstallStateAndroid] events with download progress and status. - Stream get installStateStreamAndroid { - return InAppUpdateFlutterPlatform.instance.installStateStreamAndroid; - } + Stream get installStateStreamAndroid => + _impl.installStateStreamAndroid; } diff --git a/lib/src/ios_update_check.dart b/lib/src/ios_update_check.dart new file mode 100644 index 0000000..8db013f --- /dev/null +++ b/lib/src/ios_update_check.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import 'package:in_app_update_flutter/src/models/models.dart'; + +/// Checks for iOS App Store updates using the iTunes Lookup API. +/// Pure Dart — no native code or method channels required. +Future checkUpdateIosImpl({String? iosAppStoreRegion}) async { + final packageInfo = await PackageInfo.fromPlatform(); + final bundleId = packageInfo.packageName; + final installedVersion = packageInfo.version; + + final timestamp = DateTime.now().millisecondsSinceEpoch; + final region = iosAppStoreRegion != null ? '$iosAppStoreRegion/' : ''; + final uri = Uri.parse( + 'https://itunes.apple.com/${region}lookup?bundleId=$bundleId&_=$timestamp', + ); + + final client = HttpClient(); + try { + final request = await client.getUrl(uri); + final response = await request.close(); + final responseBody = await response.transform(utf8.decoder).join(); + + final storeVersion = + RegExp('"version":\\s*"([^"]*)"').firstMatch(responseBody)?.group(1); + + if (storeVersion == null) { + return AppUpdateInfoIos( + storeVersion: '', + installedVersion: installedVersion, + updateAvailable: false, + bundleId: bundleId, + ); + } + + bool updateAvailable = false; + try { + updateAvailable = + Version.parse(storeVersion) > Version.parse(installedVersion); + } on FormatException { + // Invalid semver, updateAvailable stays false + } + + return AppUpdateInfoIos( + storeVersion: storeVersion, + installedVersion: installedVersion, + updateAvailable: updateAvailable, + bundleId: bundleId, + ); + } finally { + client.close(); + } +} diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index e150bf4..c7141ae 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -1,38 +1,60 @@ +import 'dart:io'; + import 'package:flutter/services.dart'; +import 'package:in_app_update_flutter/src/ios_update_check.dart'; import 'package:in_app_update_flutter/src/models/models.dart'; -import 'package:in_app_update_flutter/src/platform_interface/in_app_update_flutter_platform_interface.dart'; -/// An implementation of [InAppUpdateFlutterPlatform] that uses method channels. -class MethodChannelInAppUpdateFlutter extends InAppUpdateFlutterPlatform { - /// The method channel used to interact with the native platform. +/// Handles platform communication via method channels for Android +/// and delegates to pure Dart for iOS update checks. +class MethodChannelInAppUpdateFlutter { static const MethodChannel _methodChannel = MethodChannel( 'in_app_update_flutter', ); - /// The event channel for receiving install state updates during flexible updates. static const EventChannel _eventChannel = EventChannel( 'in_app_update_flutter/installStateAndroid', ); - @override + Future checkUpdate({String? iosAppStoreRegion}) async { + if (Platform.isIOS) { + final info = await checkUpdateIosImpl(iosAppStoreRegion: iosAppStoreRegion); + return AppUpdateInfo.fromIos(info); + } else if (Platform.isAndroid) { + final info = await checkUpdateAndroid(); + return AppUpdateInfo.fromAndroid(info); + } + throw UnsupportedError('Unsupported platform'); + } + + Future startUpdate({String? appStoreId}) async { + if (Platform.isIOS) { + if (appStoreId == null) { + throw ArgumentError('appStoreId is required on iOS'); + } + await showUpdateForIos(appStoreId: appStoreId); + } else if (Platform.isAndroid) { + await startImmediateUpdateAndroid(); + } else { + throw UnsupportedError('Unsupported platform'); + } + } + @Deprecated( 'Use showUpdateForIos() on iOS or checkUpdateAndroid() + ' 'startImmediateUpdateAndroid()/startFlexibleUpdateAndroid() on Android', ) - Future showUpdate({required String appStoreId}) async { - await _methodChannel.invokeMethod('showStoreUpdateIos', { - 'appStoreId': appStoreId, - }); - } + Future showUpdate({required String appStoreId}) => + showUpdateForIos(appStoreId: appStoreId); - @override Future showUpdateForIos({required String appStoreId}) async { await _methodChannel.invokeMethod('showStoreUpdateIos', { 'appStoreId': appStoreId, }); } - @override + Future checkUpdateIos({String? iosAppStoreRegion}) => + checkUpdateIosImpl(iosAppStoreRegion: iosAppStoreRegion); + Future checkUpdateAndroid() async { final result = await _methodChannel.invokeMapMethod( 'checkForUpdateAndroid', @@ -40,7 +62,6 @@ class MethodChannelInAppUpdateFlutter extends InAppUpdateFlutterPlatform { return AppUpdateInfoAndroid.fromMap(result!); } - @override Future startImmediateUpdateAndroid({ bool allowAssetPackDeletion = false, }) async { @@ -51,7 +72,6 @@ class MethodChannelInAppUpdateFlutter extends InAppUpdateFlutterPlatform { return UpdateResultAndroid.fromValue(result!); } - @override Future startFlexibleUpdateAndroid({ bool allowAssetPackDeletion = false, }) async { @@ -62,12 +82,10 @@ class MethodChannelInAppUpdateFlutter extends InAppUpdateFlutterPlatform { return UpdateResultAndroid.fromValue(result!); } - @override Future completeUpdateAndroid() async { await _methodChannel.invokeMethod('completeUpdateAndroid'); } - @override Stream get installStateStreamAndroid { return _eventChannel.receiveBroadcastStream().map((event) { return InstallStateAndroid.fromMap( diff --git a/lib/src/models/app_update_info.dart b/lib/src/models/app_update_info.dart new file mode 100644 index 0000000..4d72008 --- /dev/null +++ b/lib/src/models/app_update_info.dart @@ -0,0 +1,63 @@ +import 'package:in_app_update_flutter/src/models/models.dart'; + +/// The detected platform for the current device. +enum AppPlatform { android, ios } + +/// A unified cross-platform result from [checkUpdate]. +/// +/// Contains common update info regardless of platform, plus +/// platform-specific details when available. +class AppUpdateInfo { + /// The detected platform. + final AppPlatform platform; + + /// Whether an update is available. + final bool updateAvailable; + + /// The currently installed version string. + final String? installedVersion; + + /// The latest version available on the store. + final String? storeVersion; + + /// Android-specific update info. `null` on iOS. + final AppUpdateInfoAndroid? androidInfo; + + /// iOS-specific update info. `null` on Android. + final AppUpdateInfoIos? iosInfo; + + const AppUpdateInfo({ + required this.platform, + required this.updateAvailable, + this.installedVersion, + this.storeVersion, + this.androidInfo, + this.iosInfo, + }); + + /// Creates an [AppUpdateInfo] from an Android result. + factory AppUpdateInfo.fromAndroid(AppUpdateInfoAndroid info) { + return AppUpdateInfo( + platform: AppPlatform.android, + updateAvailable: + info.updateAvailability == UpdateAvailabilityAndroid.updateAvailable, + androidInfo: info, + ); + } + + /// Creates an [AppUpdateInfo] from an iOS result. + factory AppUpdateInfo.fromIos(AppUpdateInfoIos info) { + return AppUpdateInfo( + platform: AppPlatform.ios, + updateAvailable: info.updateAvailable, + installedVersion: info.installedVersion, + storeVersion: info.storeVersion, + iosInfo: info, + ); + } + + @override + String toString() => + 'AppUpdateInfo(platform: $platform, updateAvailable: $updateAvailable, ' + 'installed: $installedVersion, store: $storeVersion)'; +} diff --git a/lib/src/models/app_update_info_ios.dart b/lib/src/models/app_update_info_ios.dart new file mode 100644 index 0000000..1021982 --- /dev/null +++ b/lib/src/models/app_update_info_ios.dart @@ -0,0 +1,26 @@ +/// Information about an iOS app update retrieved from the App Store. +class AppUpdateInfoIos { + /// The latest version available on the App Store. + final String storeVersion; + + /// The currently installed version of the app. + final String installedVersion; + + /// Whether an update is available (store version > installed version). + final bool updateAvailable; + + /// The bundle ID used to look up the App Store listing. + final String bundleId; + + const AppUpdateInfoIos({ + required this.storeVersion, + required this.installedVersion, + required this.updateAvailable, + required this.bundleId, + }); + + @override + String toString() => + 'AppUpdateInfoIos(bundleId: $bundleId, installed: $installedVersion, ' + 'store: $storeVersion, updateAvailable: $updateAvailable)'; +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 47b8d0c..4b2acc8 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,4 +1,6 @@ +export 'app_update_info.dart'; export 'app_update_info_android.dart'; +export 'app_update_info_ios.dart'; export 'install_state_android.dart'; export 'install_status_android.dart'; export 'update_availability_android.dart'; diff --git a/lib/src/platform_interface/in_app_update_flutter_platform_interface.dart b/lib/src/platform_interface/in_app_update_flutter_platform_interface.dart deleted file mode 100644 index f2613d3..0000000 --- a/lib/src/platform_interface/in_app_update_flutter_platform_interface.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:in_app_update_flutter/src/method_channel/in_app_update_flutter_method_channel.dart'; -import 'package:in_app_update_flutter/src/models/models.dart'; - -/// The platform interface for the `in_app_update_flutter` plugin. -/// -/// This class defines the API that platform-specific implementations -/// must implement. It uses the [PlatformInterface] pattern to ensure -/// safe extension and prevent accidental breaking changes. -abstract class InAppUpdateFlutterPlatform extends PlatformInterface { - /// Constructs an InAppUpdateFlutterPlatform. - InAppUpdateFlutterPlatform() : super(token: _token); - - static final Object _token = Object(); - - static InAppUpdateFlutterPlatform _instance = - MethodChannelInAppUpdateFlutter(); - - /// The default instance of [InAppUpdateFlutterPlatform] to use. - /// - /// Defaults to [MethodChannelInAppUpdateFlutter]. - static InAppUpdateFlutterPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [InAppUpdateFlutterPlatform] when - /// they register themselves. - static set instance(InAppUpdateFlutterPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Shows the platform-specific in-app update UI. - /// - /// On iOS, this presents the App Store product page using StoreKit. - /// [appStoreId] is the numeric App Store ID of your app. - @Deprecated( - 'Use showUpdateForIos() on iOS or checkUpdateAndroid() + ' - 'startImmediateUpdateAndroid()/startFlexibleUpdateAndroid() on Android', - ) - Future showUpdate({required String appStoreId}) { - throw UnimplementedError('showUpdate() has not been implemented.'); - } - - /// iOS: Shows the App Store product page overlay via StoreKit. - /// - /// [appStoreId] is the numeric App Store ID of your app - /// (found in your App Store Connect URL). - Future showUpdateForIos({required String appStoreId}) { - throw UnimplementedError('showUpdateForIos() has not been implemented.'); - } - - /// Android: Checks whether an in-app update is available via Play Core. - /// - /// Returns an [AppUpdateInfoAndroid] containing update metadata such as - /// availability, version code, priority, staleness, and allowed update types. - Future checkUpdateAndroid() { - throw UnimplementedError('checkUpdateAndroid() has not been implemented.'); - } - - /// Android: Starts the immediate (full-screen, blocking) update flow. - /// - /// If [allowAssetPackDeletion] is `true`, the system may delete asset packs - /// to free up storage for the update. - Future startImmediateUpdateAndroid({ - bool allowAssetPackDeletion = false, - }) { - throw UnimplementedError( - 'startImmediateUpdateAndroid() has not been implemented.', - ); - } - - /// Android: Starts the flexible (background download) update flow. - /// - /// If [allowAssetPackDeletion] is `true`, the system may delete asset packs - /// to free up storage for the update. - /// - /// Listen to [installStateStreamAndroid] for download progress. - /// Call [completeUpdateAndroid] when the download is complete. - Future startFlexibleUpdateAndroid({ - bool allowAssetPackDeletion = false, - }) { - throw UnimplementedError( - 'startFlexibleUpdateAndroid() has not been implemented.', - ); - } - - /// Android: Completes a flexible update by triggering an app restart. - /// - /// Call this after [installStateStreamAndroid] reports - /// [InstallStatusAndroid.downloaded]. - Future completeUpdateAndroid() { - throw UnimplementedError( - 'completeUpdateAndroid() has not been implemented.', - ); - } - - /// Android: A stream of install state changes during a flexible update. - /// - /// Emits [InstallStateAndroid] events with download progress and status. - Stream get installStateStreamAndroid { - throw UnimplementedError( - 'installStateStreamAndroid has not been implemented.', - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 920db29..28e7989 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,12 +18,13 @@ environment: dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.2 + package_info_plus: ^10.1.0 + pub_semver: ^2.1.4 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: plugin: diff --git a/test/in_app_update_flutter_method_channel_test.dart b/test/in_app_update_flutter_method_channel_test.dart deleted file mode 100644 index 86c7825..0000000 --- a/test/in_app_update_flutter_method_channel_test.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_update_flutter/in_app_update_flutter.dart'; -import 'package:in_app_update_flutter/src/method_channel/in_app_update_flutter_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const methodChannel = MethodChannel('in_app_update_flutter'); - - late MethodChannelInAppUpdateFlutter plugin; - - setUp(() { - plugin = MethodChannelInAppUpdateFlutter(); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); - }); - - group('MethodChannelInAppUpdateFlutter', () { - group('showUpdateForIos', () { - test('calls showStoreUpdateIos method on the channel', () async { - String? invokedMethod; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - invokedMethod = call.method; - return null; - }); - - await plugin.showUpdateForIos(appStoreId: '544007664'); - expect(invokedMethod, 'showStoreUpdateIos'); - }); - - test('passes appStoreId argument to the channel', () async { - Map? invokedArgs; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - invokedArgs = call.arguments as Map; - return null; - }); - - await plugin.showUpdateForIos(appStoreId: '544007664'); - expect(invokedArgs, {'appStoreId': '544007664'}); - }); - - test('propagates platform errors', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - throw PlatformException( - code: 'STORE_ERROR', - message: 'Failed to load product', - ); - }); - - expect( - () => plugin.showUpdateForIos(appStoreId: '544007664'), - throwsA(isA()), - ); - }); - }); - - group('showUpdate (deprecated)', () { - test('calls showStoreUpdateIos method on the channel', () async { - String? invokedMethod; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - invokedMethod = call.method; - return null; - }); - - // ignore: deprecated_member_use_from_same_package - await plugin.showUpdate(appStoreId: '544007664'); - expect(invokedMethod, 'showStoreUpdateIos'); - }); - }); - - group('checkUpdateAndroid', () { - test('calls checkForUpdateAndroid and deserializes response', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - expect(call.method, 'checkForUpdateAndroid'); - return { - 'updateAvailability': 2, - 'availableVersionCode': 42, - 'updatePriority': 3, - 'clientVersionStalenessDays': 7, - 'isImmediateUpdateAllowed': true, - 'isFlexibleUpdateAllowed': true, - 'installStatus': 0, - }; - }); - - final info = await plugin.checkUpdateAndroid(); - expect( - info.updateAvailability, - UpdateAvailabilityAndroid.updateAvailable, - ); - expect(info.availableVersionCode, 42); - expect(info.updatePriority, 3); - expect(info.clientVersionStalenessDays, 7); - expect(info.isImmediateUpdateAllowed, true); - expect(info.isFlexibleUpdateAllowed, true); - expect(info.installStatus, InstallStatusAndroid.unknown); - }); - - test('handles null optional fields', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - return { - 'updateAvailability': 1, - 'availableVersionCode': null, - 'updatePriority': 0, - 'clientVersionStalenessDays': null, - 'isImmediateUpdateAllowed': false, - 'isFlexibleUpdateAllowed': false, - 'installStatus': 0, - }; - }); - - final info = await plugin.checkUpdateAndroid(); - expect( - info.updateAvailability, - UpdateAvailabilityAndroid.updateNotAvailable, - ); - expect(info.availableVersionCode, isNull); - expect(info.clientVersionStalenessDays, isNull); - }); - - test('propagates platform errors', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - throw PlatformException( - code: 'CHECK_UPDATE_FAILED', - message: 'Failed to check for updates', - ); - }); - - expect( - () => plugin.checkUpdateAndroid(), - throwsA(isA()), - ); - }); - }); - - group('startImmediateUpdateAndroid', () { - test('calls startImmediateUpdateAndroid with default args', () async { - Map? invokedArgs; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - expect(call.method, 'startImmediateUpdateAndroid'); - invokedArgs = call.arguments as Map; - return 0; - }); - - final result = await plugin.startImmediateUpdateAndroid(); - expect(result, UpdateResultAndroid.success); - expect(invokedArgs?['allowAssetPackDeletion'], false); - }); - - test('passes allowAssetPackDeletion argument', () async { - Map? invokedArgs; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - invokedArgs = call.arguments as Map; - return 0; - }); - - await plugin.startImmediateUpdateAndroid(allowAssetPackDeletion: true); - expect(invokedArgs?['allowAssetPackDeletion'], true); - }); - - test('returns userCanceled when result is 1', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - return 1; - }); - - final result = await plugin.startImmediateUpdateAndroid(); - expect(result, UpdateResultAndroid.userCanceled); - }); - - test('returns inAppUpdateFailed when result is 2', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - return 2; - }); - - final result = await plugin.startImmediateUpdateAndroid(); - expect(result, UpdateResultAndroid.inAppUpdateFailed); - }); - }); - - group('startFlexibleUpdateAndroid', () { - test('calls startFlexibleUpdateAndroid and returns success', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - expect(call.method, 'startFlexibleUpdateAndroid'); - return 0; - }); - - final result = await plugin.startFlexibleUpdateAndroid(); - expect(result, UpdateResultAndroid.success); - }); - }); - - group('completeUpdateAndroid', () { - test('calls completeUpdateAndroid on the channel', () async { - String? invokedMethod; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (call) async { - invokedMethod = call.method; - return null; - }); - - await plugin.completeUpdateAndroid(); - expect(invokedMethod, 'completeUpdateAndroid'); - }); - }); - }); -} diff --git a/test/in_app_update_flutter_test.dart b/test/in_app_update_flutter_test.dart index 09f61da..3deda70 100644 --- a/test/in_app_update_flutter_test.dart +++ b/test/in_app_update_flutter_test.dart @@ -1,191 +1,221 @@ -import 'package:in_app_update_flutter/in_app_update_flutter.dart'; -import 'package:in_app_update_flutter/src/method_channel/in_app_update_flutter_method_channel.dart'; -import 'package:in_app_update_flutter/src/platform_interface/in_app_update_flutter_platform_interface.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -/// A valid mock that extends [InAppUpdateFlutterPlatform], giving it -/// the correct token so it can be set as the platform instance. -class _MockPlatform extends InAppUpdateFlutterPlatform { - String? lastAppStoreId; - - @override - Future showUpdateForIos({required String appStoreId}) async { - lastAppStoreId = appStoreId; - } - - @override - // ignore: deprecated_member_use_from_same_package - Future showUpdate({required String appStoreId}) async { - lastAppStoreId = appStoreId; - } - - @override - Future checkUpdateAndroid() async { - return const AppUpdateInfoAndroid( - updateAvailability: UpdateAvailabilityAndroid.updateAvailable, - availableVersionCode: 42, - updatePriority: 3, - clientVersionStalenessDays: 7, - isImmediateUpdateAllowed: true, - isFlexibleUpdateAllowed: true, - installStatus: InstallStatusAndroid.unknown, - ); - } - - @override - Future startImmediateUpdateAndroid({ - bool allowAssetPackDeletion = false, - }) async { - return UpdateResultAndroid.success; - } - - @override - Future startFlexibleUpdateAndroid({ - bool allowAssetPackDeletion = false, - }) async { - return UpdateResultAndroid.success; - } - - @override - Future completeUpdateAndroid() async {} - - @override - Stream get installStateStreamAndroid => - const Stream.empty(); -} - -/// Minimal subclass that does NOT override the abstract methods, -/// so calls fall through to the base-class throw. -class _UnimplementedPlatform extends InAppUpdateFlutterPlatform {} +import 'package:in_app_update_flutter/in_app_update_flutter.dart'; void main() { - group('InAppUpdateFlutterPlatform', () { - tearDown(() { - // Restore default instance after each test. - InAppUpdateFlutterPlatform.instance = MethodChannelInAppUpdateFlutter(); - }); + TestWidgetsFlutterBinding.ensureInitialized(); - test('default instance is MethodChannelInAppUpdateFlutter', () { - expect( - InAppUpdateFlutterPlatform.instance, - isA(), - ); - }); + const methodChannel = MethodChannel('in_app_update_flutter'); - test('instance can be replaced with a valid mock', () { - final mock = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = mock; - expect(InAppUpdateFlutterPlatform.instance, same(mock)); - }); + late InAppUpdateFlutter plugin; - test('setting instance back to MethodChannel works', () { - InAppUpdateFlutterPlatform.instance = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = MethodChannelInAppUpdateFlutter(); - expect( - InAppUpdateFlutterPlatform.instance, - isA(), - ); - }); + setUp(() { + plugin = InAppUpdateFlutter(); + }); - group('base class throws UnimplementedError', () { - late _UnimplementedPlatform platform; + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, null); + }); - setUp(() { - platform = _UnimplementedPlatform(); + group('InAppUpdateFlutter', () { + group('showUpdateForIos', () { + test('calls showStoreUpdateIos method on the channel', () async { + String? invokedMethod; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + invokedMethod = call.method; + return null; + }); + + await plugin.showUpdateForIos(appStoreId: '544007664'); + expect(invokedMethod, 'showStoreUpdateIos'); }); - test('showUpdate throws UnimplementedError', () { - expect( - // ignore: deprecated_member_use_from_same_package - () => platform.showUpdate(appStoreId: '123'), - throwsA(isA()), - ); - }); + test('passes appStoreId argument to the channel', () async { + Map? invokedArgs; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + invokedArgs = call.arguments as Map; + return null; + }); - test('showUpdateForIos throws UnimplementedError', () { - expect( - () => platform.showUpdateForIos(appStoreId: '123'), - throwsA(isA()), - ); + await plugin.showUpdateForIos(appStoreId: '544007664'); + expect(invokedArgs, {'appStoreId': '544007664'}); }); - test('checkUpdateAndroid throws UnimplementedError', () { + test('propagates platform errors', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + throw PlatformException( + code: 'STORE_ERROR', + message: 'Failed to load product', + ); + }); + expect( - () => platform.checkUpdateAndroid(), - throwsA(isA()), + () => plugin.showUpdateForIos(appStoreId: '544007664'), + throwsA(isA()), ); }); + }); - test('startImmediateUpdateAndroid throws UnimplementedError', () { - expect( - () => platform.startImmediateUpdateAndroid(), - throwsA(isA()), - ); + group('showUpdate (deprecated)', () { + test('calls showStoreUpdateIos method on the channel', () async { + String? invokedMethod; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + invokedMethod = call.method; + return null; + }); + + // ignore: deprecated_member_use_from_same_package + await plugin.showUpdate(appStoreId: '544007664'); + expect(invokedMethod, 'showStoreUpdateIos'); }); + }); - test('startFlexibleUpdateAndroid throws UnimplementedError', () { + group('checkUpdateAndroid', () { + test('calls checkForUpdateAndroid and deserializes response', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, 'checkForUpdateAndroid'); + return { + 'updateAvailability': 2, + 'availableVersionCode': 42, + 'updatePriority': 3, + 'clientVersionStalenessDays': 7, + 'isImmediateUpdateAllowed': true, + 'isFlexibleUpdateAllowed': true, + 'installStatus': 0, + }; + }); + + final info = await plugin.checkUpdateAndroid(); expect( - () => platform.startFlexibleUpdateAndroid(), - throwsA(isA()), + info.updateAvailability, + UpdateAvailabilityAndroid.updateAvailable, ); + expect(info.availableVersionCode, 42); + expect(info.updatePriority, 3); + expect(info.clientVersionStalenessDays, 7); + expect(info.isImmediateUpdateAllowed, true); + expect(info.isFlexibleUpdateAllowed, true); + expect(info.installStatus, InstallStatusAndroid.unknown); }); - test('completeUpdateAndroid throws UnimplementedError', () { + test('handles null optional fields', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + return { + 'updateAvailability': 1, + 'availableVersionCode': null, + 'updatePriority': 0, + 'clientVersionStalenessDays': null, + 'isImmediateUpdateAllowed': false, + 'isFlexibleUpdateAllowed': false, + 'installStatus': 0, + }; + }); + + final info = await plugin.checkUpdateAndroid(); expect( - () => platform.completeUpdateAndroid(), - throwsA(isA()), + info.updateAvailability, + UpdateAvailabilityAndroid.updateNotAvailable, ); + expect(info.availableVersionCode, isNull); + expect(info.clientVersionStalenessDays, isNull); }); - test('installStateStreamAndroid throws UnimplementedError', () { + test('propagates platform errors', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + throw PlatformException( + code: 'CHECK_UPDATE_FAILED', + message: 'Failed to check for updates', + ); + }); + expect( - () => platform.installStateStreamAndroid, - throwsA(isA()), + () => plugin.checkUpdateAndroid(), + throwsA(isA()), ); }); }); - group('mock platform delegates', () { - test('showUpdateForIos passes appStoreId to mock', () async { - final mock = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = mock; + group('startImmediateUpdateAndroid', () { + test('calls startImmediateUpdateAndroid with default args', () async { + Map? invokedArgs; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, 'startImmediateUpdateAndroid'); + invokedArgs = call.arguments as Map; + return 0; + }); + + final result = await plugin.startImmediateUpdateAndroid(); + expect(result, UpdateResultAndroid.success); + expect(invokedArgs?['allowAssetPackDeletion'], false); + }); - await InAppUpdateFlutterPlatform.instance - .showUpdateForIos(appStoreId: '544007664'); - expect(mock.lastAppStoreId, '544007664'); + test('passes allowAssetPackDeletion argument', () async { + Map? invokedArgs; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + invokedArgs = call.arguments as Map; + return 0; + }); + + await plugin.startImmediateUpdateAndroid(allowAssetPackDeletion: true); + expect(invokedArgs?['allowAssetPackDeletion'], true); }); - test('checkUpdateAndroid returns expected info', () async { - final mock = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = mock; + test('returns userCanceled when result is 1', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + return 1; + }); - final info = - await InAppUpdateFlutterPlatform.instance.checkUpdateAndroid(); - expect( - info.updateAvailability, - UpdateAvailabilityAndroid.updateAvailable, - ); - expect(info.availableVersionCode, 42); - expect(info.updatePriority, 3); + final result = await plugin.startImmediateUpdateAndroid(); + expect(result, UpdateResultAndroid.userCanceled); }); - test('startImmediateUpdateAndroid returns success', () async { - final mock = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = mock; + test('returns inAppUpdateFailed when result is 2', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + return 2; + }); - final result = await InAppUpdateFlutterPlatform.instance - .startImmediateUpdateAndroid(); - expect(result, UpdateResultAndroid.success); + final result = await plugin.startImmediateUpdateAndroid(); + expect(result, UpdateResultAndroid.inAppUpdateFailed); }); + }); - test('startFlexibleUpdateAndroid returns success', () async { - final mock = _MockPlatform(); - InAppUpdateFlutterPlatform.instance = mock; + group('startFlexibleUpdateAndroid', () { + test('calls startFlexibleUpdateAndroid and returns success', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, 'startFlexibleUpdateAndroid'); + return 0; + }); - final result = await InAppUpdateFlutterPlatform.instance - .startFlexibleUpdateAndroid(); + final result = await plugin.startFlexibleUpdateAndroid(); expect(result, UpdateResultAndroid.success); }); }); + + group('completeUpdateAndroid', () { + test('calls completeUpdateAndroid on the channel', () async { + String? invokedMethod; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + invokedMethod = call.method; + return null; + }); + + await plugin.completeUpdateAndroid(); + expect(invokedMethod, 'completeUpdateAndroid'); + }); + }); }); } From 5015c72dfbb2fc4808e898cb7b89b28879bbba26 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:47:58 +0530 Subject: [PATCH 02/11] refactor: deduplicate startImmediate/flexibleUpdateAndroid into shared helper --- .../in_app_update_flutter_method_channel.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index c7141ae..00b9a11 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -64,19 +64,20 @@ class MethodChannelInAppUpdateFlutter { Future startImmediateUpdateAndroid({ bool allowAssetPackDeletion = false, - }) async { - final result = await _methodChannel.invokeMethod( - 'startImmediateUpdateAndroid', - {'allowAssetPackDeletion': allowAssetPackDeletion}, - ); - return UpdateResultAndroid.fromValue(result!); - } + }) => + _startUpdateAndroid('startImmediateUpdateAndroid', allowAssetPackDeletion); Future startFlexibleUpdateAndroid({ bool allowAssetPackDeletion = false, - }) async { + }) => + _startUpdateAndroid('startFlexibleUpdateAndroid', allowAssetPackDeletion); + + Future _startUpdateAndroid( + String method, + bool allowAssetPackDeletion, + ) async { final result = await _methodChannel.invokeMethod( - 'startFlexibleUpdateAndroid', + method, {'allowAssetPackDeletion': allowAssetPackDeletion}, ); return UpdateResultAndroid.fromValue(result!); From a5f0cd629d3ba561592ba1c19e8820032e6a83d5 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:49:23 +0530 Subject: [PATCH 03/11] feat: add checkAndUpdate() convenience method --- lib/in_app_update_flutter.dart | 13 +++++++++++++ .../in_app_update_flutter_method_channel.dart | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index 6328db9..21e062b 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -25,6 +25,19 @@ class InAppUpdateFlutter { Future startUpdate({String? appStoreId}) => _impl.startUpdate(appStoreId: appStoreId); + /// Cross-platform: Checks for an update and starts the flow if available. + /// + /// Convenience method that combines [checkUpdate] and [startUpdate]. + /// Returns the [AppUpdateInfo] so you can inspect the result. + Future checkAndUpdate({ + String? iosAppStoreRegion, + String? appStoreId, + }) => + _impl.checkAndUpdate( + iosAppStoreRegion: iosAppStoreRegion, + appStoreId: appStoreId, + ); + /// Shows the platform-specific in-app update UI. /// /// [appStoreId] is the numeric App Store ID of your app diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index 00b9a11..a5435e6 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -26,6 +26,14 @@ class MethodChannelInAppUpdateFlutter { throw UnsupportedError('Unsupported platform'); } + Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId}) async { + final info = await checkUpdate(iosAppStoreRegion: iosAppStoreRegion); + if (info.updateAvailable) { + await startUpdate(appStoreId: appStoreId); + } + return info; + } + Future startUpdate({String? appStoreId}) async { if (Platform.isIOS) { if (appStoreId == null) { From 9c6f05fca578e3c895e06d45c4335f2dcb2bce84 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:50:06 +0530 Subject: [PATCH 04/11] docs: clarify checkAndUpdate works on both platforms --- lib/in_app_update_flutter.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index 21e062b..e2b344a 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -28,6 +28,8 @@ class InAppUpdateFlutter { /// Cross-platform: Checks for an update and starts the flow if available. /// /// Convenience method that combines [checkUpdate] and [startUpdate]. + /// On iOS, [appStoreId] is required to present the App Store page. + /// On Android, no additional parameters are needed. /// Returns the [AppUpdateInfo] so you can inspect the result. Future checkAndUpdate({ String? iosAppStoreRegion, From 2f27c34d73b4519fad789fe774901a6612121d0f Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:52:10 +0530 Subject: [PATCH 05/11] feat: add UpdateConfig for app-level defaults (appStoreId, iosAppStoreRegion) --- lib/in_app_update_flutter.dart | 87 ++++++++++++++++--------------- lib/src/models/models.dart | 1 + lib/src/models/update_config.dart | 21 ++++++++ 3 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 lib/src/models/update_config.dart diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index e2b344a..dd6cc71 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -5,30 +5,56 @@ export 'package:in_app_update_flutter/src/models/models.dart'; /// A Flutter plugin for in-app updates. /// -/// Use [checkUpdate] and [startUpdate] for a cross-platform experience, -/// or call platform-specific methods directly for more control. +/// Create an instance with [UpdateConfig] to set defaults, then call +/// [checkUpdate], [startUpdate], or [checkAndUpdate]. +/// +/// ```dart +/// final updater = InAppUpdateFlutter(UpdateConfig( +/// appStoreId: '123456789', +/// iosAppStoreRegion: 'us', +/// )); +/// +/// // Simple: check and update in one call +/// final info = await updater.checkAndUpdate(); +/// +/// // Or step by step +/// final info = await updater.checkUpdate(); +/// if (info.updateAvailable) await updater.startUpdate(); +/// ``` class InAppUpdateFlutter { final _impl = MethodChannelInAppUpdateFlutter(); + final UpdateConfig _config; + + /// Creates an [InAppUpdateFlutter] instance. + /// + /// If [config] is provided, its values are used as defaults for all + /// operations. Per-call parameters always override config values. + InAppUpdateFlutter([this._config = const UpdateConfig()]); /// Cross-platform: Checks whether an update is available. /// /// On iOS, queries the iTunes Lookup API. On Android, uses Play Core. /// Returns a unified [AppUpdateInfo] with platform-specific details. + /// + /// [iosAppStoreRegion] overrides [UpdateConfig.iosAppStoreRegion] if provided. Future checkUpdate({String? iosAppStoreRegion}) => - _impl.checkUpdate(iosAppStoreRegion: iosAppStoreRegion); + _impl.checkUpdate( + iosAppStoreRegion: iosAppStoreRegion ?? _config.iosAppStoreRegion, + ); /// Cross-platform: Starts the update flow. /// - /// On iOS, presents the App Store product page via StoreKit - /// ([appStoreId] is required). + /// On iOS, presents the App Store product page via StoreKit. /// On Android, starts the immediate (blocking) update flow. + /// + /// [appStoreId] overrides [UpdateConfig.appStoreId] if provided. Future startUpdate({String? appStoreId}) => - _impl.startUpdate(appStoreId: appStoreId); + _impl.startUpdate(appStoreId: appStoreId ?? _config.appStoreId); /// Cross-platform: Checks for an update and starts the flow if available. /// /// Convenience method that combines [checkUpdate] and [startUpdate]. - /// On iOS, [appStoreId] is required to present the App Store page. + /// On iOS, [appStoreId] (or [UpdateConfig.appStoreId]) is required. /// On Android, no additional parameters are needed. /// Returns the [AppUpdateInfo] so you can inspect the result. Future checkAndUpdate({ @@ -36,14 +62,11 @@ class InAppUpdateFlutter { String? appStoreId, }) => _impl.checkAndUpdate( - iosAppStoreRegion: iosAppStoreRegion, - appStoreId: appStoreId, + iosAppStoreRegion: iosAppStoreRegion ?? _config.iosAppStoreRegion, + appStoreId: appStoreId ?? _config.appStoreId, ); /// Shows the platform-specific in-app update UI. - /// - /// [appStoreId] is the numeric App Store ID of your app - /// (found in your App Store Connect URL). @Deprecated( 'Use showUpdateForIos() on iOS or checkUpdateAndroid() + ' 'startImmediateUpdateAndroid()/startFlexibleUpdateAndroid() on Android', @@ -54,35 +77,25 @@ class InAppUpdateFlutter { /// iOS: Shows the App Store product page overlay via StoreKit. /// - /// [appStoreId] is the numeric App Store ID of your app - /// (found in your App Store Connect URL). - Future showUpdateForIos({required String appStoreId}) => - _impl.showUpdateForIos(appStoreId: appStoreId); + /// [appStoreId] overrides [UpdateConfig.appStoreId] if provided. + Future showUpdateForIos({String? appStoreId}) => + _impl.showUpdateForIos( + appStoreId: appStoreId ?? _config.appStoreId ?? '', + ); /// iOS: Checks whether an update is available via the iTunes Lookup API. /// - /// Queries the App Store for the latest published version and compares it - /// against the currently installed version using semantic versioning. - /// - /// An optional [iosAppStoreRegion] can be provided to check a specific - /// App Store region (e.g., `"us"`, `"gb"`). + /// [iosAppStoreRegion] overrides [UpdateConfig.iosAppStoreRegion] if provided. Future checkUpdateIos({String? iosAppStoreRegion}) => - _impl.checkUpdateIos(iosAppStoreRegion: iosAppStoreRegion); + _impl.checkUpdateIos( + iosAppStoreRegion: iosAppStoreRegion ?? _config.iosAppStoreRegion, + ); /// Android: Checks whether an in-app update is available via Play Core. - /// - /// Returns an [AppUpdateInfoAndroid] containing update metadata such as - /// availability, version code, priority, staleness, and allowed update types. Future checkUpdateAndroid() => _impl.checkUpdateAndroid(); /// Android: Starts the immediate (full-screen, blocking) update flow. - /// - /// The user must accept the update to continue using the app. If the user - /// closes the update screen, [UpdateResultAndroid.userCanceled] is returned. - /// - /// If [allowAssetPackDeletion] is `true`, the system may delete asset packs - /// to free up storage for the update. Future startImmediateUpdateAndroid({ bool allowAssetPackDeletion = false, }) => @@ -91,13 +104,6 @@ class InAppUpdateFlutter { ); /// Android: Starts the flexible (background download) update flow. - /// - /// The update downloads in the background while the user continues - /// using the app. Listen to [installStateStreamAndroid] for download - /// progress, and call [completeUpdateAndroid] when the download is complete. - /// - /// If [allowAssetPackDeletion] is `true`, the system may delete asset packs - /// to free up storage for the update. Future startFlexibleUpdateAndroid({ bool allowAssetPackDeletion = false, }) => @@ -106,14 +112,9 @@ class InAppUpdateFlutter { ); /// Android: Completes a flexible update by triggering an app restart. - /// - /// Call this after [installStateStreamAndroid] reports - /// [InstallStatusAndroid.downloaded]. Future completeUpdateAndroid() => _impl.completeUpdateAndroid(); /// Android: A stream of install state changes during a flexible update. - /// - /// Emits [InstallStateAndroid] events with download progress and status. Stream get installStateStreamAndroid => _impl.installStateStreamAndroid; } diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 4b2acc8..f259cac 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -4,4 +4,5 @@ export 'app_update_info_ios.dart'; export 'install_state_android.dart'; export 'install_status_android.dart'; export 'update_availability_android.dart'; +export 'update_config.dart'; export 'update_result_android.dart'; diff --git a/lib/src/models/update_config.dart b/lib/src/models/update_config.dart new file mode 100644 index 0000000..848477e --- /dev/null +++ b/lib/src/models/update_config.dart @@ -0,0 +1,21 @@ +/// Configuration for [InAppUpdateFlutter]. +/// +/// Provides default values for all update operations so you don't need to +/// pass the same parameters on every call. +class UpdateConfig { + /// The numeric App Store ID of your iOS app. + /// + /// Required on iOS for presenting the App Store product page via StoreKit. + /// Found in your App Store Connect URL. + final String? appStoreId; + + /// The App Store region to check for iOS updates (e.g., `"us"`, `"gb"`). + /// + /// If `null`, the default region is used. + final String? iosAppStoreRegion; + + const UpdateConfig({ + this.appStoreId, + this.iosAppStoreRegion, + }); +} From b3d195a804e2807447f1b489825f2c0b781c6f0e Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:53:43 +0530 Subject: [PATCH 06/11] docs: update README with UpdateConfig setup and cross-platform usage --- README.md | 123 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 02e8982..907831e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Flutter plugin for in-app updates on both iOS and Android. -On **iOS**, it presents the App Store product page using `SKStoreProductViewController` (StoreKit), keeping users inside the app during the update flow. On **Android**, it integrates with Google Play's In-App Updates API to support both immediate (blocking) and flexible (background) update flows. +On **iOS**, it checks for updates via the iTunes Lookup API and presents the App Store product page using `SKStoreProductViewController` (StoreKit), keeping users inside the app during the update flow. On **Android**, it integrates with Google Play's In-App Updates API to support both immediate (blocking) and flexible (background) update flows. --- @@ -16,14 +16,15 @@ On **iOS**, it presents the App Store product page using `SKStoreProductViewCont ## Features -- iOS: Show the App Store update prompt using `SKStoreProductViewController` without navigating users away from the app -- iOS: Native Swift implementation with zero AppDelegate configuration required -- iOS: Supports both Swift Package Manager (SPM) and CocoaPods -- Android: Check update availability and metadata via the Play Core API -- Android: Immediate update flow — full-screen, blocking prompt the user must accept -- Android: Flexible update flow — background download while the user continues using the app -- Android: Install state stream for monitoring flexible update download progress -- Works on Flutter with a simple, unified API +- **iOS**: Check for updates via iTunes Lookup API (pure Dart, no native code) +- **iOS**: Show the App Store update prompt using `SKStoreProductViewController` without navigating users away from the app +- **iOS**: Native Swift implementation with zero AppDelegate configuration required +- **iOS**: Supports both Swift Package Manager (SPM) and CocoaPods +- **Android**: Check update availability and metadata via the Play Core API +- **Android**: Immediate update flow — full-screen, blocking prompt the user must accept +- **Android**: Flexible update flow — background download while the user continues using the app +- **Android**: Install state stream for monitoring flexible update download progress +- **Cross-platform**: Unified `checkUpdate()`, `startUpdate()`, and `checkAndUpdate()` APIs --- @@ -44,16 +45,92 @@ flutter pub get --- -## iOS Usage +## Setup -Pass your numeric App Store ID to `showUpdateForIos`. The ID can be found in your App Store Connect URL or the app's public App Store link. +Create an `InAppUpdateFlutter` instance with an `UpdateConfig` to set your app's defaults: + +### Both iOS and Android ```dart import 'package:in_app_update_flutter/in_app_update_flutter.dart'; -await InAppUpdateFlutter().showUpdateForIos(appStoreId: '1234567890'); +final updater = InAppUpdateFlutter(UpdateConfig( + appStoreId: '1234567890', // Required for iOS + iosAppStoreRegion: 'us', // Optional: specific App Store region +)); +``` + +### iOS only + +```dart +final updater = InAppUpdateFlutter(UpdateConfig( + appStoreId: '1234567890', +)); +``` + +### Android only + +```dart +final updater = InAppUpdateFlutter(); +``` + +No configuration is needed on Android — the Play Core API handles everything natively. + +--- + +## Quick Start + +The simplest way to check and update in one call: + +```dart +final info = await updater.checkAndUpdate(); + +if (info.updateAvailable) { + // Update was started (App Store page on iOS, immediate update on Android) +} ``` +--- + +## Usage + +### Check for updates + +```dart +final info = await updater.checkUpdate(); + +if (info.updateAvailable) { + print('Update available: ${info.storeVersion}'); +} +``` + +### Start the update flow + +```dart +// On iOS: presents App Store page via StoreKit +// On Android: starts immediate (blocking) update +await updater.startUpdate(); +``` + +### Per-call overrides + +Config values can be overridden on any call: + +```dart +await updater.checkUpdate(iosAppStoreRegion: 'gb'); +await updater.startUpdate(appStoreId: '9876543210'); +await updater.checkAndUpdate( + iosAppStoreRegion: 'jp', + appStoreId: '9876543210', +); +``` + +--- + +## iOS Details + +Pass your numeric App Store ID to `showUpdateForIos` (or set it in `UpdateConfig`). The ID can be found in your App Store Connect URL or the app's public App Store link. + **How to find your App Store ID:** 1. Open your app's App Store URL — for example: `https://apps.apple.com/app/id1234567890` @@ -66,7 +143,7 @@ await InAppUpdateFlutter().showUpdateForIos(appStoreId: '1234567890'); --- -## Android Usage +## Android Details Android uses Google Play's In-App Updates API. The typical flow is: @@ -78,15 +155,11 @@ Android uses Google Play's In-App Updates API. The typical flow is: An immediate update presents a full-screen prompt that the user must complete before continuing. Use this for critical updates. ```dart -import 'package:in_app_update_flutter/in_app_update_flutter.dart'; - -final plugin = InAppUpdateFlutter(); - -final info = await plugin.checkUpdateAndroid(); +final info = await updater.checkUpdateAndroid(); if (info.updateAvailability == UpdateAvailabilityAndroid.updateAvailable && info.isImmediateUpdateAllowed) { - final result = await plugin.startImmediateUpdateAndroid(); + final result = await updater.startImmediateUpdateAndroid(); // result is UpdateResultAndroid.success or UpdateResultAndroid.userCanceled } ``` @@ -96,19 +169,15 @@ if (info.updateAvailability == UpdateAvailabilityAndroid.updateAvailable && A flexible update downloads in the background while the user continues using the app. When the download completes, call `completeUpdateAndroid()` to apply the update. ```dart -import 'package:in_app_update_flutter/in_app_update_flutter.dart'; - -final plugin = InAppUpdateFlutter(); - -final info = await plugin.checkUpdateAndroid(); +final info = await updater.checkUpdateAndroid(); if (info.updateAvailability == UpdateAvailabilityAndroid.updateAvailable && info.isFlexibleUpdateAllowed) { - await plugin.startFlexibleUpdateAndroid(); + await updater.startFlexibleUpdateAndroid(); - plugin.installStateStreamAndroid.listen((state) { + updater.installStateStreamAndroid.listen((state) { if (state.installStatus == InstallStatusAndroid.downloaded) { - plugin.completeUpdateAndroid(); + updater.completeUpdateAndroid(); } }); } From c123b13bccd548ebca4222f995512039c5c36737 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:55:49 +0530 Subject: [PATCH 07/11] refactor: checkAndUpdate returns void, just do the job --- README.md | 8 +++----- lib/in_app_update_flutter.dart | 3 +-- .../in_app_update_flutter_method_channel.dart | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 907831e..3d1c61c 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,11 @@ No configuration is needed on Android — the Play Core API handles everything n The simplest way to check and update in one call: ```dart -final info = await updater.checkAndUpdate(); - -if (info.updateAvailable) { - // Update was started (App Store page on iOS, immediate update on Android) -} +await updater.checkAndUpdate(); ``` +That's it. If an update is available, it starts the flow automatically (App Store page on iOS, immediate update on Android). + --- ## Usage diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index dd6cc71..53dbe71 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -56,8 +56,7 @@ class InAppUpdateFlutter { /// Convenience method that combines [checkUpdate] and [startUpdate]. /// On iOS, [appStoreId] (or [UpdateConfig.appStoreId]) is required. /// On Android, no additional parameters are needed. - /// Returns the [AppUpdateInfo] so you can inspect the result. - Future checkAndUpdate({ + Future checkAndUpdate({ String? iosAppStoreRegion, String? appStoreId, }) => diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index a5435e6..d8f6469 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -26,12 +26,11 @@ class MethodChannelInAppUpdateFlutter { throw UnsupportedError('Unsupported platform'); } - Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId}) async { + Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId}) async { final info = await checkUpdate(iosAppStoreRegion: iosAppStoreRegion); if (info.updateAvailable) { await startUpdate(appStoreId: appStoreId); } - return info; } Future startUpdate({String? appStoreId}) async { From 0f47991dc0399d3350d5ed2f68a653457c4c458a Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 16:57:13 +0530 Subject: [PATCH 08/11] feat: add androidUpdateType to UpdateConfig for default Android update style --- README.md | 15 +++++++-------- lib/in_app_update_flutter.dart | 14 ++++++++++---- .../in_app_update_flutter_method_channel.dart | 12 ++++++++---- lib/src/models/update_config.dart | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3d1c61c..d46207d 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,9 @@ Create an `InAppUpdateFlutter` instance with an `UpdateConfig` to set your app's import 'package:in_app_update_flutter/in_app_update_flutter.dart'; final updater = InAppUpdateFlutter(UpdateConfig( - appStoreId: '1234567890', // Required for iOS - iosAppStoreRegion: 'us', // Optional: specific App Store region + appStoreId: '1234567890', // Required for iOS + iosAppStoreRegion: 'us', // Optional: specific App Store region + androidUpdateType: AndroidUpdateType.flexible, // Optional: immediate (default) or flexible )); ``` @@ -71,22 +72,20 @@ final updater = InAppUpdateFlutter(UpdateConfig( ### Android only ```dart -final updater = InAppUpdateFlutter(); +final updater = InAppUpdateFlutter(UpdateConfig( + androidUpdateType: AndroidUpdateType.flexible, // or .immediate (default) +)); ``` -No configuration is needed on Android — the Play Core API handles everything natively. - --- ## Quick Start -The simplest way to check and update in one call: - ```dart await updater.checkAndUpdate(); ``` -That's it. If an update is available, it starts the flow automatically (App Store page on iOS, immediate update on Android). +If an update is available, it starts the flow automatically using your config defaults. --- diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index 53dbe71..59debbc 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -45,24 +45,30 @@ class InAppUpdateFlutter { /// Cross-platform: Starts the update flow. /// /// On iOS, presents the App Store product page via StoreKit. - /// On Android, starts the immediate (blocking) update flow. + /// On Android, starts the update flow using [UpdateConfig.androidUpdateType] + /// (default: immediate). /// /// [appStoreId] overrides [UpdateConfig.appStoreId] if provided. - Future startUpdate({String? appStoreId}) => - _impl.startUpdate(appStoreId: appStoreId ?? _config.appStoreId); + Future startUpdate({String? appStoreId, AndroidUpdateType? androidUpdateType}) => + _impl.startUpdate( + appStoreId: appStoreId ?? _config.appStoreId, + androidUpdateType: androidUpdateType ?? _config.androidUpdateType, + ); /// Cross-platform: Checks for an update and starts the flow if available. /// /// Convenience method that combines [checkUpdate] and [startUpdate]. /// On iOS, [appStoreId] (or [UpdateConfig.appStoreId]) is required. - /// On Android, no additional parameters are needed. + /// On Android, uses [UpdateConfig.androidUpdateType] by default. Future checkAndUpdate({ String? iosAppStoreRegion, String? appStoreId, + AndroidUpdateType? androidUpdateType, }) => _impl.checkAndUpdate( iosAppStoreRegion: iosAppStoreRegion ?? _config.iosAppStoreRegion, appStoreId: appStoreId ?? _config.appStoreId, + androidUpdateType: androidUpdateType ?? _config.androidUpdateType, ); /// Shows the platform-specific in-app update UI. diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index d8f6469..6e6ed07 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -26,21 +26,25 @@ class MethodChannelInAppUpdateFlutter { throw UnsupportedError('Unsupported platform'); } - Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId}) async { + Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId, AndroidUpdateType androidUpdateType = AndroidUpdateType.immediate}) async { final info = await checkUpdate(iosAppStoreRegion: iosAppStoreRegion); if (info.updateAvailable) { - await startUpdate(appStoreId: appStoreId); + await startUpdate(appStoreId: appStoreId, androidUpdateType: androidUpdateType); } } - Future startUpdate({String? appStoreId}) async { + Future startUpdate({String? appStoreId, AndroidUpdateType androidUpdateType = AndroidUpdateType.immediate}) async { if (Platform.isIOS) { if (appStoreId == null) { throw ArgumentError('appStoreId is required on iOS'); } await showUpdateForIos(appStoreId: appStoreId); } else if (Platform.isAndroid) { - await startImmediateUpdateAndroid(); + if (androidUpdateType == AndroidUpdateType.flexible) { + await startFlexibleUpdateAndroid(); + } else { + await startImmediateUpdateAndroid(); + } } else { throw UnsupportedError('Unsupported platform'); } diff --git a/lib/src/models/update_config.dart b/lib/src/models/update_config.dart index 848477e..e4dca31 100644 --- a/lib/src/models/update_config.dart +++ b/lib/src/models/update_config.dart @@ -1,3 +1,12 @@ +/// The type of update flow to use on Android. +enum AndroidUpdateType { + /// Full-screen, blocking update the user must accept. + immediate, + + /// Background download while the user continues using the app. + flexible, +} + /// Configuration for [InAppUpdateFlutter]. /// /// Provides default values for all update operations so you don't need to @@ -14,8 +23,15 @@ class UpdateConfig { /// If `null`, the default region is used. final String? iosAppStoreRegion; + /// The default update type on Android. + /// + /// Used by [InAppUpdateFlutter.startUpdate] and [checkAndUpdate]. + /// Defaults to [AndroidUpdateType.immediate]. + final AndroidUpdateType androidUpdateType; + const UpdateConfig({ this.appStoreId, this.iosAppStoreRegion, + this.androidUpdateType = AndroidUpdateType.immediate, }); } From c43f26590a8df1ed0e2186711a06daa22a173ca3 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 17:00:16 +0530 Subject: [PATCH 09/11] run dart format --- lib/in_app_update_flutter.dart | 6 +++--- .../in_app_update_flutter_method_channel.dart | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/in_app_update_flutter.dart b/lib/in_app_update_flutter.dart index 59debbc..39b06ec 100644 --- a/lib/in_app_update_flutter.dart +++ b/lib/in_app_update_flutter.dart @@ -49,7 +49,8 @@ class InAppUpdateFlutter { /// (default: immediate). /// /// [appStoreId] overrides [UpdateConfig.appStoreId] if provided. - Future startUpdate({String? appStoreId, AndroidUpdateType? androidUpdateType}) => + Future startUpdate( + {String? appStoreId, AndroidUpdateType? androidUpdateType}) => _impl.startUpdate( appStoreId: appStoreId ?? _config.appStoreId, androidUpdateType: androidUpdateType ?? _config.androidUpdateType, @@ -83,8 +84,7 @@ class InAppUpdateFlutter { /// iOS: Shows the App Store product page overlay via StoreKit. /// /// [appStoreId] overrides [UpdateConfig.appStoreId] if provided. - Future showUpdateForIos({String? appStoreId}) => - _impl.showUpdateForIos( + Future showUpdateForIos({String? appStoreId}) => _impl.showUpdateForIos( appStoreId: appStoreId ?? _config.appStoreId ?? '', ); diff --git a/lib/src/method_channel/in_app_update_flutter_method_channel.dart b/lib/src/method_channel/in_app_update_flutter_method_channel.dart index 6e6ed07..84a7e71 100644 --- a/lib/src/method_channel/in_app_update_flutter_method_channel.dart +++ b/lib/src/method_channel/in_app_update_flutter_method_channel.dart @@ -17,7 +17,8 @@ class MethodChannelInAppUpdateFlutter { Future checkUpdate({String? iosAppStoreRegion}) async { if (Platform.isIOS) { - final info = await checkUpdateIosImpl(iosAppStoreRegion: iosAppStoreRegion); + final info = + await checkUpdateIosImpl(iosAppStoreRegion: iosAppStoreRegion); return AppUpdateInfo.fromIos(info); } else if (Platform.isAndroid) { final info = await checkUpdateAndroid(); @@ -26,14 +27,22 @@ class MethodChannelInAppUpdateFlutter { throw UnsupportedError('Unsupported platform'); } - Future checkAndUpdate({String? iosAppStoreRegion, String? appStoreId, AndroidUpdateType androidUpdateType = AndroidUpdateType.immediate}) async { + Future checkAndUpdate( + {String? iosAppStoreRegion, + String? appStoreId, + AndroidUpdateType androidUpdateType = + AndroidUpdateType.immediate}) async { final info = await checkUpdate(iosAppStoreRegion: iosAppStoreRegion); if (info.updateAvailable) { - await startUpdate(appStoreId: appStoreId, androidUpdateType: androidUpdateType); + await startUpdate( + appStoreId: appStoreId, androidUpdateType: androidUpdateType); } } - Future startUpdate({String? appStoreId, AndroidUpdateType androidUpdateType = AndroidUpdateType.immediate}) async { + Future startUpdate( + {String? appStoreId, + AndroidUpdateType androidUpdateType = + AndroidUpdateType.immediate}) async { if (Platform.isIOS) { if (appStoreId == null) { throw ArgumentError('appStoreId is required on iOS'); @@ -76,7 +85,8 @@ class MethodChannelInAppUpdateFlutter { Future startImmediateUpdateAndroid({ bool allowAssetPackDeletion = false, }) => - _startUpdateAndroid('startImmediateUpdateAndroid', allowAssetPackDeletion); + _startUpdateAndroid( + 'startImmediateUpdateAndroid', allowAssetPackDeletion); Future startFlexibleUpdateAndroid({ bool allowAssetPackDeletion = false, From 2836b120e448de8be223b786e691199f4246cf52 Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 17:02:00 +0530 Subject: [PATCH 10/11] docs: add AGENTS.md and CLAUDE.md --- AGENTS.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 59 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..67c4a03 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md + +## Project + +Flutter plugin for in-app updates. iOS checks via iTunes Lookup API (pure Dart), shows App Store page via StoreKit. Android uses Play Core API (Kotlin native). + +## Structure + +``` +lib/ + in_app_update_flutter.dart # Public API, delegates to method channel + src/ + method_channel/ + in_app_update_flutter_method_channel.dart # Native method channel calls + models/ + models.dart # Barrel export + app_update_info.dart # Unified cross-platform result + app_update_info_ios.dart # iOS-specific result + app_update_info_android.dart # Android-specific result (pre-existing) + update_config.dart # UpdateConfig, AndroidUpdateType enum + update_availability_android.dart # Android availability enum (pre-existing) + update_result_android.dart # Android result enum (pre-existing) + install_state_android.dart # Android install state (pre-existing) + install_status_android.dart # Android install status enum (pre-existing) + ios_update_check.dart # Pure Dart iTunes Lookup API call +android/ # Kotlin native (Play Core) +ios/ # Swift native (StoreKit presentation only) +test/ # Unit tests +``` + +## Key patterns + +- `InAppUpdateFlutter` is the public entry point, takes optional `UpdateConfig` +- All methods delegate to `MethodChannelInAppUpdateFlutter` +- iOS update checking is pure Dart (`ios_update_check.dart`) — no native code +- iOS native code (`InAppUpdateFlutterPlugin.swift`) only handles `showStoreUpdateIos` (StoreKit) +- Android native code handles all Play Core methods +- `UpdateConfig` holds defaults: `appStoreId`, `iosAppStoreRegion`, `androidUpdateType` + +## Commands + +```bash +flutter analyze +flutter test +``` + +## Dependencies + +- `package_info_plus` — get installed version and bundle ID (used by iOS check) +- `pub_semver` — semantic version comparison (used by iOS check) +- No `plugin_platform_interface` — was removed, not needed + +## Conventions + +- Models use `const` constructors and factory methods +- Method channel methods are `snake_case` strings matching native side +- Deprecation via `@Deprecated` annotation, not removal +- Cross-platform methods (`checkUpdate`, `startUpdate`, `checkAndUpdate`) route via `Platform.isIOS`/`Platform.isAndroid` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4a73cfb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Read AGENTS.md From 33da8de75e2b837437feeb4b094479914b672ecc Mon Sep 17 00:00:00 2001 From: Akshay CM Date: Mon, 8 Jun 2026 17:04:19 +0530 Subject: [PATCH 11/11] docs: add dart format step to AGENTS.md --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 67c4a03..a6177f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,13 @@ test/ # Unit tests ## Commands ```bash +dart format lib/ flutter analyze flutter test ``` +Always run `dart format lib/` before committing. + ## Dependencies - `package_info_plus` — get installed version and bundle ID (used by iOS check)