-
Notifications
You must be signed in to change notification settings - Fork 222
feat: add Swift AppDelegate support for expo #624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
al-af
merged 3 commits into
AppsFlyerSDK:development
from
saseungmin:feat/swift-appdelegate-support-expo
Jul 22, 2025
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,189 @@ | ||
| const { withDangerousMod, withAppDelegate, WarningAggregator } = require('@expo/config-plugins'); | ||
| const { withAppDelegate, withDangerousMod, withXcodeProject, WarningAggregator } = require('@expo/config-plugins'); | ||
| const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode'); | ||
| const { getAppDelegate } = require('@expo/config-plugins/build/ios/Paths'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| const RNAPPSFLYER_IMPORT = `#import <RNAppsFlyer.h>\n`; | ||
| const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER = `- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {`; | ||
| const RNAPPSFLYER_OPENURL_IDENTIFIER = `- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {`; | ||
| const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE = `[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler];\n`; | ||
| const RNAPPSFLYER_OPENURL_CODE = `[[AppsFlyerAttribution shared] handleOpenUrl:url options:options];\n`; | ||
|
|
||
| function modifyAppDelegate(appDelegate) { | ||
| if (!appDelegate.includes(RNAPPSFLYER_IMPORT)) { | ||
| appDelegate = RNAPPSFLYER_IMPORT + appDelegate; | ||
| } | ||
| if (appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE)) { | ||
| const block = RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER + '\n' + RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE; | ||
| appDelegate = appDelegate.replace(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER, block); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect continueUserActivity in AppDelegate or AppsFlyer's delegate method already exists"); | ||
| } | ||
| if (appDelegate.includes(RNAPPSFLYER_OPENURL_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_OPENURL_CODE)) { | ||
| const block = RNAPPSFLYER_OPENURL_IDENTIFIER + '\n' + RNAPPSFLYER_OPENURL_CODE; | ||
| appDelegate = appDelegate.replace(RNAPPSFLYER_OPENURL_IDENTIFIER, block); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect openURL in AppDelegate or AppsFlyer's delegate method already exists"); | ||
| } | ||
| return appDelegate; | ||
| function getBridgingHeaderPathFromXcode(project) { | ||
| const buildConfigs = project.pbxXCBuildConfigurationSection(); | ||
|
|
||
| for (const key in buildConfigs) { | ||
| const config = buildConfigs[key]; | ||
| if ( | ||
| typeof config === 'object' && | ||
| config.buildSettings && | ||
| config.buildSettings['SWIFT_OBJC_BRIDGING_HEADER'] | ||
| ) { | ||
| const bridgingHeaderPath = config.buildSettings[ | ||
| 'SWIFT_OBJC_BRIDGING_HEADER' | ||
| ].replace(/"/g, ''); | ||
|
|
||
| return bridgingHeaderPath; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function modifyObjcAppDelegate(appDelegate) { | ||
| const RNAPPSFLYER_IMPORT = `#import <RNAppsFlyer.h>\n`; | ||
| const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER = `- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {`; | ||
| const RNAPPSFLYER_OPENURL_IDENTIFIER = `- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {`; | ||
| const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE = `[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler];\n`; | ||
| const RNAPPSFLYER_OPENURL_CODE = `[[AppsFlyerAttribution shared] handleOpenUrl:url options:options];\n`; | ||
|
|
||
| if (!appDelegate.includes(RNAPPSFLYER_IMPORT)) { | ||
| appDelegate = RNAPPSFLYER_IMPORT + appDelegate; | ||
| } | ||
| if (appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE)) { | ||
| const block = RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER + '\n' + RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE; | ||
| appDelegate = appDelegate.replace(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER, block); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect continueUserActivity in AppDelegate or AppsFlyer's delegate method already exists"); | ||
| } | ||
| if (appDelegate.includes(RNAPPSFLYER_OPENURL_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_OPENURL_CODE)) { | ||
| const block = RNAPPSFLYER_OPENURL_IDENTIFIER + '\n' + RNAPPSFLYER_OPENURL_CODE; | ||
| appDelegate = appDelegate.replace(RNAPPSFLYER_OPENURL_IDENTIFIER, block); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect openURL in AppDelegate or AppsFlyer's delegate method already exists"); | ||
| } | ||
| return appDelegate; | ||
| } | ||
|
|
||
| function modifySwiftAppDelegate(appDelegateContents) { | ||
| const SWIFT_OPENURL_IDENTIFIER = ` public override func application( | ||
| _ app: UIApplication, | ||
| open url: URL, | ||
| options: [UIApplication.OpenURLOptionsKey: Any] = [:] | ||
| ) -> Bool {`; | ||
| const RNAPPSFLYER_SWIFT_OPENURL_CODE = 'AppsFlyerAttribution.shared().handleOpen(url, options: options)'; | ||
|
|
||
| const SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER = ` public override func application( | ||
| _ application: UIApplication, | ||
| continue userActivity: NSUserActivity, | ||
| restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void | ||
| ) -> Bool {`; | ||
| const RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE = 'AppsFlyerAttribution.shared().continue(userActivity, restorationHandler: nil)'; | ||
|
|
||
| if (appDelegateContents.includes(SWIFT_OPENURL_IDENTIFIER) && !appDelegateContents.includes(RNAPPSFLYER_SWIFT_OPENURL_CODE)) { | ||
| appDelegateContents = appDelegateContents.replace(SWIFT_OPENURL_IDENTIFIER, `${SWIFT_OPENURL_IDENTIFIER}\n ${RNAPPSFLYER_SWIFT_OPENURL_CODE}`); | ||
| } | ||
|
|
||
| if (appDelegateContents.includes(SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegateContents.includes(RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE)) { | ||
| appDelegateContents = appDelegateContents.replace(SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER, `${SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER}\n ${RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE}`); | ||
| } | ||
|
|
||
| if (!appDelegateContents.includes(RNAPPSFLYER_SWIFT_OPENURL_CODE) || !appDelegateContents.includes(RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE)) { | ||
| WarningAggregator.addWarningIOS( | ||
| 'withAppsFlyerAppDelegate', | ||
| ` | ||
| Automatic Swift AppDelegate modification failed. | ||
| Please add AppsFlyer integration manually: | ||
| 1. Add this to your openURL method: | ||
| AppsFlyerAttribution.shared().handleOpen(url, options: options) | ||
| 2. Add this to your continueUserActivity method: | ||
| AppsFlyerAttribution.shared().continue(userActivity, restorationHandler: nil) | ||
| Supported format: Expo SDK default template | ||
| ` | ||
| ); | ||
| } | ||
|
|
||
| return appDelegateContents; | ||
| } | ||
|
|
||
| function withAppsFlyerAppDelegate(config) { | ||
| return withAppDelegate(config, (config) => { | ||
| if (['objc', 'objcpp'].includes(config.modResults.language)) { | ||
| config.modResults.contents = modifyAppDelegate(config.modResults.contents); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', `${config.modResults.language} AppDelegate file is not supported yet`); | ||
| } | ||
| return config; | ||
| }); | ||
| return withAppDelegate(config, (config) => { | ||
| const language = config.modResults.language; | ||
|
|
||
| if (['objc', 'objcpp'].includes(language)) { | ||
| config.modResults.contents = modifyObjcAppDelegate(config.modResults.contents); | ||
| } else if (language === 'swift') { | ||
| config.modResults.contents = modifySwiftAppDelegate(config.modResults.contents); | ||
| } else { | ||
| WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', `${language} AppDelegate file is not supported yet`); | ||
| } | ||
| return config; | ||
| }); | ||
| } | ||
|
|
||
| const withIosBridgingHeader = (config) => { | ||
| return withXcodeProject(config, (action) => { | ||
| const projectRoot = action.modRequest.projectRoot; | ||
| const appDelegate = getAppDelegate(projectRoot); | ||
|
|
||
| if (appDelegate.language === 'swift') { | ||
| const bridgingHeaderPath = getBridgingHeaderPathFromXcode( | ||
| action.modResults, | ||
| ); | ||
|
|
||
| const bridgingHeaderFilePath = path.join( | ||
| action.modRequest.platformProjectRoot, | ||
| bridgingHeaderPath, | ||
| ); | ||
|
|
||
| if (fs.existsSync(bridgingHeaderFilePath)) { | ||
| let content = fs.readFileSync(bridgingHeaderFilePath, 'utf8'); | ||
| const appsFlyerImport = '#import <RNAppsFlyer.h>'; | ||
|
|
||
| if (!content.includes(appsFlyerImport)) { | ||
| content += `${appsFlyerImport}\n`; | ||
| fs.writeFileSync(bridgingHeaderFilePath, content); | ||
| } | ||
|
|
||
| return action; | ||
| } | ||
|
|
||
| WarningAggregator.addWarningIOS( | ||
| 'withIosBridgingHeader', | ||
| ` | ||
| Failed to detect ${bridgingHeaderPath} file. Please add AppsFlyer integration manually: | ||
| #import <RNAppsFlyer.h> | ||
| Supported format: Expo SDK default template | ||
| ` | ||
| ); | ||
|
|
||
| return action; | ||
| } | ||
|
|
||
| return action; | ||
| }); | ||
| }; | ||
|
|
||
| function withPodfile(config, shouldUseStrictMode) { | ||
| return withDangerousMod(config, [ | ||
| 'ios', | ||
| async (config) => { | ||
| const filePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); | ||
| const contents = fs.readFileSync(filePath, 'utf-8'); | ||
|
|
||
| const mergedPodfileWithStrictMode = mergeContents({ | ||
| tag: 'AppsFlyer Strict Mode', | ||
| src: contents, | ||
| newSrc: `$RNAppsFlyerStrictMode=${shouldUseStrictMode}`, | ||
| anchor: 'use_expo_modules!', | ||
| offset: 0, | ||
| comment: '#', | ||
| }); | ||
|
|
||
| if (!mergedPodfileWithStrictMode.didMerge) { | ||
| console.log("ERROR: Cannot add AppsFlyer strict mode to the project's ios/Podfile because it's malformed. Please report this with a copy of your project Podfile."); | ||
| return config; | ||
| } | ||
|
|
||
| fs.writeFileSync(filePath, mergedPodfileWithStrictMode.contents); | ||
|
|
||
| return config; | ||
| }, | ||
| ]); | ||
| return withDangerousMod(config, [ | ||
| 'ios', | ||
| async (config) => { | ||
| const filePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); | ||
| const contents = fs.readFileSync(filePath, 'utf-8'); | ||
|
|
||
| const mergedPodfileWithStrictMode = mergeContents({ | ||
| tag: 'AppsFlyer Strict Mode', | ||
| src: contents, | ||
| newSrc: `$RNAppsFlyerStrictMode=${shouldUseStrictMode}`, | ||
| anchor: 'use_expo_modules!', | ||
| offset: 0, | ||
| comment: '#', | ||
| }); | ||
|
|
||
| if (!mergedPodfileWithStrictMode.didMerge) { | ||
| console.log("ERROR: Cannot add AppsFlyer strict mode to the project's ios/Podfile because it's malformed. Please report this with a copy of your project Podfile."); | ||
| return config; | ||
| } | ||
|
|
||
| fs.writeFileSync(filePath, mergedPodfileWithStrictMode.contents); | ||
|
|
||
| return config; | ||
| }, | ||
| ]); | ||
| } | ||
|
|
||
| module.exports = function withAppsFlyerIos(config, shouldUseStrictMode) { | ||
| config = withPodfile(config, shouldUseStrictMode); | ||
| config = withAppsFlyerAppDelegate(config); | ||
| return config; | ||
| config = withPodfile(config, shouldUseStrictMode); | ||
| config = withIosBridgingHeader(config); | ||
| config = withAppsFlyerAppDelegate(config); | ||
| return config; | ||
| }; | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw your comment that
restorationHandleris not being used internally. I do think it is better to work with the external Api in case the internal one ever starts using it. You could work around the error you were getting with:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@itsramiel
Thanks for the suggestion! I think keeping
restorationHandler: nilis cleaner for now.Current state: Since AppsFlyer doesn't use
restorationHandlerinternally, we'd be passing unused data through an unnecessary closure.Type safety concerns: The suggested
array as? [UIUserActivityRestoring]casting assumes AppsFlyer will always provide the correct type, but we don't have guarantees about their internal implementation.Future updates: If AppsFlyer adds functionality that uses
restorationHandler, we'll need to update the library anyway, so we can implement it properly at that point.I think it's better to keep the code simple and explicit rather than adding complexity for future scenarios. When the feature is actually added, we can implement it then.
I'll follow the AppsFlyer maintainer's opinion on this matter for future implementation.
Thanks!