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 1/4] 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 2/4] 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 3/4] 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 4/4] 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 {