From 9af28ab984cc74b28edeeb87ee8a58c2eecb0f03 Mon Sep 17 00:00:00 2001 From: Vailence Date: Tue, 5 May 2026 19:47:57 +0500 Subject: [PATCH 01/10] MOBILE-160: move operationsDomain after required params Place all required Configuration constructor params consecutively, with operationsDomain among the optional params at the end. --- .../lib/src/types/configuration.dart | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/mindbox_platform_interface/lib/src/types/configuration.dart b/mindbox_platform_interface/lib/src/types/configuration.dart index bcd725d..7962bd2 100644 --- a/mindbox_platform_interface/lib/src/types/configuration.dart +++ b/mindbox_platform_interface/lib/src/types/configuration.dart @@ -9,11 +9,16 @@ class Configuration { this.previousDeviceUUID = '', this.previousInstallationId = '', this.shouldCreateCustomer = true, + this.operationsDomain, }); /// Used for generating baseurl for REST. final String domain; + /// Optional host for sending operations. Overridden by + /// the value from the mobile JSON config when present. Default `nil` (use `domain`). + final String? operationsDomain; + /// Used for app identification on iOS. final String endpointIos; @@ -34,13 +39,19 @@ class Configuration { final bool shouldCreateCustomer; /// Returns map of parameters - Map toMap() => { - 'domain': domain, - 'endpointIos': endpointIos, - 'endpointAndroid': endpointAndroid, - 'previousDeviceUUID': previousDeviceUUID, - 'previousInstallationId': previousInstallationId, - 'subscribeCustomerIfCreated': subscribeCustomerIfCreated, - 'shouldCreateCustomer': shouldCreateCustomer, - }; + Map toMap() { + final map = { + 'domain': domain, + 'endpointIos': endpointIos, + 'endpointAndroid': endpointAndroid, + 'previousDeviceUUID': previousDeviceUUID, + 'previousInstallationId': previousInstallationId, + 'subscribeCustomerIfCreated': subscribeCustomerIfCreated, + 'shouldCreateCustomer': shouldCreateCustomer, + }; + if (operationsDomain != null && operationsDomain!.isNotEmpty) { + map['operationsDomain'] = operationsDomain; + } + return map; + } } From 39f119bcb15befd707024c26447bcfe07c2e160e Mon Sep 17 00:00:00 2001 From: Vailence Date: Tue, 5 May 2026 19:49:37 +0500 Subject: [PATCH 02/10] MOBILE-160: align operationsDomain with sibling fields style Switch from nullable String? with conditional toMap to default empty string with unconditional serialization. Native sides treat empty as "not set" (Android skips builder call; iOS converts to nil), mirroring how previousDeviceUUID and previousInstallationId are wired. --- .../mindbox_android/MindboxAndroidPlugin.kt | 2 ++ .../ios/Classes/SwiftMindboxIosPlugin.swift | 4 ++- .../lib/src/types/configuration.dart | 29 +++++++--------- .../test/src/types/configuration_test.dart | 33 +++++++++++++++++++ 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt index be7d742..1f53ae0 100644 --- a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt +++ b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt @@ -72,11 +72,13 @@ class MindboxAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Ne val previousInstallId: String = args["previousInstallationId"] as String val subscribeIfCreated: Boolean = args["subscribeCustomerIfCreated"] as Boolean val shouldCreateCustomer: Boolean = args["shouldCreateCustomer"] as Boolean + val operationsDomain: String = args["operationsDomain"] as? String ?: "" val config = MindboxConfiguration.Builder(context.applicationContext, domain, endpointId) .setPreviousDeviceUuid(previousDeviceUuid) .setPreviousInstallationId(previousInstallId) .subscribeCustomerIfCreated(subscribeIfCreated) .shouldCreateCustomer(shouldCreateCustomer) + .apply { if (operationsDomain.isNotEmpty()) operationsDomain(operationsDomain) } .build() Mindbox.init(activity = context, config, listOf()) result.success("initialized") diff --git a/mindbox_ios/ios/Classes/SwiftMindboxIosPlugin.swift b/mindbox_ios/ios/Classes/SwiftMindboxIosPlugin.swift index 6fb7119..5d07fa9 100644 --- a/mindbox_ios/ios/Classes/SwiftMindboxIosPlugin.swift +++ b/mindbox_ios/ios/Classes/SwiftMindboxIosPlugin.swift @@ -84,8 +84,10 @@ public class SwiftMindboxIosPlugin: NSObject, FlutterPlugin { let shouldCreateCustomer = args["shouldCreateCustomer"] as? Bool{ let prevUuid = previousUuid.isEmpty ? nil : previousUuid let prevId = previousInstallId.isEmpty ? nil : previousInstallId + let operationsDomainRaw = args["operationsDomain"] as? String ?? "" + let operationsDomain = operationsDomainRaw.isEmpty ? nil : operationsDomainRaw do{ - let config = try MBConfiguration(endpoint: endpoint, domain: domain,previousInstallationId: prevId, previousDeviceUUID: prevUuid, subscribeCustomerIfCreated: subscribeIfCreated, shouldCreateCustomer: shouldCreateCustomer) + let config = try MBConfiguration(endpoint: endpoint, domain: domain, operationsDomain: operationsDomain, previousInstallationId: prevId, previousDeviceUUID: prevUuid, subscribeCustomerIfCreated: subscribeIfCreated, shouldCreateCustomer: shouldCreateCustomer) Mindbox.shared.initialization(configuration: config) result("initialized") }catch let error { diff --git a/mindbox_platform_interface/lib/src/types/configuration.dart b/mindbox_platform_interface/lib/src/types/configuration.dart index 7962bd2..a2a69cb 100644 --- a/mindbox_platform_interface/lib/src/types/configuration.dart +++ b/mindbox_platform_interface/lib/src/types/configuration.dart @@ -9,7 +9,7 @@ class Configuration { this.previousDeviceUUID = '', this.previousInstallationId = '', this.shouldCreateCustomer = true, - this.operationsDomain, + this.operationsDomain = '', }); /// Used for generating baseurl for REST. @@ -17,7 +17,7 @@ class Configuration { /// Optional host for sending operations. Overridden by /// the value from the mobile JSON config when present. Default `nil` (use `domain`). - final String? operationsDomain; + final String operationsDomain; /// Used for app identification on iOS. final String endpointIos; @@ -39,19 +39,14 @@ class Configuration { final bool shouldCreateCustomer; /// Returns map of parameters - Map toMap() { - final map = { - 'domain': domain, - 'endpointIos': endpointIos, - 'endpointAndroid': endpointAndroid, - 'previousDeviceUUID': previousDeviceUUID, - 'previousInstallationId': previousInstallationId, - 'subscribeCustomerIfCreated': subscribeCustomerIfCreated, - 'shouldCreateCustomer': shouldCreateCustomer, - }; - if (operationsDomain != null && operationsDomain!.isNotEmpty) { - map['operationsDomain'] = operationsDomain; - } - return map; - } + Map toMap() => { + 'domain': domain, + 'endpointIos': endpointIos, + 'endpointAndroid': endpointAndroid, + 'previousDeviceUUID': previousDeviceUUID, + 'previousInstallationId': previousInstallationId, + 'subscribeCustomerIfCreated': subscribeCustomerIfCreated, + 'shouldCreateCustomer': shouldCreateCustomer, + 'operationsDomain': operationsDomain, + }; } diff --git a/mindbox_platform_interface/test/src/types/configuration_test.dart b/mindbox_platform_interface/test/src/types/configuration_test.dart index b5c6647..ee1576d 100644 --- a/mindbox_platform_interface/test/src/types/configuration_test.dart +++ b/mindbox_platform_interface/test/src/types/configuration_test.dart @@ -12,6 +12,7 @@ void main() { previousDeviceUUID: 'previousDeviceUUID', subscribeCustomerIfCreated: true, shouldCreateCustomer: true, + operationsDomain: 'operations.example.com', ); // Assert @@ -22,5 +23,37 @@ void main() { expect(configuration.previousDeviceUUID, 'previousDeviceUUID'); expect(configuration.subscribeCustomerIfCreated, true); expect(configuration.shouldCreateCustomer, true); + expect(configuration.operationsDomain, 'operations.example.com'); + }); + + test('operationsDomain defaults to empty string when not provided', () { + final Configuration configuration = Configuration( + domain: 'domain', + endpointIos: 'iOSEndpoint', + endpointAndroid: 'androidEndpoint', + ); + + expect(configuration.operationsDomain, ''); + }); + + test('toMap includes operationsDomain when set', () { + final Configuration configuration = Configuration( + domain: 'domain', + endpointIos: 'iOSEndpoint', + endpointAndroid: 'androidEndpoint', + operationsDomain: 'operations.example.com', + ); + + expect(configuration.toMap()['operationsDomain'], 'operations.example.com'); + }); + + test('toMap returns empty operationsDomain when not provided', () { + final Configuration configuration = Configuration( + domain: 'domain', + endpointIos: 'iOSEndpoint', + endpointAndroid: 'androidEndpoint', + ); + + expect(configuration.toMap()['operationsDomain'], ''); }); } From 0598966ca6834528b644716fbc5664b8240c3aeb Mon Sep 17 00:00:00 2001 From: Vailence Date: Tue, 5 May 2026 19:50:18 +0500 Subject: [PATCH 03/10] MOBILE-160: add channel-level tests for operationsDomain Capture init MethodCall args and verify operationsDomain is forwarded across the platform channel both when set and when defaulted. --- .../types/mindbox_method_handler_test.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart b/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart index dd04d93..ed685e0 100644 --- a/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart +++ b/mindbox_platform_interface/test/src/types/mindbox_method_handler_test.dart @@ -50,6 +50,56 @@ void main() { }, ); + test( + 'init() forwards operationsDomain to native channel', + () async { + final capturedArgs = {}; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + if (call.method == 'init') { + capturedArgs.addAll(Map.from(call.arguments)); + } + return mindboxMockMethodCallHandler(call); + }); + + await handler.init( + configuration: Configuration( + domain: 'domain', + endpointIos: 'endpointIos', + endpointAndroid: 'endpointAndroid', + operationsDomain: 'operations.example.com', + ), + ); + + expect(capturedArgs['operationsDomain'], 'operations.example.com'); + }, + ); + + test( + 'init() forwards empty operationsDomain by default', + () async { + final capturedArgs = {}; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + if (call.method == 'init') { + capturedArgs.addAll(Map.from(call.arguments)); + } + return mindboxMockMethodCallHandler(call); + }); + + await handler.init( + configuration: Configuration( + domain: 'domain', + endpointIos: 'endpointIos', + endpointAndroid: 'endpointAndroid', + ), + ); + + expect(capturedArgs.containsKey('operationsDomain'), isTrue); + expect(capturedArgs['operationsDomain'], ''); + }, + ); + test( 'When config is invalid, init() calling should throws MindboxException', () async { From ad7f4cab3a7bd3244e94add45a8674adae2b2dd2 Mon Sep 17 00:00:00 2001 From: Vailence Date: Tue, 5 May 2026 19:51:01 +0500 Subject: [PATCH 04/10] MOBILE-160: minor cleanups around operationsDomain - Configuration dartdoc: use Dart `null`/empty wording, remove the native-config reference that leaks SDK-internal behavior. - MindboxAndroidPlugin: rename local to operationsDomainArg and apply the builder method outside the chain to avoid name collision with the builder method. - example: drop the placeholder commented-out arg from the sample app. --- .../mindbox/mindbox_android/MindboxAndroidPlugin.kt | 10 ++++++---- .../lib/src/types/configuration.dart | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt index 1f53ae0..106cf86 100644 --- a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt +++ b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt @@ -72,14 +72,16 @@ class MindboxAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Ne val previousInstallId: String = args["previousInstallationId"] as String val subscribeIfCreated: Boolean = args["subscribeCustomerIfCreated"] as Boolean val shouldCreateCustomer: Boolean = args["shouldCreateCustomer"] as Boolean - val operationsDomain: String = args["operationsDomain"] as? String ?: "" - val config = MindboxConfiguration.Builder(context.applicationContext, domain, endpointId) + val operationsDomainArg: String = args["operationsDomain"] as? String ?: "" + val builder = MindboxConfiguration.Builder(context.applicationContext, domain, endpointId) .setPreviousDeviceUuid(previousDeviceUuid) .setPreviousInstallationId(previousInstallId) .subscribeCustomerIfCreated(subscribeIfCreated) .shouldCreateCustomer(shouldCreateCustomer) - .apply { if (operationsDomain.isNotEmpty()) operationsDomain(operationsDomain) } - .build() + if (operationsDomainArg.isNotEmpty()) { + builder.operationsDomain(operationsDomainArg) + } + val config = builder.build() Mindbox.init(activity = context, config, listOf()) result.success("initialized") } else { diff --git a/mindbox_platform_interface/lib/src/types/configuration.dart b/mindbox_platform_interface/lib/src/types/configuration.dart index a2a69cb..00fbe35 100644 --- a/mindbox_platform_interface/lib/src/types/configuration.dart +++ b/mindbox_platform_interface/lib/src/types/configuration.dart @@ -15,8 +15,8 @@ class Configuration { /// Used for generating baseurl for REST. final String domain; - /// Optional host for sending operations. Overridden by - /// the value from the mobile JSON config when present. Default `nil` (use `domain`). + /// Optional separate host for sending operations. When empty, [domain] + /// is used. Default is an empty string. final String operationsDomain; /// Used for app identification on iOS. From 5cfafe6ee643a833f3eb1e445eb596ea75696722 Mon Sep 17 00:00:00 2001 From: Vailence Date: Wed, 6 May 2026 17:04:55 +0500 Subject: [PATCH 05/10] MOBILE-160 Fix PR Comms --- .../cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt index 106cf86..e587c8b 100644 --- a/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt +++ b/mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt @@ -73,15 +73,13 @@ class MindboxAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, Ne val subscribeIfCreated: Boolean = args["subscribeCustomerIfCreated"] as Boolean val shouldCreateCustomer: Boolean = args["shouldCreateCustomer"] as Boolean val operationsDomainArg: String = args["operationsDomain"] as? String ?: "" - val builder = MindboxConfiguration.Builder(context.applicationContext, domain, endpointId) + val config = MindboxConfiguration.Builder(context.applicationContext, domain, endpointId) .setPreviousDeviceUuid(previousDeviceUuid) .setPreviousInstallationId(previousInstallId) .subscribeCustomerIfCreated(subscribeIfCreated) .shouldCreateCustomer(shouldCreateCustomer) - if (operationsDomainArg.isNotEmpty()) { - builder.operationsDomain(operationsDomainArg) - } - val config = builder.build() + .operationsDomain(operationsDomainArg) + .build() Mindbox.init(activity = context, config, listOf()) result.success("initialized") } else { From 233c1df8f8c06c336ad89b18e37ec092683c4b49 Mon Sep 17 00:00:00 2001 From: Sergei Semko <28645140+justSmK@users.noreply.github.com> Date: Tue, 12 May 2026 21:41:40 +0300 Subject: [PATCH 06/10] MOBILE-171: Document scene-mode semantics in `MindboxFlutterAppDelegate` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MindboxFlutterAppDelegate` is scene-safe (never touches `window` or `rootViewController`), but the inline comments did not reflect how its callbacks behave once the host app declares `UIApplicationSceneManifest`. Add markers at the two callbacks whose semantics change: `application(_:didFinishLaunchingWithOptions:)` — `launchOptions` is nil in scene mode; the cold-start payload arrives in the customer's `scene(_:willConnectTo:options:)` and must be forwarded via `Mindbox.shared.track(.launchScene(...))`. `application(_:continue:restorationHandler:)` — not invoked in scene mode; universal links arrive in the customer's `scene(_:continue:)` and must be forwarded via `Mindbox.shared.track(.universalLink(...))`. Translate the pre-existing inline comments in the file from Russian to English to align with the repository's language convention. No behavior change. --- .../Classes/MindboxFlutterAppDelegate.swift | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/mindbox_ios/ios/Classes/MindboxFlutterAppDelegate.swift b/mindbox_ios/ios/Classes/MindboxFlutterAppDelegate.swift index a0c2427..253d07d 100644 --- a/mindbox_ios/ios/Classes/MindboxFlutterAppDelegate.swift +++ b/mindbox_ios/ios/Classes/MindboxFlutterAppDelegate.swift @@ -3,7 +3,7 @@ import Flutter import Mindbox import MindboxNotifications -open class MindboxFlutterAppDelegate: FlutterAppDelegate{ +open class MindboxFlutterAppDelegate: FlutterAppDelegate { open func shouldRegisterForRemoteNotifications() -> Bool { return true @@ -13,7 +13,7 @@ open class MindboxFlutterAppDelegate: FlutterAppDelegate{ _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - + if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self } @@ -21,36 +21,40 @@ open class MindboxFlutterAppDelegate: FlutterAppDelegate{ if shouldRegisterForRemoteNotifications() { registerForRemoteNotifications() } - // Регистрация фоновых задач для iOS выше 13 + // Background task registration for iOS 13+ if #available(iOS 13.0, *) { Mindbox.shared.registerBGTasks() } else { UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) } - - // Передача факта открытия приложения + + // Pass the application launch event. + // Under UISceneDelegate, launchOptions == nil — the real cold-start + // payload arrives in scene(_:willConnectTo:options:) of the customer's + // SceneDelegate and must be forwarded via + // Mindbox.shared.track(.launchScene(...)). Mindbox.shared.track(.launch(launchOptions)) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + // MARK: didRegisterForRemoteNotificationsWithDeviceToken - // Передача токена APNS в SDK Mindbox + // Pass the APNS token to the Mindbox SDK. open override func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Mindbox.shared.apnsTokenUpdate(deviceToken: deviceToken) } - - // Регистрация фоновых задач для iOS до 13 + + // Background task registration for iOS below 13. open override func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Mindbox.shared.application(application, performFetchWithCompletionHandler: completionHandler) super.application(application, performFetchWithCompletionHandler: completionHandler) } - + // MARK: registerForRemoteNotifications - // Функция запроса разрешения на уведомления. В комплишн блоке надо передать статус разрешения в SDK Mindbox + // Notification permission request. The completion block must forward the permission status to the Mindbox SDK. func registerForRemoteNotifications() { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() @@ -63,37 +67,40 @@ open class MindboxFlutterAppDelegate: FlutterAppDelegate{ } } } - + + // Under UISceneDelegate this callback is not invoked — universal links + // arrive in scene(_:continue:) of the customer's SceneDelegate and + // must be forwarded via Mindbox.shared.track(.universalLink(...)). open override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { - // Передача ссылки, если приложение открыто через universalLink + // Pass the link if the application was opened via a universal link. Mindbox.shared.track(.universalLink(userActivity)) return super.application(application, continue: userActivity, restorationHandler: restorationHandler) } - + open override func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .badge, .sound]) } - + // MARK: didReceive response - // Функция обработки кликов по нотификации + // Push notification click handler. open override func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - - // передача факта клика по пушу + + // Pass the push click event. Mindbox.shared.pushClicked(response: response) - - // передача факта открытия приложения по переходу на пуш + + // Pass the application launch event from push notification tap. Mindbox.shared.track(.push(response)) - + completionHandler() super.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) } From 5ff4a826b1e068339fcef5dcf3a49aaeef1908c0 Mon Sep 17 00:00:00 2001 From: Sergei Semko <28645140+justSmK@users.noreply.github.com> Date: Mon, 18 May 2026 15:32:32 +0300 Subject: [PATCH 07/10] MOBILE-171: Migrate example app to UISceneDelegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `SceneDelegate.swift` forwarding `.launchScene` and `.universalLink` to Mindbox, and declare `UIApplicationSceneManifest` in `Info.plist`. The two example `AppDelegate` variants now conform to `FlutterImplicitEngineDelegate`: under UISceneDelegate the engine is created by the scene, so `window`/`rootViewController` are not yet available in `didFinishLaunchingWithOptions`, and plugin / `FlutterEventChannel` setup has to move into `didInitializeImplicitFlutterEngine(_:)`. Each `AppDelegate` carries a header block labeling it as the migrated variant with links to Flutter's UISceneDelegate guide and the Mindbox `UISCENE_MIGRATION.md`, plus a commented-out pre-migration class at the bottom of the file as a reference for projects still on the legacy AppDelegate-only flow. `pubspec.yaml` now requires Flutter `>=3.41` (first version where UISceneDelegate is the recommended Flutter iOS lifecycle and `FlutterImplicitEngineDelegate` is available) so anyone trying to run the example on an older SDK gets a clear `pub get` error instead of a build-time surprise. The SDK packages themselves keep their existing Flutter `>=2.0.0` constraint — only the example is gated. --- example/flutter_example/.gitignore | 5 + .../ios/Flutter/AppFrameworkInfo.plist | 2 - example/flutter_example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 26 ++++ .../xcshareddata/xcschemes/Runner.xcscheme | 21 +++ .../ios/Runner/AppDelegate.swift | 128 ++++++++++++++---- .../AppDelegateUsedMindboxDelegate.swift | 84 +++++++++--- example/flutter_example/ios/Runner/Info.plist | 21 +++ .../ios/Runner/SceneDelegate.swift | 27 ++++ example/flutter_example/pubspec.yaml | 5 + 10 files changed, 278 insertions(+), 43 deletions(-) create mode 100644 example/flutter_example/ios/Runner/SceneDelegate.swift diff --git a/example/flutter_example/.gitignore b/example/flutter_example/.gitignore index f72845b..0506d31 100644 --- a/example/flutter_example/.gitignore +++ b/example/flutter_example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -48,3 +50,6 @@ app.*.map.json # iOS /ios/Pods/ /ios/Podfile.lock + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/example/flutter_example/ios/Flutter/AppFrameworkInfo.plist b/example/flutter_example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..391a902 100644 --- a/example/flutter_example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/flutter_example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/example/flutter_example/ios/Podfile b/example/flutter_example/ios/Podfile index 86eb313..0370271 100644 --- a/example/flutter_example/ios/Podfile +++ b/example/flutter_example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/flutter_example/ios/Runner.xcodeproj/project.pbxproj b/example/flutter_example/ios/Runner.xcodeproj/project.pbxproj index fe218f6..6a0d24c 100644 --- a/example/flutter_example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/flutter_example/ios/Runner.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3A04C4242C18A6EA008FB1C3 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A04C41C2C183779008FB1C3 /* Models.swift */; }; 3AFCC3DC2C6A0B4000F047AB /* AppDelegateUsedMindboxDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCC3DB2C6A0B4000F047AB /* AppDelegateUsedMindboxDelegate.swift */; }; + 3AFCC3E02C6A0B4000F047AB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCC3DF2C6A0B4000F047AB /* SceneDelegate.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 8B81E312DFA9C6FD4EDFB0F6 /* Pods_MindboxNotificationContentExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 201DB479D6282529C705748E /* Pods_MindboxNotificationContentExtension.framework */; }; @@ -25,6 +26,7 @@ E1B395592BD985350090F3D2 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B395582BD985350090F3D2 /* NotificationViewController.swift */; }; E1B395602BD985350090F3D2 /* MindboxNotificationContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E1B395522BD985350090F3D2 /* MindboxNotificationContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; FDACF0FD3A7597BBE1F0C9BD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E84BF714550B188D12C0B51 /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -86,6 +88,7 @@ 3A04C41C2C183779008FB1C3 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; 3A04C4202C18A4E0008FB1C3 /* Mindbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Mindbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AFCC3DB2C6A0B4000F047AB /* AppDelegateUsedMindboxDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateUsedMindboxDelegate.swift; sourceTree = ""; }; + 3AFCC3DF2C6A0B4000F047AB /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4AC547651623FA3602973E87 /* Pods_MindboxNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MindboxNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C419D08BE184EB1CC17FC7F /* Pods-MindboxNotificationContentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationContentExtension.release.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationContentExtension/Pods-MindboxNotificationContentExtension.release.xcconfig"; sourceTree = ""; }; @@ -118,6 +121,7 @@ E1B3955D2BD985350090F3D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E1B395652BD985560090F3D2 /* MindboxNotificationContentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MindboxNotificationContentExtension.entitlements; sourceTree = ""; }; EE5FB2A6B1C9CC1DF5D97EF8 /* Pods-MindboxNotificationContentExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationContentExtension.profile.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationContentExtension/Pods-MindboxNotificationContentExtension.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -132,6 +136,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, FDACF0FD3A7597BBE1F0C9BD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -197,6 +202,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -234,6 +240,7 @@ isa = PBXGroup; children = ( 3AFCC3DB2C6A0B4000F047AB /* AppDelegateUsedMindboxDelegate.swift */, + 3AFCC3DF2C6A0B4000F047AB /* SceneDelegate.swift */, E19AAF922BD7F53B002D7897 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -290,6 +297,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -355,6 +365,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -583,6 +596,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 3AFCC3DC2C6A0B4000F047AB /* AppDelegateUsedMindboxDelegate.swift in Sources */, + 3AFCC3E02C6A0B4000F047AB /* SceneDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1264,6 +1278,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/flutter_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/flutter_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a67b2f3..25000c4 100644 --- a/example/flutter_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/flutter_example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + diff --git a/example/flutter_example/ios/Runner/AppDelegate.swift b/example/flutter_example/ios/Runner/AppDelegate.swift index f0b1572..aa7c32e 100644 --- a/example/flutter_example/ios/Runner/AppDelegate.swift +++ b/example/flutter_example/ios/Runner/AppDelegate.swift @@ -4,68 +4,77 @@ import mindbox_ios import Mindbox import UserNotifications -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +// Example AppDelegate migrated to UISceneDelegate. Requires Flutter >= 3.41. +// Mindbox-side notes: +// https://github.com/mindbox-cloud/flutter-sdk/blob/develop/UISCENE_MIGRATION.md +// Legacy (pre-migration) variant is kept commented out at the bottom for reference. +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { private var eventSink: FlutterEventSink? - + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - + UNUserNotificationCenter.current().delegate = self - + // tracking sources of referrals to the application via push notifications Mindbox.shared.track(.launch(launchOptions)) - + // registering background tasks for iOS above 13 if #available(iOS 13.0, *) { Mindbox.shared.registerBGTasks() } else { UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) } - - //Used for notification center - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - let eventChannel = FlutterEventChannel(name: "cloud.mindbox.flutter_example.notifications", binaryMessenger: controller.binaryMessenger) - eventChannel.setStreamHandler(self) - - GeneratedPluginRegistrant.register(with: self) + + // Plugin / FlutterEventChannel setup lives in didInitializeImplicitFlutterEngine(_:). return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let eventChannel = FlutterEventChannel( + name: "cloud.mindbox.flutter_example.notifications", + binaryMessenger: engineBridge.applicationRegistrar.messenger() + ) + eventChannel.setStreamHandler(self) + } + override func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Transfer to SDK APNs token Mindbox.shared.apnsTokenUpdate(deviceToken: deviceToken) } - + override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { - // Passing the link if the application is opened via universalLink + // Universal link in cold-start (AppDelegate-only flow). + // Under UISceneDelegate this method is not invoked — see SceneDelegate.swift. Mindbox.shared.track(.universalLink(userActivity)) return super.application(application, continue: userActivity, restorationHandler: restorationHandler) } - + // Register background tasks for iOS up to 13 override func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Mindbox.shared.application(application, performFetchWithCompletionHandler: completionHandler) } - - + + override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { //Implement display of standard notifications completionHandler([.alert, .badge, .sound]) notifyFlutterNewData() } - + override func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -73,13 +82,13 @@ import UserNotifications ) { // Send click to Mindbox Mindbox.shared.pushClicked(response: response) - + // Sending the fact that the application was opened when switching to push notification Mindbox.shared.track(.push(response)) completionHandler() super.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) } - + func notifyFlutterNewData() { if let eventSink = eventSink { eventSink("newNotification") @@ -92,9 +101,82 @@ extension AppDelegate: FlutterStreamHandler { self.eventSink = events return nil } - + func onCancel(withArguments arguments: Any?) -> FlutterError? { self.eventSink = nil return nil } } + +// MARK: - Legacy variant (pre-UISceneDelegate, Flutter < 3.41) +// +// Kept here as a reference for projects that haven't migrated to +// UISceneDelegate yet (Info.plist without `UIApplicationSceneManifest`). +// To roll back: replace the class above with the version below, remove +// `SceneDelegate.swift` and the `UIApplicationSceneManifest` entry in +// `Info.plist`. +// +// @UIApplicationMain +// @objc class AppDelegate: FlutterAppDelegate { +// private var eventSink: FlutterEventSink? +// +// override func application( +// _ application: UIApplication, +// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +// ) -> Bool { +// UIApplication.shared.registerForRemoteNotifications() +// registerForRemoteNotifications() +// +// // Tracks the source that opened the app (push, universal link, etc.). +// Mindbox.shared.track(.launch(launchOptions)) +// +// if #available(iOS 13.0, *) { +// Mindbox.shared.registerBGTasks() +// } else { +// UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) +// } +// +// // Under AppDelegate-only flow, `window`/`rootViewController` are +// // ready by this point — plugin and channel setup happen here. +// let controller = window?.rootViewController as! FlutterViewController +// let eventChannel = FlutterEventChannel( +// name: "cloud.mindbox.flutter_example.notifications", +// binaryMessenger: controller.binaryMessenger +// ) +// eventChannel.setStreamHandler(self) +// GeneratedPluginRegistrant.register(with: self) +// +// return super.application(application, didFinishLaunchingWithOptions: launchOptions) +// } +// +// override func application( +// _ application: UIApplication, +// continue userActivity: NSUserActivity, +// restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void +// ) -> Bool { +// // Under AppDelegate-only flow universal links arrive here. +// // After scene migration this method is not invoked — the event +// // is handled by `SceneDelegate.scene(_:continue:)` instead. +// Mindbox.shared.track(.universalLink(userActivity)) +// return super.application(application, continue: userActivity, restorationHandler: restorationHandler) +// } +// +// func registerForRemoteNotifications() { +// UNUserNotificationCenter.current().delegate = self +// DispatchQueue.main.async { +// UNUserNotificationCenter.current().requestAuthorization( +// options: [.alert, .sound, .badge] +// ) { granted, error in +// if let error = error { +// print("NotificationsRequestAuthorization failed: \(error.localizedDescription)") +// } +// Mindbox.shared.notificationsRequestAuthorization(granted: granted) +// } +// } +// } +// +// // The remaining overrides (`didRegisterForRemoteNotificationsWithDeviceToken`, +// // `performFetchWithCompletionHandler`, the two `userNotificationCenter` +// // delegate methods, the `FlutterStreamHandler` extension) are +// // identical to the migrated variant above. +// } diff --git a/example/flutter_example/ios/Runner/AppDelegateUsedMindboxDelegate.swift b/example/flutter_example/ios/Runner/AppDelegateUsedMindboxDelegate.swift index 6f63dc8..04dd324 100644 --- a/example/flutter_example/ios/Runner/AppDelegateUsedMindboxDelegate.swift +++ b/example/flutter_example/ios/Runner/AppDelegateUsedMindboxDelegate.swift @@ -4,41 +4,49 @@ import mindbox_ios import Mindbox import UserNotifications - -@objc class AppDelegateUsedMindboxDelegate: MindboxFlutterAppDelegate { +// Example variant using `MindboxFlutterAppDelegate` as base class (APNS, +// `.push`, BG tasks come from the base class). Migrated to UISceneDelegate. +// Requires Flutter >= 3.41. +// Mindbox-side notes: +// https://github.com/mindbox-cloud/flutter-sdk/blob/develop/UISCENE_MIGRATION.md +// Legacy (pre-migration) variant is kept commented out at the bottom for reference. +@main +@objc class AppDelegateUsedMindboxDelegate: MindboxFlutterAppDelegate, FlutterImplicitEngineDelegate { private var eventSink: FlutterEventSink? - + override func shouldRegisterForRemoteNotifications() -> Bool { return true } - + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - UIApplication.shared.registerForRemoteNotifications() - GeneratedPluginRegistrant.register(with: self) - - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - let eventChannel = FlutterEventChannel(name: "cloud.mindbox.flutter_example.notifications", binaryMessenger: controller.binaryMessenger) - eventChannel.setStreamHandler(self) - - UNUserNotificationCenter.current().delegate = self - + // Plugin / FlutterEventChannel setup lives in didInitializeImplicitFlutterEngine(_:). return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let eventChannel = FlutterEventChannel( + name: "cloud.mindbox.flutter_example.notifications", + binaryMessenger: engineBridge.applicationRegistrar.messenger() + ) + eventChannel.setStreamHandler(self) + } + func notifyFlutterNewData() { if let eventSink = eventSink { eventSink("newNotification") } } - + override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { super.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler) notifyFlutterNewData() } - + } extension AppDelegateUsedMindboxDelegate: FlutterStreamHandler { @@ -46,9 +54,51 @@ extension AppDelegateUsedMindboxDelegate: FlutterStreamHandler { self.eventSink = events return nil } - + func onCancel(withArguments arguments: Any?) -> FlutterError? { self.eventSink = nil return nil } } + +// MARK: - Legacy variant (pre-UISceneDelegate, Flutter < 3.41) +// +// Kept here as a reference for projects that haven't migrated to +// UISceneDelegate yet (Info.plist without `UIApplicationSceneManifest`). +// To roll back: replace the class above with the version below, remove +// `SceneDelegate.swift` and the `UIApplicationSceneManifest` entry in +// `Info.plist`. +// +// @main +// @objc class AppDelegateUsedMindboxDelegate: MindboxFlutterAppDelegate { +// private var eventSink: FlutterEventSink? +// +// override func shouldRegisterForRemoteNotifications() -> Bool { +// return true +// } +// +// override func application( +// _ application: UIApplication, +// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +// ) -> Bool { +// UIApplication.shared.registerForRemoteNotifications() +// GeneratedPluginRegistrant.register(with: self) +// +// // Under AppDelegate-only flow `window`/`rootViewController` are +// // ready here, so plugin and channel setup happen in this method. +// let controller = window?.rootViewController as! FlutterViewController +// let eventChannel = FlutterEventChannel( +// name: "cloud.mindbox.flutter_example.notifications", +// binaryMessenger: controller.binaryMessenger +// ) +// eventChannel.setStreamHandler(self) +// +// UNUserNotificationCenter.current().delegate = self +// +// return super.application(application, didFinishLaunchingWithOptions: launchOptions) +// } +// +// // The remaining `userNotificationCenter(_:willPresent:...)` override +// // and the `FlutterStreamHandler` extension are identical to the +// // migrated variant above. +// } diff --git a/example/flutter_example/ios/Runner/Info.plist b/example/flutter_example/ios/Runner/Info.plist index d51f971..3ada10c 100644 --- a/example/flutter_example/ios/Runner/Info.plist +++ b/example/flutter_example/ios/Runner/Info.plist @@ -40,6 +40,27 @@ processing remote-notification + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/example/flutter_example/ios/Runner/SceneDelegate.swift b/example/flutter_example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..bab1cdf --- /dev/null +++ b/example/flutter_example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,27 @@ +import UIKit +import Flutter +import Mindbox + +// Sample SceneDelegate forwarding scene events to Mindbox. Requires Flutter >= 3.35. +// Mindbox-side notes: +// https://github.com/mindbox-cloud/flutter-sdk/blob/develop/mindbox_ios/UISCENE_MIGRATION.md +@available(iOS 13.0, *) +class SceneDelegate: FlutterSceneDelegate { + + override func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + Mindbox.shared.track(.launchScene(connectionOptions)) + super.scene(scene, willConnectTo: session, options: connectionOptions) + } + + override func scene( + _ scene: UIScene, + continue userActivity: NSUserActivity + ) { + Mindbox.shared.track(.universalLink(userActivity)) + super.scene(scene, continue: userActivity) + } +} diff --git a/example/flutter_example/pubspec.yaml b/example/flutter_example/pubspec.yaml index 84ac68d..5283f64 100644 --- a/example/flutter_example/pubspec.yaml +++ b/example/flutter_example/pubspec.yaml @@ -6,6 +6,11 @@ version: 1.0.0+1 environment: sdk: '>=3.3.4 <4.0.0' + # Example is migrated to UISceneDelegate (FlutterImplicitEngineDelegate). + # Flutter 3.41 is the first version where UISceneDelegate-based lifecycle + # is the recommended path for new iOS Flutter apps. See: + # https://docs.flutter.dev/release/breaking-changes/uiscenedelegate + flutter: '>=3.41.0' dependencies: flutter: From 7c897443d98b5da0e0c45ea90a705567e02d4a5e Mon Sep 17 00:00:00 2001 From: Sergei Semko <28645140+justSmK@users.noreply.github.com> Date: Tue, 19 May 2026 12:15:42 +0300 Subject: [PATCH 08/10] MOBILE-171: Document UISceneDelegate migration for integrators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps that declare `UIApplicationSceneManifest` in `Info.plist` no longer receive `application(_:didFinishLaunchingWithOptions:)` with a populated `launchOptions`, and `application(_:continue:restorationHandler:)` is not invoked at all — universal-link arrivals land in `scene(_:continue:)` on the scene delegate. Without forwarding those events Mindbox loses launch and universal-link tracking. `UISCENE_MIGRATION.md` (at the repo root for discoverability) describes only the Mindbox-side glue: where to call `Mindbox.shared.track(.launchScene(_:))` and `Mindbox.shared.track(.universalLink(_:))` from the customer's `SceneDelegate`, and confirms that the rest (APNS token, push handlers, BG tasks, notification extensions, `MindboxFlutterAppDelegate`) needs no changes under scene mode. Anything that is not Mindbox-specific points at the official Flutter UISceneDelegate guide instead of duplicating it. The repo `README.md` is the single entry point linking to the guide — the per-package READMEs (`mindbox`, `mindbox_ios`) are not the right place for scene-mode integration guidance, since UISceneDelegate is an app-side migration with no implications for the SDK packages themselves. --- README.md | 10 ++++ UISCENE_MIGRATION.md | 110 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 UISCENE_MIGRATION.md diff --git a/README.md b/README.md index b3898c8..fb4556d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,16 @@ Learn how to send events to Mindbox. Create a new Operation class object and set Mindbox SDK helps handle push notifications. Configuration and usage instructions can be found in the SDK documentation [here](https://developers.mindbox.ru/docs/firebase-send-push-notifications-flutter), [here](https://developers.mindbox.ru/docs/huawei-send-push-notifications-flutter) and [here](https://developers.mindbox.ru/docs/ios-send-push-notifications-flutter). +### iOS UISceneDelegate migration + +If your iOS app declares `UIApplicationSceneManifest` in `Info.plist` +(Flutter's [recommended iOS lifecycle][flutter-uiscene] since 3.41), follow +[UISCENE_MIGRATION.md](UISCENE_MIGRATION.md) to update your `AppDelegate` +and add a `SceneDelegate`. Apps that keep the legacy `AppDelegate`-only +flow don't need any code changes. + +[flutter-uiscene]: https://docs.flutter.dev/release/breaking-changes/uiscenedelegate + ## Troubleshooting Refer to the [Example of integration(IOS)](https://github.com/mindbox-cloud/flutter-sdk/tree/develop/mindbox_ios/example) or [Example of integration(Android)](https://github.com/mindbox-cloud/flutter-sdk/tree/develop/mindbox_android/example) in case of any issues. diff --git a/UISCENE_MIGRATION.md b/UISCENE_MIGRATION.md new file mode 100644 index 0000000..7269c6f --- /dev/null +++ b/UISCENE_MIGRATION.md @@ -0,0 +1,110 @@ +# Migrating a Mindbox Flutter SDK integration to UISceneDelegate + +Starting with Flutter 3.35 the iOS engine ships `FlutterSceneDelegate`, and +since Flutter 3.41 `UISceneDelegate`-based lifecycle is the recommended path +for new iOS Flutter apps. The official Flutter migration guide is the +authoritative source for everything not Mindbox-specific: + +- [Flutter UISceneDelegate migration guide][flutter-uiscene] + +This document only describes the Mindbox-side glue you need on top of the +Flutter migration: where to forward `.launchScene` and `.universalLink` +events, and what stays in your `AppDelegate`. It does **not** repeat the +Flutter-side steps already covered in the link above (`Info.plist` scene +manifest, `FlutterImplicitEngineDelegate` boilerplate, etc.) — follow them +there and use this page for the Mindbox-specific bits. + +If your `Info.plist` does **not** contain `UIApplicationSceneManifest`, +nothing changes for you. The Mindbox iOS SDK pod itself does not depend on +any scene-specific Flutter API, so it keeps building and running on every +Flutter version we supported before. You can update to the latest Mindbox +SDK without touching your code. + +## Why this matters for Mindbox integrators + +Under `UIApplicationSceneManifest` two `AppDelegate` callbacks Mindbox +previously relied on stop working as before: + +- `application(_:didFinishLaunchingWithOptions:)` still fires, but + `launchOptions` is `nil`, so `Mindbox.shared.track(.launch(launchOptions))` + tracks nothing useful. +- `application(_:continue:restorationHandler:)` is never invoked — universal + links arrive in `scene(_:continue:)` instead. + +To keep Mindbox receiving launch and universal-link events you need to +forward them from your scene delegate. + +## Prerequisites + +- **Flutter ≥ 3.41** — required for `FlutterImplicitEngineDelegate` on the + app side (see the Flutter guide above). +- iOS deployment target ≥ 13.0. + +## What to add in your scene delegate + +Copy +[`example/flutter_example/ios/Runner/SceneDelegate.swift`](../example/flutter_example/ios/Runner/SceneDelegate.swift) +into your `Runner` target. The two relevant calls: + +```swift +override func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions +) { + Mindbox.shared.track(.launchScene(connectionOptions)) + super.scene(scene, willConnectTo: session, options: connectionOptions) +} + +override func scene( + _ scene: UIScene, + continue userActivity: NSUserActivity +) { + Mindbox.shared.track(.universalLink(userActivity)) + super.scene(scene, continue: userActivity) +} +``` + +That replaces, respectively, `Mindbox.shared.track(.launch(launchOptions))` +in `application(_:didFinishLaunchingWithOptions:)` and +`Mindbox.shared.track(.universalLink(userActivity))` in +`application(_:continue:restorationHandler:)`. + +You may keep both AppDelegate-side and SceneDelegate-side calls — in scene +mode the AppDelegate-side `.launch(nil)` and `application(_:continue:)` are +inert (`launchOptions == nil`, the method is not invoked), so the two paths +do not produce duplicate events. + +## What stays unchanged in your AppDelegate + +Even after the scene migration you keep all of the following on your +`AppDelegate` (typically a subclass of `MindboxFlutterAppDelegate`): + +- `Mindbox.shared.apnsTokenUpdate(deviceToken:)` in + `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`. +- `Mindbox.shared.pushClicked(response:)` and + `Mindbox.shared.track(.push(response))` in your + `UNUserNotificationCenterDelegate` methods. +- `Mindbox.shared.registerBGTasks()`. +- Notification permission requests. + +`UNUserNotificationCenterDelegate` is a process-global API and is not +affected by scene mode, so push handling needs no changes. + +`MindboxFlutterAppDelegate` itself is scene-safe — it does not touch +`window` or `rootViewController` — and continues to work as a base class +unchanged. + +## Notification Service / Content Extensions + +`MindboxNotificationServiceExtension` and +`MindboxNotificationContentExtension` are extension processes and have +nothing to do with the host app's scene flow. They need no changes. + +## Reference implementation + +[`example/flutter_example/ios/Runner`](../example/flutter_example/ios/Runner) +is fully migrated and serves as a working reference for both the +Flutter-side and Mindbox-side parts of the migration. + +[flutter-uiscene]: https://docs.flutter.dev/release/breaking-changes/uiscenedelegate From 29a8b0ab90872bffdec97eb354c2fc13d3872c54 Mon Sep 17 00:00:00 2001 From: Sergei Semko <28645140+justSmK@users.noreply.github.com> Date: Tue, 19 May 2026 19:04:06 +0300 Subject: [PATCH 09/10] MOBILE-171: Fix doc paths and clarify Flutter version note in scene example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After UISCENE_MIGRATION.md moved to the repo root, the in-source link in SceneDelegate.swift and two relative paths inside the doc still pointed at the old `mindbox_ios/...` and `../example/...` locations. Switched them to absolute GitHub URLs on `develop`, matching the developers.mindbox.ru callout and the in-source comment. Also rewrote the SceneDelegate.swift header comment: `FlutterSceneDelegate` itself needs Flutter ≥ 3.35, but the example app pins to ≥ 3.41 because of `FlutterImplicitEngineDelegate` in AppDelegate. --- UISCENE_MIGRATION.md | 4 ++-- example/flutter_example/ios/Runner/SceneDelegate.swift | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/UISCENE_MIGRATION.md b/UISCENE_MIGRATION.md index 7269c6f..5fda547 100644 --- a/UISCENE_MIGRATION.md +++ b/UISCENE_MIGRATION.md @@ -43,7 +43,7 @@ forward them from your scene delegate. ## What to add in your scene delegate Copy -[`example/flutter_example/ios/Runner/SceneDelegate.swift`](../example/flutter_example/ios/Runner/SceneDelegate.swift) +[`example/flutter_example/ios/Runner/SceneDelegate.swift`](https://github.com/mindbox-cloud/flutter-sdk/blob/develop/example/flutter_example/ios/Runner/SceneDelegate.swift) into your `Runner` target. The two relevant calls: ```swift @@ -103,7 +103,7 @@ nothing to do with the host app's scene flow. They need no changes. ## Reference implementation -[`example/flutter_example/ios/Runner`](../example/flutter_example/ios/Runner) +[`example/flutter_example/ios/Runner`](https://github.com/mindbox-cloud/flutter-sdk/tree/develop/example/flutter_example/ios/Runner) is fully migrated and serves as a working reference for both the Flutter-side and Mindbox-side parts of the migration. diff --git a/example/flutter_example/ios/Runner/SceneDelegate.swift b/example/flutter_example/ios/Runner/SceneDelegate.swift index bab1cdf..d575ed3 100644 --- a/example/flutter_example/ios/Runner/SceneDelegate.swift +++ b/example/flutter_example/ios/Runner/SceneDelegate.swift @@ -2,9 +2,12 @@ import UIKit import Flutter import Mindbox -// Sample SceneDelegate forwarding scene events to Mindbox. Requires Flutter >= 3.35. +// Sample SceneDelegate forwarding scene events to Mindbox. +// `FlutterSceneDelegate` itself requires Flutter >= 3.35; this example app +// also relies on `FlutterImplicitEngineDelegate` in AppDelegate, which is +// Flutter >= 3.41. // Mindbox-side notes: -// https://github.com/mindbox-cloud/flutter-sdk/blob/develop/mindbox_ios/UISCENE_MIGRATION.md +// https://github.com/mindbox-cloud/flutter-sdk/blob/develop/UISCENE_MIGRATION.md @available(iOS 13.0, *) class SceneDelegate: FlutterSceneDelegate { From f73f7c389344edebd8c8c370a0ba9ec1ecaf10c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 14:03:12 +0000 Subject: [PATCH 10/10] Bump SDK versions: Flutter=2.15.2, Android=2.15.2, iOS=2.15.1 --- mindbox/CHANGELOG.md | 5 +++++ mindbox/pubspec.yaml | 8 ++++---- mindbox_android/CHANGELOG.md | 4 ++++ mindbox_android/android/build.gradle | 2 +- mindbox_android/pubspec.yaml | 4 ++-- mindbox_ios/CHANGELOG.md | 4 ++++ mindbox_ios/ios/mindbox_ios.podspec | 6 +++--- mindbox_ios/pubspec.yaml | 4 ++-- mindbox_platform_interface/CHANGELOG.md | 5 +++++ mindbox_platform_interface/pubspec.yaml | 2 +- 10 files changed, 31 insertions(+), 13 deletions(-) diff --git a/mindbox/CHANGELOG.md b/mindbox/CHANGELOG.md index 9212ca2..ff78d00 100644 --- a/mindbox/CHANGELOG.md +++ b/mindbox/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.15.2 + +* Upgrade native Android SDK dependency to v2.15.2. +* Upgrade native iOS SDK dependency to v2.15.1. + ## 2.15.1 * Upgrade native Android SDK dependency to v2.15.1. diff --git a/mindbox/pubspec.yaml b/mindbox/pubspec.yaml index 6029d95..1975261 100644 --- a/mindbox/pubspec.yaml +++ b/mindbox/pubspec.yaml @@ -1,6 +1,6 @@ name: mindbox description: Flutter Mindbox SDK. Plugin wrapper over of Mindbox iOS/Android SDK. -version: 2.15.1 +version: 2.15.2 homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox documentation: https://developers.mindbox.ru/docs/flutter-sdk-integration @@ -20,9 +20,9 @@ flutter: dependencies: flutter: sdk: flutter - mindbox_android: ^2.15.1 - mindbox_ios: ^2.15.1 - mindbox_platform_interface: ^2.15.1 + mindbox_android: ^2.15.2 + mindbox_ios: ^2.15.2 + mindbox_platform_interface: ^2.15.2 dev_dependencies: flutter_test: diff --git a/mindbox_android/CHANGELOG.md b/mindbox_android/CHANGELOG.md index 7366545..4a7e745 100644 --- a/mindbox_android/CHANGELOG.md +++ b/mindbox_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.15.2 + +* Upgrade native Android SDK dependency to v2.15.2. + ## 2.15.1 * Upgrade native Android SDK dependency to v2.15.1. diff --git a/mindbox_android/android/build.gradle b/mindbox_android/android/build.gradle index 990c134..6a56dab 100644 --- a/mindbox_android/android/build.gradle +++ b/mindbox_android/android/build.gradle @@ -50,5 +50,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api 'cloud.mindbox:mobile-sdk:2.15.1' + api 'cloud.mindbox:mobile-sdk:2.15.2' } diff --git a/mindbox_android/pubspec.yaml b/mindbox_android/pubspec.yaml index 046fb3a..4632a0b 100644 --- a/mindbox_android/pubspec.yaml +++ b/mindbox_android/pubspec.yaml @@ -1,6 +1,6 @@ name: mindbox_android description: The implementation of 'mindbox' plugin for the Android platform. -version: 2.15.1 +version: 2.15.2 homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_android @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - mindbox_platform_interface: ^2.15.1 + mindbox_platform_interface: ^2.15.2 dev_dependencies: flutter_test: diff --git a/mindbox_ios/CHANGELOG.md b/mindbox_ios/CHANGELOG.md index 670692d..4c20726 100644 --- a/mindbox_ios/CHANGELOG.md +++ b/mindbox_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.15.2 + +* Upgrade native iOS SDK dependency to v2.15.1. + ## 2.15.1 * Upgrade native iOS SDK dependency to v2.15.0. diff --git a/mindbox_ios/ios/mindbox_ios.podspec b/mindbox_ios/ios/mindbox_ios.podspec index 53b054d..1b40a94 100644 --- a/mindbox_ios/ios/mindbox_ios.podspec +++ b/mindbox_ios/ios/mindbox_ios.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'mindbox_ios' - s.version = '2.15.0' + s.version = '2.15.1' s.summary = 'Mindbox Flutter SDK' s.description = <<-DESC The implementation of 'mindbox' plugin for the iOS platform @@ -15,8 +15,8 @@ The implementation of 'mindbox' plugin for the iOS platform s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'Mindbox', '2.15.0' - s.dependency 'MindboxNotifications', '2.15.0' + s.dependency 'Mindbox', '2.15.1' + s.dependency 'MindboxNotifications', '2.15.1' s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. diff --git a/mindbox_ios/pubspec.yaml b/mindbox_ios/pubspec.yaml index efa7f9f..e49c476 100644 --- a/mindbox_ios/pubspec.yaml +++ b/mindbox_ios/pubspec.yaml @@ -1,6 +1,6 @@ name: mindbox_ios description: The implementation of 'mindbox' plugin for the iOS platform. -version: 2.15.1 +version: 2.15.2 homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_ios @@ -18,7 +18,7 @@ flutter: dependencies: flutter: sdk: flutter - mindbox_platform_interface: ^2.15.1 + mindbox_platform_interface: ^2.15.2 dev_dependencies: flutter_test: diff --git a/mindbox_platform_interface/CHANGELOG.md b/mindbox_platform_interface/CHANGELOG.md index bcb041a..0b19b14 100644 --- a/mindbox_platform_interface/CHANGELOG.md +++ b/mindbox_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.15.2 + +* Upgrade native Android SDK dependency to v2.15.2. +* Upgrade native iOS SDK dependency to v2.15.1. + ## 2.15.1 * Upgrade native Android SDK dependency to v2.15.1. diff --git a/mindbox_platform_interface/pubspec.yaml b/mindbox_platform_interface/pubspec.yaml index 1bc0dd2..dfd4b1f 100644 --- a/mindbox_platform_interface/pubspec.yaml +++ b/mindbox_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: mindbox_platform_interface description: Mindbox platform interface. -version: 2.15.1 +version: 2.15.2 homepage: https://mindbox.cloud/ repository: https://github.com/mindbox-cloud/flutter-sdk/tree/master/mindbox_platform_interface