Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 175 additions & 60 deletions expo/withAppsFlyerIos.js
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)';

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 restorationHandler is 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:

      AppsFlyerAttribution.shared().continue(userActivity) { array in
        restorationHandler(array as? [UIUserActivityRestoring])
      }

Copy link
Contributor Author

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: nil is cleaner for now.

  1. Current state: Since AppsFlyer doesn't use restorationHandler internally, we'd be passing unused data through an unnecessary closure.

  2. 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.

  3. 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!


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;
};