Skip to content

Commit f963d40

Browse files
committed
fix(expo): support Swift AppDelegate without public
1 parent 10f70f3 commit f963d40

4 files changed

Lines changed: 194 additions & 32 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-native-app-auth": patch
3+
---
4+
5+
Fix Expo config plugin support for Swift AppDelegate templates that omit `public` before the AppDelegate class declaration.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { applyExpo53AppDelegatePatch } from '../ios/app-delegate';
2+
3+
const expo56AppDelegate = `internal import Expo
4+
import React
5+
import ReactAppDependencyProvider
6+
7+
@main
8+
class AppDelegate: ExpoAppDelegate {
9+
var window: UIWindow?
10+
11+
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
12+
var reactNativeFactory: RCTReactNativeFactory?
13+
14+
public override func application(
15+
_ app: UIApplication,
16+
open url: URL,
17+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
18+
) -> Bool {
19+
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
20+
}
21+
}`;
22+
23+
const publicAppDelegate = expo56AppDelegate.replace(
24+
'class AppDelegate: ExpoAppDelegate',
25+
'public class AppDelegate: ExpoAppDelegate'
26+
);
27+
28+
describe('applyExpo53AppDelegatePatch', () => {
29+
it('adds AppAuth conformance to Expo 56 Swift AppDelegate templates', () => {
30+
const result = applyExpo53AppDelegatePatch(expo56AppDelegate);
31+
32+
expect(result).toContain('class AppDelegate: ExpoAppDelegate, RNAppAuthAuthorizationFlowManager {');
33+
expect(result).toContain(
34+
'public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?'
35+
);
36+
expect(result).toContain('authorizationFlowManagerDelegate.resumeExternalUserAgentFlow(with: url)');
37+
});
38+
39+
it('preserves older public Swift AppDelegate templates', () => {
40+
const result = applyExpo53AppDelegatePatch(publicAppDelegate);
41+
42+
expect(result).toContain('public class AppDelegate: ExpoAppDelegate, RNAppAuthAuthorizationFlowManager {');
43+
});
44+
45+
it('preserves existing protocol conformances', () => {
46+
const result = applyExpo53AppDelegatePatch(
47+
expo56AppDelegate.replace(
48+
'class AppDelegate: ExpoAppDelegate',
49+
'class AppDelegate: ExpoAppDelegate, UIApplicationDelegate'
50+
)
51+
);
52+
53+
expect(result).toContain(
54+
'class AppDelegate: ExpoAppDelegate, UIApplicationDelegate, RNAppAuthAuthorizationFlowManager {'
55+
);
56+
});
57+
58+
it('does not duplicate an existing multiline AppAuth delegate property', () => {
59+
const appDelegateWithMultilineProperty = expo56AppDelegate.replace(
60+
' var reactNativeFactory: RCTReactNativeFactory?',
61+
` var reactNativeFactory: RCTReactNativeFactory?
62+
63+
public weak var authorizationFlowManagerDelegate:
64+
RNAppAuthAuthorizationFlowManagerDelegate?`
65+
);
66+
67+
const result = applyExpo53AppDelegatePatch(appDelegateWithMultilineProperty);
68+
69+
expect(result.match(/\bvar\s+authorizationFlowManagerDelegate\b/g)).toHaveLength(1);
70+
});
71+
72+
it('is idempotent', () => {
73+
const once = applyExpo53AppDelegatePatch(expo56AppDelegate);
74+
const twice = applyExpo53AppDelegatePatch(once);
75+
76+
expect(twice).toBe(once);
77+
});
78+
});
Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,63 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
14
interface ExpoConfig {
25
sdkVersion?: string;
6+
_internal?: {
7+
[key: string]: any;
8+
projectRoot?: string;
9+
};
310
}
411

5-
const EXPO_SDK_MAJOR_VERSION = 53;
12+
export const MIN_EXPO_SDK_MAJOR_VERSION = 53;
13+
14+
const parseMajorVersion = (version?: string): number | null => {
15+
if (!version) {
16+
return null;
17+
}
18+
19+
const match = version.match(/\d+/);
20+
if (!match) {
21+
return null;
22+
}
23+
24+
return Number.parseInt(match[0], 10);
25+
};
26+
27+
const readExpoPackageVersion = (projectRoot?: string): string | undefined => {
28+
if (!projectRoot) {
29+
return undefined;
30+
}
31+
32+
const packageJsonPath = path.join(projectRoot, 'package.json');
33+
if (!fs.existsSync(packageJsonPath)) {
34+
return undefined;
35+
}
36+
37+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
38+
return packageJson.dependencies?.expo || packageJson.devDependencies?.expo;
39+
};
40+
41+
export const getExpoSdkMajorVersion = (
42+
config: ExpoConfig,
43+
projectRoot = config._internal?.projectRoot
44+
): number | null => {
45+
return (
46+
parseMajorVersion(config.sdkVersion) ??
47+
parseMajorVersion(readExpoPackageVersion(projectRoot))
48+
);
49+
};
50+
51+
export const isExpo53OrLater = (config: ExpoConfig, projectRoot?: string): boolean => {
52+
const major = getExpoSdkMajorVersion(config, projectRoot);
53+
return major != null && major >= MIN_EXPO_SDK_MAJOR_VERSION;
54+
};
655

7-
export const isExpo53OrLater = (config: ExpoConfig): boolean => {
8-
const expoSdkVersion = config.sdkVersion || '0.0.0';
9-
const [major] = expoSdkVersion.split('.');
10-
return Number.parseInt(major, 10) >= EXPO_SDK_MAJOR_VERSION;
11-
};
56+
export const assertExpo53OrLater = (config: ExpoConfig, projectRoot?: string): void => {
57+
const major = getExpoSdkMajorVersion(config, projectRoot);
58+
if (major != null && major < MIN_EXPO_SDK_MAJOR_VERSION) {
59+
throw new Error(
60+
`react-native-app-auth config plugin requires Expo SDK ${MIN_EXPO_SDK_MAJOR_VERSION} or later. Detected Expo SDK ${major}.`
61+
);
62+
}
63+
};
Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,64 @@
11
import { withAppDelegate, ConfigPlugin } from '@expo/config-plugins';
2-
import { isExpo53OrLater } from '../expo-version';
2+
import { assertExpo53OrLater, isExpo53OrLater } from '../expo-version';
33

44
const codeModIOs = require('@expo/config-plugins/build/ios/codeMod');
55

6-
const withAppDelegateSwift: ConfigPlugin = rootConfig => {
7-
return withAppDelegate(rootConfig, config => {
8-
let { contents } = config.modResults;
9-
10-
if (!contents.includes('RNAppAuthAuthorizationFlowManager')) {
11-
const replaceText = 'class AppDelegate: ExpoAppDelegate';
12-
contents = contents.replace(replaceText, `${replaceText}, RNAppAuthAuthorizationFlowManager`);
13-
14-
const replaceText2 =
15-
'return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)';
16-
contents = contents.replace(
17-
replaceText2,
18-
`if let authorizationFlowManagerDelegate = self.authorizationFlowManagerDelegate {
6+
const APP_AUTH_PROTOCOL = 'RNAppAuthAuthorizationFlowManager';
7+
const APP_AUTH_DELEGATE_PROPERTY =
8+
'public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?';
9+
const APP_AUTH_DELEGATE_PROPERTY_PATTERN =
10+
/\bvar\s+authorizationFlowManagerDelegate\s*:\s*RNAppAuthAuthorizationFlowManagerDelegate\??/;
11+
const APP_AUTH_RESUME_BLOCK = `if let authorizationFlowManagerDelegate = self.authorizationFlowManagerDelegate {
1912
if authorizationFlowManagerDelegate.resumeExternalUserAgentFlow(with: url) {
20-
return true
13+
return true
14+
}
15+
}`;
16+
17+
export const applyExpo53AppDelegatePatch = (contents: string): string => {
18+
contents = contents.replace(
19+
/^(\s*(?:public\s+)?class\s+AppDelegate\s*:\s*ExpoAppDelegate)([^{]*)(\{)/m,
20+
(match, declaration, conformances, openingBrace) => {
21+
const trimmedConformances = conformances.trim();
22+
if (trimmedConformances.includes(APP_AUTH_PROTOCOL)) {
23+
return match;
2124
}
25+
26+
return `${declaration}${trimmedConformances}, ${APP_AUTH_PROTOCOL} ${openingBrace}`;
2227
}
23-
${replaceText2}`
24-
);
28+
);
2529

26-
const replaceText3 = 'var reactNativeFactory: RCTReactNativeFactory?';
30+
if (!APP_AUTH_DELEGATE_PROPERTY_PATTERN.test(contents)) {
31+
const reactNativeFactoryPattern =
32+
/^(\s*)(?:public\s+)?var\s+reactNativeFactory\s*:\s*RCTReactNativeFactory\?\s*$/m;
33+
const factoryMatch = contents.match(reactNativeFactoryPattern);
34+
if (factoryMatch) {
35+
const indent = factoryMatch[1];
2736
contents = contents.replace(
28-
replaceText3,
29-
`${replaceText3}\n\n public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?`
37+
reactNativeFactoryPattern,
38+
match => `${match}\n\n${indent}${APP_AUTH_DELEGATE_PROPERTY}`
3039
);
3140
}
41+
}
3242

33-
config.modResults.contents = contents;
43+
if (!contents.includes('resumeExternalUserAgentFlow(with: url)')) {
44+
contents = contents.replace(
45+
/((?:public\s+)?override\s+func\s+application\s*\([\s\S]*?open\s+url\s*:\s*URL[\s\S]*?\)\s*->\s*Bool\s*\{)/m,
46+
match => `${match}\n ${APP_AUTH_RESUME_BLOCK}\n`
47+
);
48+
}
49+
50+
return contents;
51+
};
52+
53+
const withAppDelegateSwift: ConfigPlugin = rootConfig => {
54+
return withAppDelegate(rootConfig, config => {
55+
assertExpo53OrLater(config, config.modRequest.projectRoot);
56+
config.modResults.contents = applyExpo53AppDelegatePatch(config.modResults.contents);
3457
return config;
3558
});
3659
};
3760

38-
export const withAppAuthAppDelegate: ConfigPlugin = rootConfig => {
39-
if (isExpo53OrLater(rootConfig)) {
40-
return withAppDelegateSwift(rootConfig);
41-
}
42-
61+
export const withLegacyAppAuthAppDelegate: ConfigPlugin = rootConfig => {
4362
return withAppDelegate(rootConfig, config => {
4463
let { contents } = config.modResults;
4564

@@ -59,3 +78,11 @@ export const withAppAuthAppDelegate: ConfigPlugin = rootConfig => {
5978
return config;
6079
});
6180
};
81+
82+
export const withAppAuthAppDelegate: ConfigPlugin = rootConfig => {
83+
if (isExpo53OrLater(rootConfig)) {
84+
return withAppDelegateSwift(rootConfig);
85+
}
86+
87+
return withLegacyAppAuthAppDelegate(rootConfig);
88+
};

0 commit comments

Comments
 (0)