Skip to content

Commit 96a9c49

Browse files
committed
fix(expo): support Swift AppDelegate without public
1 parent 24a2a4f commit 96a9c49

4 files changed

Lines changed: 172 additions & 30 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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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('is idempotent', () => {
59+
const once = applyExpo53AppDelegatePatch(expo56AppDelegate);
60+
const twice = applyExpo53AppDelegatePatch(once);
61+
62+
expect(twice).toBe(once);
63+
});
64+
});
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+
};

packages/react-native-app-auth/plugin/src/ios/app-delegate.ts

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,66 @@
11
import { withAppDelegate, ConfigPlugin } from '@expo/config-plugins';
2-
import { isExpo53OrLater } from '../expo-version';
2+
import { assertExpo53OrLater } 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_RESUME_BLOCK = `if let authorizationFlowManagerDelegate = self.authorizationFlowManagerDelegate {
1910
if authorizationFlowManagerDelegate.resumeExternalUserAgentFlow(with: url) {
20-
return true
11+
return true
2112
}
13+
}`;
14+
15+
export const applyExpo53AppDelegatePatch = (contents: string): string => {
16+
contents = contents.replace(
17+
/^(\s*(?:public\s+)?class\s+AppDelegate\s*:\s*ExpoAppDelegate)([^{]*)(\{)/m,
18+
(match, declaration, conformances, openingBrace) => {
19+
const trimmedConformances = conformances.trim();
20+
if (trimmedConformances.includes(APP_AUTH_PROTOCOL)) {
21+
return match;
22+
}
23+
24+
return `${declaration}${trimmedConformances}, ${APP_AUTH_PROTOCOL} ${openingBrace}`;
2225
}
23-
${replaceText2}`
24-
);
26+
);
2527

26-
const replaceText3 = 'var reactNativeFactory: RCTReactNativeFactory?';
28+
if (!contents.includes(APP_AUTH_DELEGATE_PROPERTY)) {
29+
const reactNativeFactoryPattern =
30+
/^(\s*)(?:public\s+)?var\s+reactNativeFactory\s*:\s*RCTReactNativeFactory\?\s*$/m;
31+
const factoryMatch = contents.match(reactNativeFactoryPattern);
32+
if (factoryMatch) {
33+
const indent = factoryMatch[1];
2734
contents = contents.replace(
28-
replaceText3,
29-
`${replaceText3}\n\n public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?`
35+
reactNativeFactoryPattern,
36+
match => `${match}\n\n${indent}${APP_AUTH_DELEGATE_PROPERTY}`
3037
);
3138
}
39+
}
3240

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

3859
export const withAppAuthAppDelegate: ConfigPlugin = rootConfig => {
39-
if (isExpo53OrLater(rootConfig)) {
40-
return withAppDelegateSwift(rootConfig);
41-
}
60+
return withAppDelegateSwift(rootConfig);
61+
};
4262

63+
export const withLegacyAppAuthAppDelegate: ConfigPlugin = rootConfig => {
4364
return withAppDelegate(rootConfig, config => {
4465
let { contents } = config.modResults;
4566

0 commit comments

Comments
 (0)