Skip to content

Commit 13053ba

Browse files
authored
Merge pull request #438 from AppsFlyerSDK/fix/DELIVERY-114618-uiscene-lifecycle
fix(ios): adopt FlutterSceneLifeCycleDelegate for UIScene deep link support
2 parents dabf605 + c7c1dba commit 13053ba

3 files changed

Lines changed: 88 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,26 @@ jobs:
164164
working-directory: example
165165
run: flutter pub get
166166

167-
# Step 7: Build Android APK (debug mode)
167+
# Step 7: Create dummy .env file for CI (not committed to repo)
168+
- name: 🔑 Create dummy .env for CI build
169+
working-directory: example
170+
run: |
171+
echo "DEV_KEY=dummy_dev_key" > .env
172+
echo "APP_ID=dummy_app_id" >> .env
173+
174+
# Step 8: Build Android APK (debug mode)
168175
# This validates that the plugin integrates correctly with Android
169176
- name: 🔨 Build Android APK (debug)
170177
working-directory: example
171178
run: flutter build apk --debug
172179

173-
# Step 8: Build Android App Bundle (release mode, no signing)
180+
# Step 9: Build Android App Bundle (release mode, no signing)
174181
# App Bundle is the preferred format for Play Store
175182
- name: 🔨 Build Android App Bundle (release)
176183
working-directory: example
177184
run: flutter build appbundle --release
178-
179-
# Step 9: Upload build artifacts (optional)
185+
186+
# Step 10: Upload build artifacts (optional)
180187
# Useful for manual testing or archiving
181188
- name: 📤 Upload APK artifact
182189
if: success()
@@ -239,19 +246,26 @@ jobs:
239246
working-directory: example/ios
240247
run: pod install
241248

242-
# Step 8: Build for iOS Simulator (fastest iOS build)
249+
# Step 8: Create dummy .env file for CI (not committed to repo)
250+
- name: 🔑 Create dummy .env for CI build
251+
working-directory: example
252+
run: |
253+
echo "DEV_KEY=dummy_dev_key" > .env
254+
echo "APP_ID=dummy_app_id" >> .env
255+
256+
# Step 9: Build for iOS Simulator (fastest iOS build)
243257
# Validates that the plugin compiles for iOS
244258
- name: 🔨 Build iOS for Simulator
245259
working-directory: example
246260
run: flutter build ios --simulator --debug
247261

248-
# Step 9: Build iOS IPA without code signing (release mode)
262+
# Step 10: Build iOS IPA without code signing (release mode)
249263
# This validates a full release build without requiring certificates
250264
- name: 🔨 Build iOS IPA (no codesign)
251265
working-directory: example
252266
run: flutter build ipa --release --no-codesign
253-
254-
# Step 10: Upload build artifacts (optional)
267+
268+
# Step 11: Upload build artifacts (optional)
255269
- name: 📤 Upload iOS build artifact
256270
if: success()
257271
uses: actions/upload-artifact@v4

ios/Classes/AppsflyerSdkPlugin.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
#import "AppsFlyerLib.h"
77
#endif
88

9+
#if __has_include(<Flutter/FlutterSceneLifeCycleDelegate.h>)
10+
@interface AppsflyerSdkPlugin: NSObject<FlutterPlugin, FlutterSceneLifeCycleDelegate>
11+
#else
912
@interface AppsflyerSdkPlugin: NSObject<FlutterPlugin>
13+
#endif
1014

1115
@property (readwrite, nonatomic) BOOL isManualStart;
1216

@@ -18,7 +22,7 @@
1822
@end
1923

2024
// Appsflyer JS objects
21-
#define kAppsFlyerPluginVersion @"6.17.8"
25+
#define kAppsFlyerPluginVersion @"6.17.9"
2226
#define afDevKey @"afDevKey"
2327
#define afAppId @"afAppId"
2428
#define afIsDebug @"isDebug"

ios/Classes/AppsflyerSdkPlugin.m

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
6868
[registrar addMethodCallDelegate:instance channel:channel];
6969
[registrar addMethodCallDelegate:instance channel:callbackChannel];
7070
[registrar addApplicationDelegate:instance];
71-
71+
#if __has_include(<Flutter/FlutterSceneLifeCycleDelegate.h>)
72+
if (@available(iOS 13.0, *)) {
73+
[registrar addSceneDelegate:instance];
74+
}
75+
#endif
7276

7377
}
7478

@@ -904,9 +908,20 @@ - (void)appDidBecomeActive {
904908

905909

906910
+ (FlutterViewController*) getViewController{
907-
UIViewController *topMostViewControllerObj = [[[UIApplication sharedApplication] delegate] window].rootViewController;
911+
UIWindow *window = nil;
912+
if (@available(iOS 13.0, *)) {
913+
for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) {
914+
if (scene.activationState == UISceneActivationStateForegroundActive) {
915+
window = scene.windows.firstObject;
916+
break;
917+
}
918+
}
919+
}
920+
if (window == nil) {
921+
window = [[[UIApplication sharedApplication] delegate] window];
922+
}
923+
UIViewController *topMostViewControllerObj = window.rootViewController;
908924
FlutterViewController *flutterViewController = (FlutterViewController *)topMostViewControllerObj;
909-
910925
return flutterViewController;
911926
}
912927

@@ -947,10 +962,52 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceAppl
947962
// Open Universal Links
948963
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
949964
[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler];
950-
965+
951966
// Results of this are ORed and NO doesn't affect other delegate interceptors' result.
952967
return NO;
953968
}
954969

970+
#if __has_include(<Flutter/FlutterSceneLifeCycleDelegate.h>)
971+
#pragma mark - FlutterSceneLifeCycleDelegate
972+
973+
// UIScene-based URI-scheme deep links (iOS 13+, Flutter 3.41+ UIScene migration)
974+
- (BOOL)scene:(UIScene*)scene openURLContexts:(NSSet<UIOpenURLContext*>*)URLContexts API_AVAILABLE(ios(13.0)) {
975+
for (UIOpenURLContext *context in URLContexts) {
976+
NSDictionary *opts = @{};
977+
if (context.options.sourceApplication) {
978+
opts = @{UIApplicationOpenURLOptionsSourceApplicationKey: context.options.sourceApplication};
979+
}
980+
[[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:opts];
981+
}
982+
return NO;
983+
}
984+
985+
// Cold-start deep links delivered via UISceneConnectionOptions (iOS 13+)
986+
// Handles both URI-scheme links (URLContexts) and Universal Links (userActivities)
987+
- (BOOL)scene:(UIScene*)scene
988+
willConnectToSession:(UISceneSession*)session
989+
options:(UISceneConnectionOptions*)connectionOptions API_AVAILABLE(ios(13.0)) {
990+
for (UIOpenURLContext *context in connectionOptions.URLContexts) {
991+
NSDictionary *opts = @{};
992+
if (context.options.sourceApplication) {
993+
opts = @{UIApplicationOpenURLOptionsSourceApplicationKey: context.options.sourceApplication};
994+
}
995+
[[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:opts];
996+
}
997+
for (NSUserActivity *activity in connectionOptions.userActivities) {
998+
if ([activity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
999+
[[AppsFlyerAttribution shared] continueUserActivity:activity restorationHandler:nil];
1000+
}
1001+
}
1002+
return NO;
1003+
}
1004+
1005+
// UIScene-based Universal Links (iOS 13+)
1006+
- (BOOL)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity API_AVAILABLE(ios(13.0)) {
1007+
[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:nil];
1008+
return NO;
1009+
}
1010+
#endif // __has_include(<Flutter/FlutterSceneLifeCycleDelegate.h>)
1011+
9551012

9561013
@end

0 commit comments

Comments
 (0)