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 {