Skip to content

Commit 011ef3a

Browse files
authored
Merge pull request #624 from saseungmin/feat/swift-appdelegate-support-expo
feat: add Swift AppDelegate support for expo
2 parents 2e05d95 + 8115218 commit 011ef3a

File tree

1 file changed

+175
-60
lines changed

1 file changed

+175
-60
lines changed

expo/withAppsFlyerIos.js

Lines changed: 175 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,189 @@
1-
const { withDangerousMod, withAppDelegate, WarningAggregator } = require('@expo/config-plugins');
1+
const { withAppDelegate, withDangerousMod, withXcodeProject, WarningAggregator } = require('@expo/config-plugins');
22
const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');
3+
const { getAppDelegate } = require('@expo/config-plugins/build/ios/Paths');
34
const fs = require('fs');
45
const path = require('path');
56

6-
const RNAPPSFLYER_IMPORT = `#import <RNAppsFlyer.h>\n`;
7-
const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER = `- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {`;
8-
const RNAPPSFLYER_OPENURL_IDENTIFIER = `- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {`;
9-
const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE = `[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler];\n`;
10-
const RNAPPSFLYER_OPENURL_CODE = `[[AppsFlyerAttribution shared] handleOpenUrl:url options:options];\n`;
11-
12-
function modifyAppDelegate(appDelegate) {
13-
if (!appDelegate.includes(RNAPPSFLYER_IMPORT)) {
14-
appDelegate = RNAPPSFLYER_IMPORT + appDelegate;
15-
}
16-
if (appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE)) {
17-
const block = RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER + '\n' + RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE;
18-
appDelegate = appDelegate.replace(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER, block);
19-
} else {
20-
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect continueUserActivity in AppDelegate or AppsFlyer's delegate method already exists");
21-
}
22-
if (appDelegate.includes(RNAPPSFLYER_OPENURL_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_OPENURL_CODE)) {
23-
const block = RNAPPSFLYER_OPENURL_IDENTIFIER + '\n' + RNAPPSFLYER_OPENURL_CODE;
24-
appDelegate = appDelegate.replace(RNAPPSFLYER_OPENURL_IDENTIFIER, block);
25-
} else {
26-
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect openURL in AppDelegate or AppsFlyer's delegate method already exists");
27-
}
28-
return appDelegate;
7+
function getBridgingHeaderPathFromXcode(project) {
8+
const buildConfigs = project.pbxXCBuildConfigurationSection();
9+
10+
for (const key in buildConfigs) {
11+
const config = buildConfigs[key];
12+
if (
13+
typeof config === 'object' &&
14+
config.buildSettings &&
15+
config.buildSettings['SWIFT_OBJC_BRIDGING_HEADER']
16+
) {
17+
const bridgingHeaderPath = config.buildSettings[
18+
'SWIFT_OBJC_BRIDGING_HEADER'
19+
].replace(/"/g, '');
20+
21+
return bridgingHeaderPath;
22+
}
23+
}
24+
25+
return null;
26+
}
27+
28+
function modifyObjcAppDelegate(appDelegate) {
29+
const RNAPPSFLYER_IMPORT = `#import <RNAppsFlyer.h>\n`;
30+
const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER = `- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {`;
31+
const RNAPPSFLYER_OPENURL_IDENTIFIER = `- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {`;
32+
const RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE = `[[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler];\n`;
33+
const RNAPPSFLYER_OPENURL_CODE = `[[AppsFlyerAttribution shared] handleOpenUrl:url options:options];\n`;
34+
35+
if (!appDelegate.includes(RNAPPSFLYER_IMPORT)) {
36+
appDelegate = RNAPPSFLYER_IMPORT + appDelegate;
37+
}
38+
if (appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE)) {
39+
const block = RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER + '\n' + RNAPPSFLYER_CONTINUE_USER_ACTIVITY_CODE;
40+
appDelegate = appDelegate.replace(RNAPPSFLYER_CONTINUE_USER_ACTIVITY_IDENTIFIER, block);
41+
} else {
42+
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect continueUserActivity in AppDelegate or AppsFlyer's delegate method already exists");
43+
}
44+
if (appDelegate.includes(RNAPPSFLYER_OPENURL_IDENTIFIER) && !appDelegate.includes(RNAPPSFLYER_OPENURL_CODE)) {
45+
const block = RNAPPSFLYER_OPENURL_IDENTIFIER + '\n' + RNAPPSFLYER_OPENURL_CODE;
46+
appDelegate = appDelegate.replace(RNAPPSFLYER_OPENURL_IDENTIFIER, block);
47+
} else {
48+
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', "Failed to detect openURL in AppDelegate or AppsFlyer's delegate method already exists");
49+
}
50+
return appDelegate;
51+
}
52+
53+
function modifySwiftAppDelegate(appDelegateContents) {
54+
const SWIFT_OPENURL_IDENTIFIER = ` public override func application(
55+
_ app: UIApplication,
56+
open url: URL,
57+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
58+
) -> Bool {`;
59+
const RNAPPSFLYER_SWIFT_OPENURL_CODE = 'AppsFlyerAttribution.shared().handleOpen(url, options: options)';
60+
61+
const SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER = ` public override func application(
62+
_ application: UIApplication,
63+
continue userActivity: NSUserActivity,
64+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
65+
) -> Bool {`;
66+
const RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE = 'AppsFlyerAttribution.shared().continue(userActivity, restorationHandler: nil)';
67+
68+
if (appDelegateContents.includes(SWIFT_OPENURL_IDENTIFIER) && !appDelegateContents.includes(RNAPPSFLYER_SWIFT_OPENURL_CODE)) {
69+
appDelegateContents = appDelegateContents.replace(SWIFT_OPENURL_IDENTIFIER, `${SWIFT_OPENURL_IDENTIFIER}\n ${RNAPPSFLYER_SWIFT_OPENURL_CODE}`);
70+
}
71+
72+
if (appDelegateContents.includes(SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER) && !appDelegateContents.includes(RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE)) {
73+
appDelegateContents = appDelegateContents.replace(SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER, `${SWIFT_CONTINUE_USER_ACTIVITY_IDENTIFIER}\n ${RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE}`);
74+
}
75+
76+
if (!appDelegateContents.includes(RNAPPSFLYER_SWIFT_OPENURL_CODE) || !appDelegateContents.includes(RNAPPSFLYER_SWIFT_CONTINUE_USER_ACTIVITY_CODE)) {
77+
WarningAggregator.addWarningIOS(
78+
'withAppsFlyerAppDelegate',
79+
`
80+
Automatic Swift AppDelegate modification failed.
81+
Please add AppsFlyer integration manually:
82+
83+
1. Add this to your openURL method:
84+
AppsFlyerAttribution.shared().handleOpen(url, options: options)
85+
86+
2. Add this to your continueUserActivity method:
87+
AppsFlyerAttribution.shared().continue(userActivity, restorationHandler: nil)
88+
89+
Supported format: Expo SDK default template
90+
`
91+
);
92+
}
93+
94+
return appDelegateContents;
2995
}
3096

3197
function withAppsFlyerAppDelegate(config) {
32-
return withAppDelegate(config, (config) => {
33-
if (['objc', 'objcpp'].includes(config.modResults.language)) {
34-
config.modResults.contents = modifyAppDelegate(config.modResults.contents);
35-
} else {
36-
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', `${config.modResults.language} AppDelegate file is not supported yet`);
37-
}
38-
return config;
39-
});
98+
return withAppDelegate(config, (config) => {
99+
const language = config.modResults.language;
100+
101+
if (['objc', 'objcpp'].includes(language)) {
102+
config.modResults.contents = modifyObjcAppDelegate(config.modResults.contents);
103+
} else if (language === 'swift') {
104+
config.modResults.contents = modifySwiftAppDelegate(config.modResults.contents);
105+
} else {
106+
WarningAggregator.addWarningIOS('withAppsFlyerAppDelegate', `${language} AppDelegate file is not supported yet`);
107+
}
108+
return config;
109+
});
40110
}
41111

112+
const withIosBridgingHeader = (config) => {
113+
return withXcodeProject(config, (action) => {
114+
const projectRoot = action.modRequest.projectRoot;
115+
const appDelegate = getAppDelegate(projectRoot);
116+
117+
if (appDelegate.language === 'swift') {
118+
const bridgingHeaderPath = getBridgingHeaderPathFromXcode(
119+
action.modResults,
120+
);
121+
122+
const bridgingHeaderFilePath = path.join(
123+
action.modRequest.platformProjectRoot,
124+
bridgingHeaderPath,
125+
);
126+
127+
if (fs.existsSync(bridgingHeaderFilePath)) {
128+
let content = fs.readFileSync(bridgingHeaderFilePath, 'utf8');
129+
const appsFlyerImport = '#import <RNAppsFlyer.h>';
130+
131+
if (!content.includes(appsFlyerImport)) {
132+
content += `${appsFlyerImport}\n`;
133+
fs.writeFileSync(bridgingHeaderFilePath, content);
134+
}
135+
136+
return action;
137+
}
138+
139+
WarningAggregator.addWarningIOS(
140+
'withIosBridgingHeader',
141+
`
142+
Failed to detect ${bridgingHeaderPath} file. Please add AppsFlyer integration manually:
143+
#import <RNAppsFlyer.h>
144+
145+
Supported format: Expo SDK default template
146+
`
147+
);
148+
149+
return action;
150+
}
151+
152+
return action;
153+
});
154+
};
155+
42156
function withPodfile(config, shouldUseStrictMode) {
43-
return withDangerousMod(config, [
44-
'ios',
45-
async (config) => {
46-
const filePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
47-
const contents = fs.readFileSync(filePath, 'utf-8');
48-
49-
const mergedPodfileWithStrictMode = mergeContents({
50-
tag: 'AppsFlyer Strict Mode',
51-
src: contents,
52-
newSrc: `$RNAppsFlyerStrictMode=${shouldUseStrictMode}`,
53-
anchor: 'use_expo_modules!',
54-
offset: 0,
55-
comment: '#',
56-
});
57-
58-
if (!mergedPodfileWithStrictMode.didMerge) {
59-
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.");
60-
return config;
61-
}
62-
63-
fs.writeFileSync(filePath, mergedPodfileWithStrictMode.contents);
64-
65-
return config;
66-
},
67-
]);
157+
return withDangerousMod(config, [
158+
'ios',
159+
async (config) => {
160+
const filePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
161+
const contents = fs.readFileSync(filePath, 'utf-8');
162+
163+
const mergedPodfileWithStrictMode = mergeContents({
164+
tag: 'AppsFlyer Strict Mode',
165+
src: contents,
166+
newSrc: `$RNAppsFlyerStrictMode=${shouldUseStrictMode}`,
167+
anchor: 'use_expo_modules!',
168+
offset: 0,
169+
comment: '#',
170+
});
171+
172+
if (!mergedPodfileWithStrictMode.didMerge) {
173+
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.");
174+
return config;
175+
}
176+
177+
fs.writeFileSync(filePath, mergedPodfileWithStrictMode.contents);
178+
179+
return config;
180+
},
181+
]);
68182
}
69183

70184
module.exports = function withAppsFlyerIos(config, shouldUseStrictMode) {
71-
config = withPodfile(config, shouldUseStrictMode);
72-
config = withAppsFlyerAppDelegate(config);
73-
return config;
185+
config = withPodfile(config, shouldUseStrictMode);
186+
config = withIosBridgingHeader(config);
187+
config = withAppsFlyerAppDelegate(config);
188+
return config;
74189
};

0 commit comments

Comments
 (0)