Skip to content

Commit

Permalink
feat(auth): Add Expo support for phone auth (#6645)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
4 people authored Oct 30, 2022
1 parent 1f2385b commit 97a4ea5
Show file tree
Hide file tree
Showing 12 changed files with 578 additions and 361 deletions.
16 changes: 16 additions & 0 deletions docs/auth/phone-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ For reliable automated testing, you may want to disable both automatic and fallb

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.

# Expo Setup

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.

```json
{
"expo": {
"plugins": ["@react-native-firebase/app", "@react-native-firebase/auth"]
}
}
```

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.

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).

# Sign-in

The module provides a `signInWithPhoneNumber` method which accepts a phone number. Firebase sends an SMS message to the
Expand Down
1 change: 1 addition & 0 deletions packages/auth/app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./plugin/build');
11 changes: 10 additions & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"scripts": {
"build": "genversion --semi lib/version.js",
"build:clean": "rimraf android/build && rimraf ios/build",
"prepare": "yarn run build"
"build:plugin": "rimraf plugin/build && tsc --build plugin",
"lint:plugin": "eslint plugin/src/*",
"prepare": "yarn run build && yarn run build:plugin"
},
"repository": {
"type": "git",
Expand All @@ -21,10 +23,17 @@
"firebase",
"auth"
],
"dependencies": {
"plist": "^3.0.5",
"@expo/config-plugins": "^5.0.1"
},
"peerDependencies": {
"@react-native-firebase/app": "16.3.1"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/plist": "^3.0.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Config Plugin iOS Tests adds url types to the Info.plist 1`] = `
{
"CFBundleURLTypes": [
{
"CFBundleURLSchemes": [
"com.googleusercontent.apps.SomeRandomClientIdString",
],
},
],
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>SomeRandomClientIdString.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.SomeRandomClientIdString</string>
<key>ANDROID_CLIENT_ID</key>
<string>SomeRandomAndroidClientIdString.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>SomeRandomApiKeyString</string>
<key>GCM_SENDER_ID</key>
<string>SomeRandomGcmSenderIdNumber</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.example.app</string>
<key>PROJECT_ID</key>
<string>example</string>
<key>STORAGE_BUCKET</key>
<string>example.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1234:1234:ios:1234</string>
<key>DATABASE_URL</key>
<string>https://example.firebaseio.com</string>
</dict>
</plist>
75 changes: 75 additions & 0 deletions packages/auth/plugin/__tests__/iosPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import path from 'path';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { setUrlTypesForCaptcha } from '../src/ios/urlTypes';

describe('Config Plugin iOS Tests', () => {
beforeEach(function () {
jest.resetAllMocks();
});

it('throws if path to GoogleServer-Info.plist is not provided', async () => {
expect(() => {
setUrlTypesForCaptcha({
config: {
name: 'TestName',
slug: 'TestSlug',
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
modResults: {},
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
ios: {},
},
});
}).toThrow(
`[@react-native-firebase/auth] Your app.json file is missing ios.googleServicesFile. Please add this field.`,
);
});

it('throws if GoogleServer-Info.plist cannot be read', async () => {
const googleServiceFilePath = path.join(__dirname, 'fixtures', 'ThisFileDoesNotExist.plist');
expect(() => {
setUrlTypesForCaptcha({
config: {
name: 'TestName',
slug: 'TestSlug',
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
modResults: {},
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
ios: { googleServicesFile: 'ThisFileDoesNotExist.plist' },
},
});
}).toThrow(
`[@react-native-firebase/auth] GoogleService-Info.plist doesn't exist in ${googleServiceFilePath}. Place it there or configure the path in app.json`,
);
});

it('throws if GoogleServer-Info.plist has no reversed client id', async () => {
expect(() => {
setUrlTypesForCaptcha({
config: {
name: 'TestName',
slug: 'TestSlug',
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
modResults: {},
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
ios: { googleServicesFile: 'TestGoogleService-Info.incomplete.plist' },
},
});
}).toThrow(
'[@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?',
);
});

it('adds url types to the Info.plist', async () => {
const result = setUrlTypesForCaptcha({
config: {
name: 'TestName',
slug: 'TestSlug',
modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any,
modResults: {},
modRawConfig: { name: 'TestName', slug: 'TestSlug' },
ios: { googleServicesFile: 'TestGoogleService-Info.plist' },
},
});
expect(result.modResults).toMatchSnapshot();
});
});
16 changes: 16 additions & 0 deletions packages/auth/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins';

import { withIosCaptchaUrlTypes } from './ios';

/**
* A config plugin for configuring `@react-native-firebase/auth`
*/
const withRnFirebaseAuth: ConfigPlugin = config => {
return withPlugins(config, [
// iOS
withIosCaptchaUrlTypes,
]);
};

const pak = require('@react-native-firebase/auth/package.json');
export default createRunOncePlugin(withRnFirebaseAuth, pak.name, pak.version);
3 changes: 3 additions & 0 deletions packages/auth/plugin/src/ios/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { withIosCaptchaUrlTypes } from './urlTypes';

export { withIosCaptchaUrlTypes };
90 changes: 90 additions & 0 deletions packages/auth/plugin/src/ios/urlTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
ConfigPlugin,
IOSConfig,
withInfoPlist,
ExportedConfigWithProps,
} from '@expo/config-plugins';
import fs from 'fs';
import path from 'path';
import plist from 'plist';

// does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project
export const withIosCaptchaUrlTypes: ConfigPlugin = config => {
return withInfoPlist(config, config => {
return setUrlTypesForCaptcha({ config });
});
};

function getReversedClientId(googleServiceFilePath: string): string {
try {
const googleServicePlist = fs.readFileSync(googleServiceFilePath, 'utf8');

const googleServiceJson = plist.parse(googleServicePlist) as { REVERSED_CLIENT_ID: string };
const REVERSED_CLIENT_ID = googleServiceJson.REVERSED_CLIENT_ID;

if (!REVERSED_CLIENT_ID) {
throw new TypeError('REVERSED_CLIENT_ID missing');
}

return REVERSED_CLIENT_ID;
} catch {
throw new Error(
'[@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?',
);
}
}

// add phone auth support by configuring recaptcha
// https://github.com/invertase/react-native-firebase/pull/6167
function addUriScheme(
config: ExportedConfigWithProps<IOSConfig.InfoPlist>,
reversedClientId: string,
): ExportedConfigWithProps<IOSConfig.InfoPlist> {
if (!config.modResults) {
config.modResults = {};
}

if (!config.modResults.CFBundleURLTypes) {
config.modResults.CFBundleURLTypes = [];
}

const hasReverseClientId = config.modResults.CFBundleURLTypes?.some(urlType =>
urlType.CFBundleURLSchemes.includes(reversedClientId),
);

if (!hasReverseClientId) {
config.modResults.CFBundleURLTypes.push({
CFBundleURLSchemes: [reversedClientId],
});
}

return config;
}

export function setUrlTypesForCaptcha({
config,
}: {
config: ExportedConfigWithProps<IOSConfig.InfoPlist>;
}) {
const googleServicesFileRelativePath = config.ios?.googleServicesFile;
if (!googleServicesFileRelativePath) {
throw new Error(
`[@react-native-firebase/auth] Your app.json file is missing ios.googleServicesFile. Please add this field.`,
);
}
const googleServiceFilePath = path.resolve(
config.modRequest.projectRoot,
googleServicesFileRelativePath,
);

if (!fs.existsSync(googleServiceFilePath)) {
throw new Error(
`[@react-native-firebase/auth] GoogleService-Info.plist doesn't exist in ${googleServiceFilePath}. Place it there or configure the path in app.json`,
);
}

const reversedClientId = getReversedClientId(googleServiceFilePath);
addUriScheme(config, reversedClientId);

return config;
}
9 changes: 9 additions & 0 deletions packages/auth/plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "@tsconfig/node12/tsconfig.json",
"compilerOptions": {
"outDir": "build",
"rootDir": "src",
"declaration": true
},
"include": ["./src"]
}
Loading

1 comment on commit 97a4ea5

@vercel
Copy link

@vercel vercel bot commented on 97a4ea5 Oct 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.