Skip to content

Commit 2524d70

Browse files
jeremypltmikehardy
authored andcommitted
feat(app-check): add Swift AppDelegate support for Expo SDK53+
1 parent b0535f4 commit 2524d70

File tree

3 files changed

+162
-10
lines changed

3 files changed

+162
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Expo
2+
import React
3+
4+
@UIApplicationMain
5+
class AppDelegate: ExpoAppDelegate {
6+
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
7+
// Initialize the factory
8+
let factory = ExpoReactNativeFactory(delegate: delegate)
9+
factory.startReactNative(withModuleName: "main", in: window, launchOptions: launchOptions)
10+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
11+
}
12+
}

packages/app-check/plugin/__tests__/iosPlugin.test.ts

+72-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals';
44
import fs from 'fs/promises';
55
import path from 'path';
66

7-
import { modifyAppDelegateAsync, modifyObjcAppDelegate } from '../src/ios/appDelegate';
7+
import {
8+
modifyAppDelegateAsync,
9+
modifyObjcAppDelegate,
10+
modifySwiftAppDelegate,
11+
} from '../src/ios/appDelegate';
812

913
describe('Config Plugin iOS Tests', function () {
1014
beforeEach(function () {
@@ -74,17 +78,52 @@ describe('Config Plugin iOS Tests', function () {
7478
);
7579
});
7680

77-
it("doesn't support Swift AppDelegate", async function () {
78-
jest.spyOn(fs, 'writeFile').mockImplementation(async () => {});
81+
it('supports Swift AppDelegate', async function () {
82+
// Use MockedFunction to properly type the mock
83+
const writeFileMock = jest
84+
.spyOn(fs, 'writeFile')
85+
.mockImplementation(async () => {}) as jest.MockedFunction<typeof fs.writeFile>;
86+
87+
const swiftContents = `import Expo
88+
import React
89+
90+
@UIApplicationMain
91+
class AppDelegate: ExpoAppDelegate {
92+
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
93+
// Initialize the factory
94+
let factory = ExpoReactNativeFactory(delegate: delegate)
95+
factory.startReactNative(withModuleName: "main", in: window, launchOptions: launchOptions)
96+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
97+
}
98+
}`;
7999

80100
const appDelegateFileInfo: AppDelegateProjectFile = {
81-
path: '.',
101+
path: '/app/ios/App/AppDelegate.swift',
82102
language: 'swift',
83-
contents: 'some dummy content',
103+
contents: swiftContents,
84104
};
85105

86-
await expect(modifyAppDelegateAsync(appDelegateFileInfo)).rejects.toThrow();
87-
expect(fs.writeFile).not.toHaveBeenCalled();
106+
await modifyAppDelegateAsync(appDelegateFileInfo);
107+
108+
// Check if writeFile was called
109+
expect(writeFileMock).toHaveBeenCalled();
110+
111+
// Get the modified content with explicit string type assertion
112+
const modifiedContents = writeFileMock.mock.calls[0][1] as string;
113+
114+
// Verify import was added
115+
expect(modifiedContents).toContain('import RNFBAppCheck');
116+
117+
// Verify initialization code was added
118+
expect(modifiedContents).toContain('RNFBAppCheckModule.sharedInstance()');
119+
120+
// Verify Firebase.configure() was added
121+
expect(modifiedContents).toContain('FirebaseApp.configure()');
122+
123+
// Verify the code was added before startReactNative (with explicit type assertion)
124+
const codeIndex = (modifiedContents as string).indexOf('RNFBAppCheckModule.sharedInstance()');
125+
const startReactNativeIndex = (modifiedContents as string).indexOf('factory.startReactNative');
126+
expect(codeIndex).toBeLessThan(startReactNativeIndex);
88127
});
89128

90129
it('does not add the firebase import multiple times', async function () {
@@ -104,4 +143,30 @@ describe('Config Plugin iOS Tests', function () {
104143
expect(twiceModifiedAppDelegate).toContain(singleImport);
105144
expect(twiceModifiedAppDelegate).not.toContain(doubleImport);
106145
});
146+
147+
it('does not add the swift import multiple times', async function () {
148+
const swiftContents = `import Expo
149+
import React
150+
151+
@UIApplicationMain
152+
class AppDelegate: ExpoAppDelegate {
153+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
154+
let factory = ExpoReactNativeFactory(delegate: delegate)
155+
factory.startReactNative(withModuleName: "main", in: window, launchOptions: launchOptions)
156+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
157+
}
158+
}`;
159+
160+
const onceModifiedContents = modifySwiftAppDelegate(swiftContents);
161+
expect(onceModifiedContents).toContain('import RNFBAppCheck');
162+
163+
// Count occurrences of the import
164+
const importCount = (onceModifiedContents.match(/import RNFBAppCheck/g) || []).length;
165+
expect(importCount).toBe(1);
166+
167+
// Modify a second time and ensure imports aren't duplicated
168+
const twiceModifiedContents = modifySwiftAppDelegate(onceModifiedContents);
169+
const secondImportCount = (twiceModifiedContents.match(/import RNFBAppCheck/g) || []).length;
170+
expect(secondImportCount).toBe(1);
171+
});
107172
});

packages/app-check/plugin/src/ios/appDelegate.ts

+78-3
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,91 @@ export function modifyObjcAppDelegate(contents: string): string {
6868
}
6969
}
7070

71+
export function modifySwiftAppDelegate(contents: string): string {
72+
// Add imports for Swift
73+
if (!contents.includes('import RNFBAppCheck')) {
74+
// Try to add after FirebaseCore if it exists
75+
if (contents.includes('import FirebaseCore')) {
76+
contents = contents.replace(
77+
/import FirebaseCore/g,
78+
`import FirebaseCore
79+
import RNFBAppCheck`,
80+
);
81+
} else {
82+
// Otherwise add after Expo
83+
contents = contents.replace(
84+
/import Expo/g,
85+
`import Expo
86+
import RNFBAppCheck`,
87+
);
88+
}
89+
}
90+
91+
// Check if App Check code is already added to avoid duplication
92+
if (contents.includes('RNFBAppCheckModule.sharedInstance()')) {
93+
return contents;
94+
}
95+
96+
// Find the Firebase initialization end line to insert after
97+
const firebaseLine = '// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions';
98+
99+
if (contents.includes(firebaseLine)) {
100+
// Insert right after Firebase initialization
101+
return contents.replace(
102+
firebaseLine,
103+
`${firebaseLine}
104+
FirebaseApp.configure()
105+
RNFBAppCheckModule.sharedInstance()
106+
`,
107+
);
108+
}
109+
110+
// If Firebase initialization block not found, add both Firebase and App Check initialization
111+
// This is to make sure Firebase is initialized before App Check
112+
const methodInvocationBlock = `FirebaseApp.configure()
113+
RNFBAppCheckModule.sharedInstance()`;
114+
115+
const methodInvocationLineMatcher = /(?:factory\.startReactNative\()/;
116+
117+
if (!methodInvocationLineMatcher.test(contents)) {
118+
WarningAggregator.addWarningIOS(
119+
'@react-native-firebase/app-check',
120+
'Unable to determine correct insertion point in AppDelegate.swift. Skipping App Check addition.',
121+
);
122+
return contents;
123+
}
124+
125+
try {
126+
return mergeContents({
127+
tag: '@react-native-firebase/app-check',
128+
src: contents,
129+
newSrc: methodInvocationBlock,
130+
anchor: methodInvocationLineMatcher,
131+
offset: 0,
132+
comment: '//',
133+
}).contents;
134+
} catch (e) {
135+
WarningAggregator.addWarningIOS(
136+
'@react-native-firebase/app-check',
137+
'Failed to insert App Check initialization code.',
138+
);
139+
return contents;
140+
}
141+
}
142+
71143
export async function modifyAppDelegateAsync(appDelegateFileInfo: AppDelegateProjectFile) {
72144
const { language, path, contents } = appDelegateFileInfo;
73145

146+
let newContents;
74147
if (['objc', 'objcpp'].includes(language)) {
75-
const newContents = modifyObjcAppDelegate(contents);
76-
await fs.promises.writeFile(path, newContents);
148+
newContents = modifyObjcAppDelegate(contents);
149+
} else if (language === 'swift') {
150+
newContents = modifySwiftAppDelegate(contents);
77151
} else {
78-
// TODO: Support Swift
79152
throw new Error(`Cannot add Firebase code to AppDelegate of language "${language}"`);
80153
}
154+
155+
await fs.promises.writeFile(path, newContents);
81156
}
82157

83158
export const withFirebaseAppDelegate: ConfigPlugin = config => {

0 commit comments

Comments
 (0)