Skip to content

fix(ios): adopt FlutterSceneLifeCycleDelegate for UIScene deep link support#438

Merged
Kobikg78 merged 5 commits intodevelopmentfrom
fix/DELIVERY-114618-uiscene-lifecycle
Mar 26, 2026
Merged

fix(ios): adopt FlutterSceneLifeCycleDelegate for UIScene deep link support#438
Kobikg78 merged 5 commits intodevelopmentfrom
fix/DELIVERY-114618-uiscene-lifecycle

Conversation

@Kobikg78
Copy link
Copy Markdown
Collaborator

Summary

Fixes deep link breakage on iOS apps using Flutter 3.41+ (DELIVERY-114618).

Problem

Flutter 3.41 auto-migrates apps to UIScene by default. When UIScene is active, UIKit routes deep links through the scene delegate (scene:openURLContexts:, scene:willConnectToSession:options:) rather than the traditional AppDelegate methods (application:openURL:options:). The plugin only implemented the AppDelegate path, causing deep links to be silently dropped.

Fix

AppsflyerSdkPlugin now conforms to FlutterSceneLifeCycleDelegate and registers via [registrar addSceneDelegate:instance] (iOS 13+). Three new methods added:

  • scene:willConnectToSession:options: — cold-start URI-scheme deep links
  • scene:openURLContexts: — warm-start URI-scheme deep links
  • scene:continueUserActivity: — Universal Links via UIScene

No app-side changes required.

Testing

  • Reproduced the bug on iOS 18.4 simulator + Flutter 3.41.4 (confirmed broken delegate chain via system logs)
  • Verified fix: scene:willConnectToSession:options: fires before startSDK, onDeepLinking Status.FOUND delivered correctly
  • Full E2E: 40/40 checks passed (Android + iOS 18.4, all 3 phases)

Fixes: DELIVERY-114618

…upport

The plugin only implemented AppDelegate-based lifecycle methods for deep
linking. Since Flutter 3.41 auto-migrates apps to UIScene by default,
UIKit no longer calls application:openURL:options: or
application:continueUserActivity: — routing everything through the scene
delegate instead. This caused deep links to be silently dropped on iOS
apps using Flutter 3.41+.

Fix:
- Adopt FlutterSceneLifeCycleDelegate in AppsflyerSdkPlugin
- Register via [registrar addSceneDelegate:instance] (iOS 13+)
- Add scene:openURLContexts: for warm-start URI-scheme deep links
- Add scene:willConnectToSession:options: for cold-start URI-scheme deep links
- Add scene:continueUserActivity: for Universal Links via UIScene

Verified on iOS 18.4 simulator with Flutter 3.41.4. Both cold-start and
warm-start deep link callbacks now reach the plugin correctly without
requiring any app-side workaround.

Fixes: DELIVERY-114618

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Dani-Koza-AF
Copy link
Copy Markdown
Collaborator

Deprecated getViewController not fixed - ios/Classes/AppsflyerSdkPlugin.m
Line 908-912 still uses UIApplication.delegate.window which returns nil under UIScene:

+ (FlutterViewController*) getViewController{
    UIViewController *topMostViewControllerObj =  [[[UIApplication sharedApplication] delegate] window].rootViewController;
    FlutterViewController *flutterViewController = (FlutterViewController *)topMostViewControllerObj;
    return flutterViewController;
}

While not directly related to deep linking, this is a ticking time bomb for any code path that calls it under UIScene.

@Dani-Koza-AF
Copy link
Copy Markdown
Collaborator

Per the Flutter migration guide for plugins, the minimum Flutter SDK should be bumped to >=3.38.0 (or the protocol adoption must be compile-time guarded). Currently i see it's flutter: ">=1.10.0" you might have to take care of it too.

Kobikg78 and others added 4 commits March 25, 2026 17:08
- getViewController: replaced deprecated UIApplication.delegate.window
  (returns nil under UIScene) with UIWindowScene lookup on iOS 13+,
  falling back to the AppDelegate window for older iOS versions.

- pubspec.yaml: bumped Flutter minimum SDK from >=1.10.0 to >=3.38.0
  as required by the Flutter plugin migration guide for adopting
  FlutterSceneLifeCycleDelegate.

Addresses review comments on PR #438.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The example app declares .env as a Flutter asset (needed for flutter_dotenv),
but .env is gitignored and not present in CI. This caused both Android and iOS
builds to fail with "No file or variants found for asset: .env".

Add a CI step to create a minimal dummy .env before each platform build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of hard-requiring Flutter >=3.38.0, wrap FlutterSceneLifeCycleDelegate
protocol adoption and addSceneDelegate with #if __has_include guards.

- Flutter >=3.38 (header present): UIScene delegate registered, all three
  scene callbacks active — deep links work under UIScene migration.
- Flutter <3.38 (header absent): guard skips it at compile time, plugin
  falls back to application:openURL: — backward compatible.

pubspec.yaml reverts flutter constraint to >=1.10.0 (no breaking change).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes based on review feedback:

1. openURLContexts / willConnectToSession: pass sourceApplication from
   UIOpenURLContext.options instead of empty @{}, so AppsFlyer SDK receives
   correct attribution metadata on URI-scheme deep links.

2. willConnectToSession: also iterate connectionOptions.userActivities and
   forward NSUserActivityTypeBrowsingWeb entries to continueUserActivity:,
   fixing cold-start Universal Links under UIScene (the core reported issue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Kobikg78
Copy link
Copy Markdown
Collaborator Author

Fixed in commit 2317776. getViewController now iterates [UIApplication sharedApplication].connectedScenes to find the foreground-active UIWindowScene, then falls back to delegate.window on iOS < 13.

@Kobikg78
Copy link
Copy Markdown
Collaborator Author

Addressed in commit 783530c with a compile-time #if __has_include guard instead of raising the minimum Flutter version. The pubspec.yaml stays at flutter: ">=1.10.0" — Flutter <3.38 builds silently skip the UIScene delegate adoption, while Flutter >=3.38 gets the full fix. No breaking change for existing users.

@Kobikg78
Copy link
Copy Markdown
Collaborator Author

Replying to #438 (comment)

Deprecated getViewController not fixed — still uses UIApplication.delegate.window which returns nil under UIScene.

Fixed. getViewController now uses UIWindowScene.connectedScenes to find the foreground-active scene on iOS 13+, and falls back to delegate.window only on older iOS:

+ (FlutterViewController*) getViewController {
    UIWindow *window = nil;
    if (@available(iOS 13.0, *)) {
        for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) {
            if (scene.activationState == UISceneActivationStateForegroundActive) {
                window = scene.windows.firstObject;
                break;
            }
        }
    }
    if (window == nil) {
        window = [[[UIApplication sharedApplication] delegate] window];
    }
    UIViewController *topMostViewControllerObj = window.rootViewController;
    return (FlutterViewController *)topMostViewControllerObj;
}

Copy link
Copy Markdown
Collaborator

@Dani-Koza-AF Dani-Koza-AF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @Kobikg78 !

@Kobikg78 Kobikg78 merged commit 13053ba into development Mar 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants