Skip to content

Commit 97a4ea5

Browse files
DoctorJohnnandorojobarthapmikehardy
authored
feat(auth): Add Expo support for phone auth (#6645)
* feat(auth) expo config plugin for phone auth * add expo config plugin dependency * build scripts * fix types * Update docs/auth/phone-auth.md Co-authored-by: Bartłomiej Klocek <[email protected]> * reformat * plugin file * plist dep * Update package.json * Add tests for auth expo config plugin * Add missing files previously ignored by .gitignore * build(deps): bump to new expo config plugins version Co-authored-by: Fernando Rojo <[email protected]> Co-authored-by: Bartłomiej Klocek <[email protected]> Co-authored-by: Mike Hardy <[email protected]>
1 parent 1f2385b commit 97a4ea5

File tree

12 files changed

+578
-361
lines changed

12 files changed

+578
-361
lines changed

docs/auth/phone-auth.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ For reliable automated testing, you may want to disable both automatic and fallb
2929

3030
Ensure that all parts of step 1 and 2 from [the official firebase Android phone auth docs](https://firebase.google.com/docs/auth/android/phone-auth#enable-phone-number-sign-in-for-your-firebase-project) have been followed.
3131

32+
# Expo Setup
33+
34+
To use phone auth in an expo app, add the `@react-native-firebase/auth` config plug-in to the [`plugins`](https://docs.expo.io/versions/latest/config/app/#plugins) section of your `app.json`. This is in addition to the `@react-native-firebase/app` plugin.
35+
36+
```json
37+
{
38+
"expo": {
39+
"plugins": ["@react-native-firebase/app", "@react-native-firebase/auth"]
40+
}
41+
}
42+
```
43+
44+
The `@react-native-firebase/auth` config plugin is not required for all auth providers, but it is required to use phone auth. The plugin [will set up reCAPTCHA](https://firebase.google.com/docs/auth/ios/phone-auth#set-up-recaptcha-verification) verification for you on iOS.
45+
46+
The recommendation is to use a [custom development client](https://docs.expo.dev/clients/getting-started/). For more info on using Expo with React Native Firebase, see our [Expo docs](/#expo).
47+
3248
# Sign-in
3349

3450
The module provides a `signInWithPhoneNumber` method which accepts a phone number. Firebase sends an SMS message to the

packages/auth/app.plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./plugin/build');

packages/auth/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"scripts": {
99
"build": "genversion --semi lib/version.js",
1010
"build:clean": "rimraf android/build && rimraf ios/build",
11-
"prepare": "yarn run build"
11+
"build:plugin": "rimraf plugin/build && tsc --build plugin",
12+
"lint:plugin": "eslint plugin/src/*",
13+
"prepare": "yarn run build && yarn run build:plugin"
1214
},
1315
"repository": {
1416
"type": "git",
@@ -21,10 +23,17 @@
2123
"firebase",
2224
"auth"
2325
],
26+
"dependencies": {
27+
"plist": "^3.0.5",
28+
"@expo/config-plugins": "^5.0.1"
29+
},
2430
"peerDependencies": {
2531
"@react-native-firebase/app": "16.3.1"
2632
},
2733
"publishConfig": {
2834
"access": "public"
35+
},
36+
"devDependencies": {
37+
"@types/plist": "^3.0.2"
2938
}
3039
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Config Plugin iOS Tests adds url types to the Info.plist 1`] = `
4+
{
5+
"CFBundleURLTypes": [
6+
{
7+
"CFBundleURLSchemes": [
8+
"com.googleusercontent.apps.SomeRandomClientIdString",
9+
],
10+
},
11+
],
12+
}
13+
`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
</dict>
6+
</plist>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CLIENT_ID</key>
6+
<string>SomeRandomClientIdString.apps.googleusercontent.com</string>
7+
<key>REVERSED_CLIENT_ID</key>
8+
<string>com.googleusercontent.apps.SomeRandomClientIdString</string>
9+
<key>ANDROID_CLIENT_ID</key>
10+
<string>SomeRandomAndroidClientIdString.apps.googleusercontent.com</string>
11+
<key>API_KEY</key>
12+
<string>SomeRandomApiKeyString</string>
13+
<key>GCM_SENDER_ID</key>
14+
<string>SomeRandomGcmSenderIdNumber</string>
15+
<key>PLIST_VERSION</key>
16+
<string>1</string>
17+
<key>BUNDLE_ID</key>
18+
<string>com.example.app</string>
19+
<key>PROJECT_ID</key>
20+
<string>example</string>
21+
<key>STORAGE_BUCKET</key>
22+
<string>example.appspot.com</string>
23+
<key>IS_ADS_ENABLED</key>
24+
<false></false>
25+
<key>IS_ANALYTICS_ENABLED</key>
26+
<false></false>
27+
<key>IS_APPINVITE_ENABLED</key>
28+
<true></true>
29+
<key>IS_GCM_ENABLED</key>
30+
<true></true>
31+
<key>IS_SIGNIN_ENABLED</key>
32+
<true></true>
33+
<key>GOOGLE_APP_ID</key>
34+
<string>1234:1234:ios:1234</string>
35+
<key>DATABASE_URL</key>
36+
<string>https://example.firebaseio.com</string>
37+
</dict>
38+
</plist>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import path from 'path';
2+
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
3+
import { setUrlTypesForCaptcha } from '../src/ios/urlTypes';
4+
5+
describe('Config Plugin iOS Tests', () => {
6+
beforeEach(function () {
7+
jest.resetAllMocks();
8+
});
9+
10+
it('throws if path to GoogleServer-Info.plist is not provided', async () => {
11+
expect(() => {
12+
setUrlTypesForCaptcha({
13+
config: {
14+
name: 'TestName',
15+
slug: 'TestSlug',
16+
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
17+
modResults: {},
18+
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
19+
ios: {},
20+
},
21+
});
22+
}).toThrow(
23+
`[@react-native-firebase/auth] Your app.json file is missing ios.googleServicesFile. Please add this field.`,
24+
);
25+
});
26+
27+
it('throws if GoogleServer-Info.plist cannot be read', async () => {
28+
const googleServiceFilePath = path.join(__dirname, 'fixtures', 'ThisFileDoesNotExist.plist');
29+
expect(() => {
30+
setUrlTypesForCaptcha({
31+
config: {
32+
name: 'TestName',
33+
slug: 'TestSlug',
34+
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
35+
modResults: {},
36+
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
37+
ios: { googleServicesFile: 'ThisFileDoesNotExist.plist' },
38+
},
39+
});
40+
}).toThrow(
41+
`[@react-native-firebase/auth] GoogleService-Info.plist doesn't exist in ${googleServiceFilePath}. Place it there or configure the path in app.json`,
42+
);
43+
});
44+
45+
it('throws if GoogleServer-Info.plist has no reversed client id', async () => {
46+
expect(() => {
47+
setUrlTypesForCaptcha({
48+
config: {
49+
name: 'TestName',
50+
slug: 'TestSlug',
51+
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
52+
modResults: {},
53+
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
54+
ios: { googleServicesFile: 'TestGoogleService-Info.incomplete.plist' },
55+
},
56+
});
57+
}).toThrow(
58+
'[@react-native-firebase/auth] Failed to parse your GoogleService-Info.plist. Are you sure it is a valid Info.Plist file with a REVERSE_CLIENT_ID field?',
59+
);
60+
});
61+
62+
it('adds url types to the Info.plist', async () => {
63+
const result = setUrlTypesForCaptcha({
64+
config: {
65+
name: 'TestName',
66+
slug: 'TestSlug',
67+
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
68+
modResults: {},
69+
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
70+
ios: { googleServicesFile: 'TestGoogleService-Info.plist' },
71+
},
72+
});
73+
expect(result.modResults).toMatchSnapshot();
74+
});
75+
});

packages/auth/plugin/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins';
2+
3+
import { withIosCaptchaUrlTypes } from './ios';
4+
5+
/**
6+
* A config plugin for configuring `@react-native-firebase/auth`
7+
*/
8+
const withRnFirebaseAuth: ConfigPlugin = config => {
9+
return withPlugins(config, [
10+
// iOS
11+
withIosCaptchaUrlTypes,
12+
]);
13+
};
14+
15+
const pak = require('@react-native-firebase/auth/package.json');
16+
export default createRunOncePlugin(withRnFirebaseAuth, pak.name, pak.version);

packages/auth/plugin/src/ios/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { withIosCaptchaUrlTypes } from './urlTypes';
2+
3+
export { withIosCaptchaUrlTypes };
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
ConfigPlugin,
3+
IOSConfig,
4+
withInfoPlist,
5+
ExportedConfigWithProps,
6+
} from '@expo/config-plugins';
7+
import fs from 'fs';
8+
import path from 'path';
9+
import plist from 'plist';
10+
11+
// does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project
12+
export const withIosCaptchaUrlTypes: ConfigPlugin = config => {
13+
return withInfoPlist(config, config => {
14+
return setUrlTypesForCaptcha({ config });
15+
});
16+
};
17+
18+
function getReversedClientId(googleServiceFilePath: string): string {
19+
try {
20+
const googleServicePlist = fs.readFileSync(googleServiceFilePath, 'utf8');
21+
22+
const googleServiceJson = plist.parse(googleServicePlist) as { REVERSED_CLIENT_ID: string };
23+
const REVERSED_CLIENT_ID = googleServiceJson.REVERSED_CLIENT_ID;
24+
25+
if (!REVERSED_CLIENT_ID) {
26+
throw new TypeError('REVERSED_CLIENT_ID missing');
27+
}
28+
29+
return REVERSED_CLIENT_ID;
30+
} catch {
31+
throw new Error(
32+
'[@react-native-firebase/auth] Failed to parse your GoogleService-Info.plist. Are you sure it is a valid Info.Plist file with a REVERSE_CLIENT_ID field?',
33+
);
34+
}
35+
}
36+
37+
// add phone auth support by configuring recaptcha
38+
// https://github.com/invertase/react-native-firebase/pull/6167
39+
function addUriScheme(
40+
config: ExportedConfigWithProps<IOSConfig.InfoPlist>,
41+
reversedClientId: string,
42+
): ExportedConfigWithProps<IOSConfig.InfoPlist> {
43+
if (!config.modResults) {
44+
config.modResults = {};
45+
}
46+
47+
if (!config.modResults.CFBundleURLTypes) {
48+
config.modResults.CFBundleURLTypes = [];
49+
}
50+
51+
const hasReverseClientId = config.modResults.CFBundleURLTypes?.some(urlType =>
52+
urlType.CFBundleURLSchemes.includes(reversedClientId),
53+
);
54+
55+
if (!hasReverseClientId) {
56+
config.modResults.CFBundleURLTypes.push({
57+
CFBundleURLSchemes: [reversedClientId],
58+
});
59+
}
60+
61+
return config;
62+
}
63+
64+
export function setUrlTypesForCaptcha({
65+
config,
66+
}: {
67+
config: ExportedConfigWithProps<IOSConfig.InfoPlist>;
68+
}) {
69+
const googleServicesFileRelativePath = config.ios?.googleServicesFile;
70+
if (!googleServicesFileRelativePath) {
71+
throw new Error(
72+
`[@react-native-firebase/auth] Your app.json file is missing ios.googleServicesFile. Please add this field.`,
73+
);
74+
}
75+
const googleServiceFilePath = path.resolve(
76+
config.modRequest.projectRoot,
77+
googleServicesFileRelativePath,
78+
);
79+
80+
if (!fs.existsSync(googleServiceFilePath)) {
81+
throw new Error(
82+
`[@react-native-firebase/auth] GoogleService-Info.plist doesn't exist in ${googleServiceFilePath}. Place it there or configure the path in app.json`,
83+
);
84+
}
85+
86+
const reversedClientId = getReversedClientId(googleServiceFilePath);
87+
addUriScheme(config, reversedClientId);
88+
89+
return config;
90+
}

0 commit comments

Comments
 (0)