@react-native-firebase/auth 26.4.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/RNFBAuth.podspec +8 -2
- package/android/build.gradle +3 -0
- package/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java +1 -3
- package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthCacheRegistry.kt +77 -0
- package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthListenerRegistry.kt +76 -0
- package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthCacheRegistryTest.java +12 -0
- package/dist/module/index.js +1 -2
- package/dist/module/index.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/lib/types/internal.d.ts +2 -3
- package/dist/typescript/lib/types/internal.d.ts.map +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/ios/RNFBAuth/RNFBAuthCacheRegistry.m +27 -8
- package/ios/RNFBAuth/RNFBAuthCacheRegistryStorage.swift +53 -0
- package/ios/RNFBAuth/RNFBAuthErrorCodeMapper.swift +333 -0
- package/ios/RNFBAuth/RNFBAuthHelper.m +24 -154
- package/ios/RNFBAuth/RNFBAuthListenerRegistry.m +26 -26
- package/ios/RNFBAuth/RNFBAuthListenerRegistryStorage.swift +76 -0
- package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.h +40 -0
- package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.m +49 -0
- package/ios/RNFBAuth.xcodeproj/project.pbxproj +22 -0
- package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryStorageTests.swift +72 -0
- package/ios/RNFBAuthUnitTests/RNFBAuthErrorCodeMapperTests.swift +250 -0
- package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryStorageTests.swift +122 -0
- package/ios/RNFBAuthUnitTests/RNFBAuthNullSentinelDecodeTests.m +78 -0
- package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/project.pbxproj +50 -0
- package/lib/index.ts +1 -1
- package/lib/types/internal.ts +2 -2
- package/lib/version.ts +1 -1
- package/package.json +4 -4
- package/plugin/build/index.js +4 -0
- package/plugin/build/ios/index.d.ts +2 -1
- package/plugin/build/ios/index.js +3 -1
- package/plugin/build/ios/openUrlFix.js +12 -0
- package/plugin/build/ios/sceneDelegate.d.ts +34 -0
- package/plugin/build/ios/sceneDelegate.js +127 -0
- package/plugin/build/ios/sceneLifecycle.d.ts +32 -0
- package/plugin/build/ios/sceneLifecycle.js +115 -0
- package/plugin/src/index.ts +9 -1
- package/plugin/src/ios/index.ts +2 -1
- package/plugin/src/ios/openUrlFix.ts +15 -0
- package/plugin/src/ios/sceneDelegate.ts +159 -0
- package/plugin/src/ios/sceneLifecycle.ts +134 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthCacheRegistry.java +0 -66
- package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthListenerRegistry.java +0 -76
package/lib/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '
|
|
2
|
+
export const version = '27.0.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-firebase/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "27.0.0",
|
|
4
4
|
"author": "Invertase <oss@invertase.io> (http://invertase.io)",
|
|
5
5
|
"description": "React Native Firebase - The authentication module provides an easy-to-use API to integrate an authentication workflow into new and existing applications. React Native Firebase provides access to all Firebase authentication methods and identity providers.",
|
|
6
6
|
"main": "./dist/module/index.js",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"@expo/plist": "^0.8.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@react-native-firebase/app": "
|
|
50
|
+
"@react-native-firebase/app": "27.0.0",
|
|
51
51
|
"expo": ">=47.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@react-native-firebase/app": "
|
|
54
|
+
"@react-native-firebase/app": "27.0.0",
|
|
55
55
|
"expo": "^55.0.18",
|
|
56
56
|
"react-native-builder-bob": "^0.40.13"
|
|
57
57
|
},
|
|
@@ -102,5 +102,5 @@
|
|
|
102
102
|
"node_modules/",
|
|
103
103
|
"dist/"
|
|
104
104
|
],
|
|
105
|
-
"gitHead": "
|
|
105
|
+
"gitHead": "0c7e942bca361f1765abaf233bf447d795718eff"
|
|
106
106
|
}
|
package/plugin/build/index.js
CHANGED
|
@@ -9,7 +9,11 @@ const withRnFirebaseAuth = (config, props) => {
|
|
|
9
9
|
return (0, config_plugins_1.withPlugins)(config, [
|
|
10
10
|
// iOS
|
|
11
11
|
[ios_1.withIosCaptchaUrlTypes, props],
|
|
12
|
+
// The two openURL fixes are mutually exclusive and detect which applies at prebuild time:
|
|
13
|
+
// app-delegate life cycle patches `AppDelegate.swift`, UIScene life cycle (Expo SDK 58+)
|
|
14
|
+
// patches `SceneDelegate.swift`.
|
|
12
15
|
[ios_1.withIosCaptchaOpenUrlFix, props],
|
|
16
|
+
[ios_1.withIosCaptchaSceneDelegateFix, props],
|
|
13
17
|
]);
|
|
14
18
|
};
|
|
15
19
|
const pak = require('@react-native-firebase/auth/package.json');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { withIosCaptchaUrlTypes } from './urlTypes';
|
|
2
2
|
import { withIosCaptchaOpenUrlFix } from './openUrlFix';
|
|
3
|
-
|
|
3
|
+
import { withIosCaptchaSceneDelegateFix } from './sceneDelegate';
|
|
4
|
+
export { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix, withIosCaptchaSceneDelegateFix };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withIosCaptchaOpenUrlFix = exports.withIosCaptchaUrlTypes = void 0;
|
|
3
|
+
exports.withIosCaptchaSceneDelegateFix = exports.withIosCaptchaOpenUrlFix = exports.withIosCaptchaUrlTypes = void 0;
|
|
4
4
|
const urlTypes_1 = require("./urlTypes");
|
|
5
5
|
Object.defineProperty(exports, "withIosCaptchaUrlTypes", { enumerable: true, get: function () { return urlTypes_1.withIosCaptchaUrlTypes; } });
|
|
6
6
|
const openUrlFix_1 = require("./openUrlFix");
|
|
7
7
|
Object.defineProperty(exports, "withIosCaptchaOpenUrlFix", { enumerable: true, get: function () { return openUrlFix_1.withIosCaptchaOpenUrlFix; } });
|
|
8
|
+
const sceneDelegate_1 = require("./sceneDelegate");
|
|
9
|
+
Object.defineProperty(exports, "withIosCaptchaSceneDelegateFix", { enumerable: true, get: function () { return sceneDelegate_1.withIosCaptchaSceneDelegateFix; } });
|
|
@@ -10,6 +10,7 @@ exports.isFirebaseSwizzlingDisabled = isFirebaseSwizzlingDisabled;
|
|
|
10
10
|
exports.ensureFirebaseSwizzlingIsEnabled = ensureFirebaseSwizzlingIsEnabled;
|
|
11
11
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
12
12
|
const generateCode_1 = require("@expo/config-plugins/build/utils/generateCode");
|
|
13
|
+
const sceneLifecycle_1 = require("./sceneLifecycle");
|
|
13
14
|
const withIosCaptchaOpenUrlFix = (config, props) => {
|
|
14
15
|
// check configuration
|
|
15
16
|
if (!shouldApplyIosOpenUrlFix({ config, props })) {
|
|
@@ -46,6 +47,17 @@ function withOpenUrlFixForAppDelegate({ config, props, }) {
|
|
|
46
47
|
const configValue = props?.ios?.captchaOpenUrlFix || 'default';
|
|
47
48
|
const newContents = modifyAppDelegate(contents, language);
|
|
48
49
|
if (newContents === null) {
|
|
50
|
+
// Under the UIScene life cycle (Xcode 27+) UIKit stops calling `application(_:open:options:)` and the
|
|
51
|
+
// template drops the method. `withIosCaptchaSceneDelegateFix` patches `SceneDelegate.swift` instead.
|
|
52
|
+
const platformProjectRoot = config.modRequest.platformProjectRoot;
|
|
53
|
+
if ((0, sceneLifecycle_1.findSceneDelegateFile)(platformProjectRoot) !== null) {
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
if ((0, sceneLifecycle_1.infoPlistDeclaresSceneManifest)(platformProjectRoot)) {
|
|
57
|
+
// Scene life cycle declared but no SceneDelegate.swift to patch: neither plugin can apply the fix.
|
|
58
|
+
config_plugins_1.WarningAggregator.addWarningIOS('@react-native-firebase/auth', 'Skipping iOS openURL fix because Info.plist declares UIApplicationSceneManifest but no SceneDelegate.swift was found. Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.');
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
49
61
|
if (configValue === true) {
|
|
50
62
|
throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found");
|
|
51
63
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
import { PluginConfigType } from '../pluginConfig';
|
|
3
|
+
export declare const generatedTag = "@react-native-firebase/auth-openURL";
|
|
4
|
+
/**
|
|
5
|
+
* Warm-path contract for the SceneDelegate reCAPTCHA patch.
|
|
6
|
+
*
|
|
7
|
+
* Firebase Auth presents reCAPTCHA in an in-app `SFSafariViewController` / auth session while the
|
|
8
|
+
* app is already running, then returns via a custom-scheme URL. Upstream documents and samples
|
|
9
|
+
* handle that return only in `scene(_:openURLContexts:)` (see Firebase phone-auth appendix and the
|
|
10
|
+
* Auth quickstart `SceneDelegate`); they do not route Auth reCAPTCHA through
|
|
11
|
+
* `scene(_:willConnectTo:options:)`. A cold start would also drop Auth's in-memory pending
|
|
12
|
+
* verification, so this plugin intentionally patches the warm path only.
|
|
13
|
+
*/
|
|
14
|
+
export declare const sceneDelegateWarmPathContract: {
|
|
15
|
+
readonly handlesOpenURLContexts: true;
|
|
16
|
+
readonly handlesWillConnectTo: false;
|
|
17
|
+
readonly evidence: readonly ["https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling", "https://github.com/firebase/quickstart-ios/blob/main/authentication/AuthenticationExample/SceneDelegate.swift"];
|
|
18
|
+
};
|
|
19
|
+
export declare const sceneDelegateFirebaseAuthImport = "import FirebaseAuth";
|
|
20
|
+
export declare const sceneDelegateOpenUrlBlock: string;
|
|
21
|
+
/**
|
|
22
|
+
* Add an `openURLContexts` override that lets Firebase Auth handle reCAPTCHA redirects, then
|
|
23
|
+
* forwards only unhandled contexts to `super` (Expo Router / RCTLinkingManager).
|
|
24
|
+
*
|
|
25
|
+
* Returns the modified contents, or null when the patch does not apply (already present, or the
|
|
26
|
+
* file is not a recognizable `SceneDelegate` subclass).
|
|
27
|
+
*/
|
|
28
|
+
export declare function modifySceneDelegate(contents: string): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Applies the reCAPTCHA `openURL` fix to `SceneDelegate.swift` on projects that use the UIScene
|
|
31
|
+
* life cycle. On projects that still use the app-delegate life cycle this is a no-op, the
|
|
32
|
+
* `withIosCaptchaOpenUrlFix` plugin handles those.
|
|
33
|
+
*/
|
|
34
|
+
export declare const withIosCaptchaSceneDelegateFix: ConfigPlugin<PluginConfigType>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.withIosCaptchaSceneDelegateFix = exports.sceneDelegateOpenUrlBlock = exports.sceneDelegateFirebaseAuthImport = exports.sceneDelegateWarmPathContract = exports.generatedTag = void 0;
|
|
7
|
+
exports.modifySceneDelegate = modifySceneDelegate;
|
|
8
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const openUrlFix_1 = require("./openUrlFix");
|
|
12
|
+
const sceneLifecycle_1 = require("./sceneLifecycle");
|
|
13
|
+
exports.generatedTag = '@react-native-firebase/auth-openURL';
|
|
14
|
+
/**
|
|
15
|
+
* Warm-path contract for the SceneDelegate reCAPTCHA patch.
|
|
16
|
+
*
|
|
17
|
+
* Firebase Auth presents reCAPTCHA in an in-app `SFSafariViewController` / auth session while the
|
|
18
|
+
* app is already running, then returns via a custom-scheme URL. Upstream documents and samples
|
|
19
|
+
* handle that return only in `scene(_:openURLContexts:)` (see Firebase phone-auth appendix and the
|
|
20
|
+
* Auth quickstart `SceneDelegate`); they do not route Auth reCAPTCHA through
|
|
21
|
+
* `scene(_:willConnectTo:options:)`. A cold start would also drop Auth's in-memory pending
|
|
22
|
+
* verification, so this plugin intentionally patches the warm path only.
|
|
23
|
+
*/
|
|
24
|
+
exports.sceneDelegateWarmPathContract = {
|
|
25
|
+
handlesOpenURLContexts: true,
|
|
26
|
+
handlesWillConnectTo: false,
|
|
27
|
+
evidence: [
|
|
28
|
+
'https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling',
|
|
29
|
+
'https://github.com/firebase/quickstart-ios/blob/main/authentication/AuthenticationExample/SceneDelegate.swift',
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
exports.sceneDelegateFirebaseAuthImport = 'import FirebaseAuth';
|
|
33
|
+
exports.sceneDelegateOpenUrlBlock = `\
|
|
34
|
+
// @generated begin ${exports.generatedTag} - expo prebuild (DO NOT MODIFY)
|
|
35
|
+
// Warm path only: Auth reCAPTCHA returns via openURLContexts while the app is running.
|
|
36
|
+
// Cold-start scene connection is intentionally not patched (see sceneDelegateWarmPathContract).
|
|
37
|
+
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
38
|
+
// Let Firebase Auth handle owned URLs (reCAPTCHA); forward the rest to Expo / RCTLinkingManager.
|
|
39
|
+
let forwardedContexts = Set(URLContexts.filter { !Auth.auth().canHandle($0.url) })
|
|
40
|
+
guard !forwardedContexts.isEmpty else {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
super.scene(scene, openURLContexts: forwardedContexts)
|
|
44
|
+
}
|
|
45
|
+
// @generated end ${exports.generatedTag}\
|
|
46
|
+
`;
|
|
47
|
+
// Matches the opening brace of a `SceneDelegate` class declaration so the override can be inserted
|
|
48
|
+
// as the first member.
|
|
49
|
+
const sceneDelegateClassMatcher = /class\s+SceneDelegate\s*:[^\n{]*\{\n/;
|
|
50
|
+
function ensureFirebaseAuthImport(contents) {
|
|
51
|
+
if (/^\s*import\s+FirebaseAuth\s*$/m.test(contents)) {
|
|
52
|
+
return contents;
|
|
53
|
+
}
|
|
54
|
+
const newline = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
55
|
+
const expoImport = /^[ \t]*(?:internal\s+)?import\s+Expo[ \t]*$/m.exec(contents);
|
|
56
|
+
if (expoImport?.index !== undefined) {
|
|
57
|
+
const insertionPoint = expoImport.index + expoImport[0].length;
|
|
58
|
+
return `${contents.slice(0, insertionPoint)}${newline}${exports.sceneDelegateFirebaseAuthImport}${contents.slice(insertionPoint)}`;
|
|
59
|
+
}
|
|
60
|
+
const firstImport = /^[ \t]*import\b[^\r\n]*$/m.exec(contents);
|
|
61
|
+
if (firstImport?.index !== undefined) {
|
|
62
|
+
return `${contents.slice(0, firstImport.index)}${exports.sceneDelegateFirebaseAuthImport}${newline}${contents.slice(firstImport.index)}`;
|
|
63
|
+
}
|
|
64
|
+
return `${exports.sceneDelegateFirebaseAuthImport}${newline}${contents}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add an `openURLContexts` override that lets Firebase Auth handle reCAPTCHA redirects, then
|
|
68
|
+
* forwards only unhandled contexts to `super` (Expo Router / RCTLinkingManager).
|
|
69
|
+
*
|
|
70
|
+
* Returns the modified contents, or null when the patch does not apply (already present, or the
|
|
71
|
+
* file is not a recognizable `SceneDelegate` subclass).
|
|
72
|
+
*/
|
|
73
|
+
function modifySceneDelegate(contents) {
|
|
74
|
+
if (contents.includes(exports.generatedTag)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const match = contents.match(sceneDelegateClassMatcher);
|
|
78
|
+
if (!match || match.index === undefined) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const withImport = ensureFirebaseAuthImport(contents);
|
|
82
|
+
// Re-match after import insertion — the class body offset can shift.
|
|
83
|
+
const rematch = withImport.match(sceneDelegateClassMatcher);
|
|
84
|
+
if (!rematch || rematch.index === undefined) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const insertionPoint = rematch.index + rematch[0].length;
|
|
88
|
+
return `${withImport.slice(0, insertionPoint)}${exports.sceneDelegateOpenUrlBlock}\n${withImport.slice(insertionPoint)}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Applies the reCAPTCHA `openURL` fix to `SceneDelegate.swift` on projects that use the UIScene
|
|
92
|
+
* life cycle. On projects that still use the app-delegate life cycle this is a no-op, the
|
|
93
|
+
* `withIosCaptchaOpenUrlFix` plugin handles those.
|
|
94
|
+
*/
|
|
95
|
+
const withIosCaptchaSceneDelegateFix = (config, props) => {
|
|
96
|
+
if (!(0, openUrlFix_1.shouldApplyIosOpenUrlFix)({ config, props })) {
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
return (0, config_plugins_1.withDangerousMod)(config, [
|
|
100
|
+
'ios',
|
|
101
|
+
async (config) => {
|
|
102
|
+
const resolution = (0, sceneLifecycle_1.resolveSceneDelegateFile)(config.modRequest.platformProjectRoot);
|
|
103
|
+
if (resolution.kind === 'missing') {
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
if (resolution.kind === 'ambiguous') {
|
|
107
|
+
config_plugins_1.WarningAggregator.addWarningIOS('@react-native-firebase/auth', `Skipping iOS openURL fix because multiple SceneDelegate.swift files were found and none is uniquely co-located with AppDelegate (${resolution.candidates
|
|
108
|
+
.map(candidate => path_1.default.relative(config.modRequest.platformProjectRoot, candidate))
|
|
109
|
+
.join(', ')}). Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.`);
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
112
|
+
const sceneDelegatePath = resolution.path;
|
|
113
|
+
const contents = await fs_1.default.promises.readFile(sceneDelegatePath, 'utf-8');
|
|
114
|
+
const newContents = modifySceneDelegate(contents);
|
|
115
|
+
if (newContents === null) {
|
|
116
|
+
// Already patched is the common case and needs no warning; an unrecognized shape does.
|
|
117
|
+
if (!contents.includes(exports.generatedTag)) {
|
|
118
|
+
config_plugins_1.WarningAggregator.addWarningIOS('@react-native-firebase/auth', `Skipping iOS openURL fix because ${path_1.default.basename(sceneDelegatePath)} is not a recognizable SceneDelegate subclass. Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.`);
|
|
119
|
+
}
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
await fs_1.default.promises.writeFile(sceneDelegatePath, newContents);
|
|
123
|
+
return config;
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
};
|
|
127
|
+
exports.withIosCaptchaSceneDelegateFix = withIosCaptchaSceneDelegateFix;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SceneDelegateResolution = {
|
|
2
|
+
kind: 'found';
|
|
3
|
+
path: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'missing';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'ambiguous';
|
|
8
|
+
candidates: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the application target's `SceneDelegate.swift` deterministically.
|
|
12
|
+
*
|
|
13
|
+
* - Zero candidates → missing.
|
|
14
|
+
* - Exactly one candidate → that file (Expo nested layout or bare-expo `ios/` root).
|
|
15
|
+
* - Multiple candidates → prefer the one co-located with an `AppDelegate.*`; if that is not
|
|
16
|
+
* unique, reject as ambiguous rather than patching a distractor (extension / secondary target).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveSceneDelegateFile(platformProjectRoot: string): SceneDelegateResolution;
|
|
19
|
+
/**
|
|
20
|
+
* Locate the app's `SceneDelegate.swift`, or `null` when none exists or resolution is ambiguous.
|
|
21
|
+
*
|
|
22
|
+
* Expo SDK 58 writes it next to `AppDelegate.swift` (`ios/<ProjectName>/SceneDelegate.swift`), but
|
|
23
|
+
* `apps/bare-expo` keeps it at `ios/SceneDelegate.swift`, so search rather than assume a layout.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findSceneDelegateFile(platformProjectRoot: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Detect a `UIApplicationSceneManifest` entry in the app's `Info.plist`.
|
|
28
|
+
*
|
|
29
|
+
* Independent of `findSceneDelegateFile`: a project can declare the scene life cycle without shipping
|
|
30
|
+
* a `SceneDelegate.swift` the plugin can patch, and callers need to tell the two cases apart.
|
|
31
|
+
*/
|
|
32
|
+
export declare function infoPlistDeclaresSceneManifest(platformProjectRoot: string): boolean;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveSceneDelegateFile = resolveSceneDelegateFile;
|
|
7
|
+
exports.findSceneDelegateFile = findSceneDelegateFile;
|
|
8
|
+
exports.infoPlistDeclaresSceneManifest = infoPlistDeclaresSceneManifest;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
// Directories that never contain the app's own scene delegate but are expensive to walk.
|
|
12
|
+
const IGNORED_DIRECTORIES = new Set(['Pods', 'build', 'node_modules', 'DerivedData', '.git']);
|
|
13
|
+
const APP_DELEGATE_NAMES = new Set(['AppDelegate.swift', 'AppDelegate.m', 'AppDelegate.mm']);
|
|
14
|
+
/**
|
|
15
|
+
* Walk `platformProjectRoot` (typically `ios/`) and collect matching file paths, skipping
|
|
16
|
+
* CocoaPods / build / Xcode bundle trees.
|
|
17
|
+
*/
|
|
18
|
+
function collectFilesNamed(platformProjectRoot, fileNamePredicate) {
|
|
19
|
+
if (!platformProjectRoot || !fs_1.default.existsSync(platformProjectRoot)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const matches = [];
|
|
23
|
+
const queue = [platformProjectRoot];
|
|
24
|
+
while (queue.length > 0) {
|
|
25
|
+
const directory = queue.shift();
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = fs_1.default.readdirSync(directory, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const entryPath = path_1.default.join(directory, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
if (!IGNORED_DIRECTORIES.has(entry.name) &&
|
|
37
|
+
!entry.name.endsWith('.xcodeproj') &&
|
|
38
|
+
!entry.name.endsWith('.xcworkspace')) {
|
|
39
|
+
queue.push(entryPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (fileNamePredicate(entry.name)) {
|
|
43
|
+
matches.push(entryPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return matches.sort();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the application target's `SceneDelegate.swift` deterministically.
|
|
51
|
+
*
|
|
52
|
+
* - Zero candidates → missing.
|
|
53
|
+
* - Exactly one candidate → that file (Expo nested layout or bare-expo `ios/` root).
|
|
54
|
+
* - Multiple candidates → prefer the one co-located with an `AppDelegate.*`; if that is not
|
|
55
|
+
* unique, reject as ambiguous rather than patching a distractor (extension / secondary target).
|
|
56
|
+
*/
|
|
57
|
+
function resolveSceneDelegateFile(platformProjectRoot) {
|
|
58
|
+
const candidates = collectFilesNamed(platformProjectRoot, name => name === 'SceneDelegate.swift');
|
|
59
|
+
if (candidates.length === 0) {
|
|
60
|
+
return { kind: 'missing' };
|
|
61
|
+
}
|
|
62
|
+
if (candidates.length === 1) {
|
|
63
|
+
return { kind: 'found', path: candidates[0] };
|
|
64
|
+
}
|
|
65
|
+
const appDelegateDirs = new Set(collectFilesNamed(platformProjectRoot, name => APP_DELEGATE_NAMES.has(name)).map(p => path_1.default.dirname(p)));
|
|
66
|
+
const coLocated = candidates.filter(candidate => appDelegateDirs.has(path_1.default.dirname(candidate)));
|
|
67
|
+
if (coLocated.length === 1) {
|
|
68
|
+
return { kind: 'found', path: coLocated[0] };
|
|
69
|
+
}
|
|
70
|
+
return { kind: 'ambiguous', candidates };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Locate the app's `SceneDelegate.swift`, or `null` when none exists or resolution is ambiguous.
|
|
74
|
+
*
|
|
75
|
+
* Expo SDK 58 writes it next to `AppDelegate.swift` (`ios/<ProjectName>/SceneDelegate.swift`), but
|
|
76
|
+
* `apps/bare-expo` keeps it at `ios/SceneDelegate.swift`, so search rather than assume a layout.
|
|
77
|
+
*/
|
|
78
|
+
function findSceneDelegateFile(platformProjectRoot) {
|
|
79
|
+
const resolution = resolveSceneDelegateFile(platformProjectRoot);
|
|
80
|
+
return resolution.kind === 'found' ? resolution.path : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Detect a `UIApplicationSceneManifest` entry in the app's `Info.plist`.
|
|
84
|
+
*
|
|
85
|
+
* Independent of `findSceneDelegateFile`: a project can declare the scene life cycle without shipping
|
|
86
|
+
* a `SceneDelegate.swift` the plugin can patch, and callers need to tell the two cases apart.
|
|
87
|
+
*/
|
|
88
|
+
function infoPlistDeclaresSceneManifest(platformProjectRoot) {
|
|
89
|
+
if (!platformProjectRoot) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs_1.default.readdirSync(platformProjectRoot, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isDirectory() || IGNORED_DIRECTORIES.has(entry.name)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const infoPlist = path_1.default.join(platformProjectRoot, entry.name, 'Info.plist');
|
|
104
|
+
try {
|
|
105
|
+
if (fs_1.default.existsSync(infoPlist) &&
|
|
106
|
+
fs_1.default.readFileSync(infoPlist, 'utf-8').includes('UIApplicationSceneManifest')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
package/plugin/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
withIosCaptchaUrlTypes,
|
|
5
|
+
withIosCaptchaOpenUrlFix,
|
|
6
|
+
withIosCaptchaSceneDelegateFix,
|
|
7
|
+
} from './ios';
|
|
4
8
|
import { PluginConfigType } from './pluginConfig';
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -10,7 +14,11 @@ const withRnFirebaseAuth: ConfigPlugin<PluginConfigType> = (config, props) => {
|
|
|
10
14
|
return withPlugins(config, [
|
|
11
15
|
// iOS
|
|
12
16
|
[withIosCaptchaUrlTypes, props],
|
|
17
|
+
// The two openURL fixes are mutually exclusive and detect which applies at prebuild time:
|
|
18
|
+
// app-delegate life cycle patches `AppDelegate.swift`, UIScene life cycle (Expo SDK 58+)
|
|
19
|
+
// patches `SceneDelegate.swift`.
|
|
13
20
|
[withIosCaptchaOpenUrlFix, props],
|
|
21
|
+
[withIosCaptchaSceneDelegateFix, props],
|
|
14
22
|
]);
|
|
15
23
|
};
|
|
16
24
|
|
package/plugin/src/ios/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withIosCaptchaUrlTypes } from './urlTypes';
|
|
2
2
|
import { withIosCaptchaOpenUrlFix } from './openUrlFix';
|
|
3
|
+
import { withIosCaptchaSceneDelegateFix } from './sceneDelegate';
|
|
3
4
|
|
|
4
|
-
export { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix };
|
|
5
|
+
export { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix, withIosCaptchaSceneDelegateFix };
|
|
@@ -10,6 +10,7 @@ import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Path
|
|
|
10
10
|
import type { InfoPlist } from '@expo/config-plugins/build/ios/IosConfig.types';
|
|
11
11
|
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
|
|
12
12
|
import { PluginConfigType } from '../pluginConfig';
|
|
13
|
+
import { findSceneDelegateFile, infoPlistDeclaresSceneManifest } from './sceneLifecycle';
|
|
13
14
|
|
|
14
15
|
export const withIosCaptchaOpenUrlFix: ConfigPlugin<PluginConfigType> = (
|
|
15
16
|
config: ExpoConfig,
|
|
@@ -64,6 +65,20 @@ export function withOpenUrlFixForAppDelegate({
|
|
|
64
65
|
|
|
65
66
|
const newContents = modifyAppDelegate(contents, language);
|
|
66
67
|
if (newContents === null) {
|
|
68
|
+
// Under the UIScene life cycle (Xcode 27+) UIKit stops calling `application(_:open:options:)` and the
|
|
69
|
+
// template drops the method. `withIosCaptchaSceneDelegateFix` patches `SceneDelegate.swift` instead.
|
|
70
|
+
const platformProjectRoot = config.modRequest.platformProjectRoot;
|
|
71
|
+
if (findSceneDelegateFile(platformProjectRoot) !== null) {
|
|
72
|
+
return config;
|
|
73
|
+
}
|
|
74
|
+
if (infoPlistDeclaresSceneManifest(platformProjectRoot)) {
|
|
75
|
+
// Scene life cycle declared but no SceneDelegate.swift to patch: neither plugin can apply the fix.
|
|
76
|
+
WarningAggregator.addWarningIOS(
|
|
77
|
+
'@react-native-firebase/auth',
|
|
78
|
+
'Skipping iOS openURL fix because Info.plist declares UIApplicationSceneManifest but no SceneDelegate.swift was found. Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.',
|
|
79
|
+
);
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
67
82
|
if (configValue === true) {
|
|
68
83
|
throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found");
|
|
69
84
|
} else {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ConfigPlugin, WarningAggregator, withDangerousMod } from '@expo/config-plugins';
|
|
2
|
+
import type { ExpoConfig } from '@expo/config/build/Config.types';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import { PluginConfigType } from '../pluginConfig';
|
|
7
|
+
import { shouldApplyIosOpenUrlFix } from './openUrlFix';
|
|
8
|
+
import { resolveSceneDelegateFile } from './sceneLifecycle';
|
|
9
|
+
|
|
10
|
+
export const generatedTag = '@react-native-firebase/auth-openURL';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Warm-path contract for the SceneDelegate reCAPTCHA patch.
|
|
14
|
+
*
|
|
15
|
+
* Firebase Auth presents reCAPTCHA in an in-app `SFSafariViewController` / auth session while the
|
|
16
|
+
* app is already running, then returns via a custom-scheme URL. Upstream documents and samples
|
|
17
|
+
* handle that return only in `scene(_:openURLContexts:)` (see Firebase phone-auth appendix and the
|
|
18
|
+
* Auth quickstart `SceneDelegate`); they do not route Auth reCAPTCHA through
|
|
19
|
+
* `scene(_:willConnectTo:options:)`. A cold start would also drop Auth's in-memory pending
|
|
20
|
+
* verification, so this plugin intentionally patches the warm path only.
|
|
21
|
+
*/
|
|
22
|
+
export const sceneDelegateWarmPathContract = {
|
|
23
|
+
handlesOpenURLContexts: true,
|
|
24
|
+
handlesWillConnectTo: false,
|
|
25
|
+
evidence: [
|
|
26
|
+
'https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling',
|
|
27
|
+
'https://github.com/firebase/quickstart-ios/blob/main/authentication/AuthenticationExample/SceneDelegate.swift',
|
|
28
|
+
],
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const sceneDelegateFirebaseAuthImport = 'import FirebaseAuth';
|
|
32
|
+
|
|
33
|
+
export const sceneDelegateOpenUrlBlock: string = `\
|
|
34
|
+
// @generated begin ${generatedTag} - expo prebuild (DO NOT MODIFY)
|
|
35
|
+
// Warm path only: Auth reCAPTCHA returns via openURLContexts while the app is running.
|
|
36
|
+
// Cold-start scene connection is intentionally not patched (see sceneDelegateWarmPathContract).
|
|
37
|
+
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
38
|
+
// Let Firebase Auth handle owned URLs (reCAPTCHA); forward the rest to Expo / RCTLinkingManager.
|
|
39
|
+
let forwardedContexts = Set(URLContexts.filter { !Auth.auth().canHandle($0.url) })
|
|
40
|
+
guard !forwardedContexts.isEmpty else {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
super.scene(scene, openURLContexts: forwardedContexts)
|
|
44
|
+
}
|
|
45
|
+
// @generated end ${generatedTag}\
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// Matches the opening brace of a `SceneDelegate` class declaration so the override can be inserted
|
|
49
|
+
// as the first member.
|
|
50
|
+
const sceneDelegateClassMatcher = /class\s+SceneDelegate\s*:[^\n{]*\{\n/;
|
|
51
|
+
|
|
52
|
+
function ensureFirebaseAuthImport(contents: string): string {
|
|
53
|
+
if (/^\s*import\s+FirebaseAuth\s*$/m.test(contents)) {
|
|
54
|
+
return contents;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const newline = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
58
|
+
const expoImport = /^[ \t]*(?:internal\s+)?import\s+Expo[ \t]*$/m.exec(contents);
|
|
59
|
+
if (expoImport?.index !== undefined) {
|
|
60
|
+
const insertionPoint = expoImport.index + expoImport[0].length;
|
|
61
|
+
return `${contents.slice(0, insertionPoint)}${newline}${sceneDelegateFirebaseAuthImport}${contents.slice(
|
|
62
|
+
insertionPoint,
|
|
63
|
+
)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const firstImport = /^[ \t]*import\b[^\r\n]*$/m.exec(contents);
|
|
67
|
+
if (firstImport?.index !== undefined) {
|
|
68
|
+
return `${contents.slice(0, firstImport.index)}${sceneDelegateFirebaseAuthImport}${newline}${contents.slice(
|
|
69
|
+
firstImport.index,
|
|
70
|
+
)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `${sceneDelegateFirebaseAuthImport}${newline}${contents}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add an `openURLContexts` override that lets Firebase Auth handle reCAPTCHA redirects, then
|
|
78
|
+
* forwards only unhandled contexts to `super` (Expo Router / RCTLinkingManager).
|
|
79
|
+
*
|
|
80
|
+
* Returns the modified contents, or null when the patch does not apply (already present, or the
|
|
81
|
+
* file is not a recognizable `SceneDelegate` subclass).
|
|
82
|
+
*/
|
|
83
|
+
export function modifySceneDelegate(contents: string): string | null {
|
|
84
|
+
if (contents.includes(generatedTag)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const match = contents.match(sceneDelegateClassMatcher);
|
|
89
|
+
if (!match || match.index === undefined) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const withImport = ensureFirebaseAuthImport(contents);
|
|
94
|
+
// Re-match after import insertion — the class body offset can shift.
|
|
95
|
+
const rematch = withImport.match(sceneDelegateClassMatcher);
|
|
96
|
+
if (!rematch || rematch.index === undefined) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const insertionPoint = rematch.index + rematch[0].length;
|
|
101
|
+
return `${withImport.slice(0, insertionPoint)}${sceneDelegateOpenUrlBlock}\n${withImport.slice(
|
|
102
|
+
insertionPoint,
|
|
103
|
+
)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Applies the reCAPTCHA `openURL` fix to `SceneDelegate.swift` on projects that use the UIScene
|
|
108
|
+
* life cycle. On projects that still use the app-delegate life cycle this is a no-op, the
|
|
109
|
+
* `withIosCaptchaOpenUrlFix` plugin handles those.
|
|
110
|
+
*/
|
|
111
|
+
export const withIosCaptchaSceneDelegateFix: ConfigPlugin<PluginConfigType> = (
|
|
112
|
+
config: ExpoConfig,
|
|
113
|
+
props?: PluginConfigType,
|
|
114
|
+
) => {
|
|
115
|
+
if (!shouldApplyIosOpenUrlFix({ config, props })) {
|
|
116
|
+
return config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return withDangerousMod(config, [
|
|
120
|
+
'ios',
|
|
121
|
+
async config => {
|
|
122
|
+
const resolution = resolveSceneDelegateFile(config.modRequest.platformProjectRoot);
|
|
123
|
+
|
|
124
|
+
if (resolution.kind === 'missing') {
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (resolution.kind === 'ambiguous') {
|
|
129
|
+
WarningAggregator.addWarningIOS(
|
|
130
|
+
'@react-native-firebase/auth',
|
|
131
|
+
`Skipping iOS openURL fix because multiple SceneDelegate.swift files were found and none is uniquely co-located with AppDelegate (${resolution.candidates
|
|
132
|
+
.map(candidate => path.relative(config.modRequest.platformProjectRoot, candidate))
|
|
133
|
+
.join(', ')}). Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.`,
|
|
134
|
+
);
|
|
135
|
+
return config;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sceneDelegatePath = resolution.path;
|
|
139
|
+
const contents = await fs.promises.readFile(sceneDelegatePath, 'utf-8');
|
|
140
|
+
const newContents = modifySceneDelegate(contents);
|
|
141
|
+
|
|
142
|
+
if (newContents === null) {
|
|
143
|
+
// Already patched is the common case and needs no warning; an unrecognized shape does.
|
|
144
|
+
if (!contents.includes(generatedTag)) {
|
|
145
|
+
WarningAggregator.addWarningIOS(
|
|
146
|
+
'@react-native-firebase/auth',
|
|
147
|
+
`Skipping iOS openURL fix because ${path.basename(
|
|
148
|
+
sceneDelegatePath,
|
|
149
|
+
)} is not a recognizable SceneDelegate subclass. Firebase Auth reCAPTCHA redirect URLs may be forwarded to your router.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return config;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await fs.promises.writeFile(sceneDelegatePath, newContents);
|
|
156
|
+
return config;
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
};
|