Skip to content

Commit

Permalink
Merge pull request #16723 from infinitered/apple-sign-in
Browse files Browse the repository at this point in the history
Apple sign-in
  • Loading branch information
marcochavezf authored Jul 19, 2023
2 parents 568aab2 + e026875 commit f321998
Show file tree
Hide file tree
Showing 34 changed files with 822 additions and 53 deletions.
4 changes: 4 additions & 0 deletions assets/images/signIn/apple-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions assets/images/signIn/google-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 128 additions & 0 deletions contributingGuides/TESTING_APPLE_GOOGLE_SIGNIN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Testing Apple/Google sign-in

Due to some technical constraints, Apple and Google sign-in may require additional configuration to be able to work in the development environment as expected. This document describes any additional steps for each platform.

## Apple

### Web

The Sign in with Apple process will break after the user signs in if the pop-up process is not started from a page at an HTTPS domain registered with Apple. To fix this, you could make a new configuration with your own HTTPS domain, but then the Apple configuration won't match that of Expensify's backend.

So to be able to test this, we have two parts:
1. Create a valid Sign in with Apple token using valid configuration for the Expensify app, by creating and intercepting one on Android
2. Host the development web app at an HTTPS domain using SSH tunneling, and in the web app use a custom Apple config wiht this HTTPS domain registered

Requirements:
1. Authorization on an Apple Development account or team to create new Service IDs
2. A paid ngrok.io account, to be able to use custom subdomains, or use serveo.net for a free alternative (must be signed in to use custom subdomains)

#### Generate the token to use

On an Android build, alter the `AppleSignIn` component to log the token generated, instead of sending it to the Expensify API:

```js
// .then((token) => Session.beginAppleSignIn(token))
.then((token) => console.log("TOKEN: ", token))
```

If you need to check that you received the correct data, check it on [jwt.io](https://jwt.io), which will decode it if it is a valid JWT token. It will also show when the token expires.

Add this token to a `.env` file at the root of the project:

```
ASI_TOKEN_OVERRIDE="..."
```

#### Configure the SSH tunneling

You can use any SSH tunneling service that allows you to configure custom subdomains so that we have a consistent address to use. We'll use ngrok in these examples, but ngrok requires a paid account for this. If you need a free option, try serveo.net.

After you've set ngrok up to be able to run on your machine (requires configuring a key with the command line tool), test hosting the web app on a custom subdomain. This example assumes the development web app is running at `localhost:8080`:

```
ngrok http 8080 --host-header="localhost:8080" --subdomain=mysubdomain
```

The `--host-header` flag is there to avoid webpack errors with header validation. In addition, add `allowedHosts: 'all'` to the dev server config in `webpack.dev.js`:

```js
devServer: {
...,
allowedHosts: 'all',
}
```

#### Configure Apple Service ID

Now that you have an HTTPS address to use, you can create an Apple Service ID configuration that will work with it.

1. Create a new app ID on your Apple development team that can be used to test this, following the instructions [here](https://github.com/invertase/react-native-apple-authentication/blob/main/docs/INITIAL_SETUP.md).
2. Create a new service ID following the instructions [here](https://github.com/invertase/react-native-apple-authentication/blob/main/docs/ANDROID_EXTRA.md). For allowed domains, enter your SSH tunnel address (e.g., `https://mysubdomain.ngrok.io`), and for redirect URLs, just make up an endpoint, it's never actually invoked (e.g., `mysubdomain.ngrok.io/appleauth`).

Notes:
- Depending on your Apple account configuration, you may need additional permissions to access some of the features described in the instructions above.
- While the Apple Sign In configuration requires a `clientId`, the Apple Developer console calls this a `Service ID`.

Finally, edit `.env` to use your client (service) ID and redirect URL config:

```
ASI_CLIENTID_OVERRIDE=com.example.test
ASI_REDIRECTURI_OVERRIDE=https://mysubdomain.ngrok.io/appleauth
```

#### Run the app

Remember that you will need to restart the web server if you make a change to the `.env` file.

### Desktop

Desktop will require the same configuration, with these additional steps:

#### Configure web app URL in .env

Add `NEW_EXPENSIFY_URL` to .env, and set it to the HTTPS URL where the web app can be found, for example:

```
NEW_EXPENSIFY_URL=https://subdomain.ngrok.io
```

This is required because the desktop app needs to know the address of the web app, and must open it at
the HTTPS domain configured to work with Sign in with Apple.

#### Set Environment to something other than "Development"

The DeepLinkWrapper component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development".

Within the `.env` file, set `envName` to something other than "Development", for example:

```
envName=Staging
```

Alternatively, within the `DeepLinkWrapper/index.website.js` file you can set the `CONFIG.ENVIRONMENT` to something other than "Development".

#### Handle deep links in dev on MacOS

If developing on MacOS, the development desktop app can't handle deeplinks correctly. To be able to test deeplinking back to the app, follow these steps:

1. Create a "real" build of the desktop app, which can handle deep links, open the build folder, and install the dmg there:

```
npm run desktop-build --publish=never
open desktop-build
# Then double-click "NewExpensify.dmg" in Finder window
```

2. Even with this build, the deep link may not be handled by the correct app, as the development Electron config seems to intercept it sometimes. To manage this, install [SwiftDefaultApps](https://github.com/Lord-Kamina/SwiftDefaultApps), which adds a preference pane that can be used to configure which app should handle deep links.

## Google

### Web

#### Port requirements

Google allows the web app to be hosted at localhost, but according to the current Google console configuration, it must be hosted on port 8080.

#### Visual differences

Google's web button has a visible rectangular iframe around it when the app is running at `localhost`. When the app is hosted at an HTTPS address, this iframe is not shown.
4 changes: 4 additions & 0 deletions ios/NewExpensify/Chat.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:new.expensify.com</string>
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,8 @@ PODS:
- React-jsi (= 0.71.2-alpha.3)
- React-logger (= 0.71.2-alpha.3)
- React-perflogger (= 0.71.2-alpha.3)
- RNAppleAuthentication (2.2.2):
- React-Core
- RNCAsyncStorage (1.17.11):
- React-Core
- RNCClipboard (1.5.1):
Expand Down Expand Up @@ -800,6 +802,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNAppleAuthentication (from `../node_modules/@invertase/react-native-apple-authentication`)"
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
Expand Down Expand Up @@ -980,6 +983,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
RNAppleAuthentication:
:path: "../node_modules/@invertase/react-native-apple-authentication"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
Expand Down Expand Up @@ -1114,6 +1119,7 @@ SPEC CHECKSUMS:
React-RCTVibration: 53291ee889eb2e1558a1507168af310926ad1ce1
React-runtimeexecutor: 2c2c364acf7d90ec4d503e9f97b83683e040de08
ReactCommon: 470b1793330b7254a68741f071c5ae432a0a25d6
RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNCPicker: 0b65be85fe7954fbb2062ef079e3d1cde252d888
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@formatjs/intl-numberformat": "^8.5.0",
"@formatjs/intl-pluralrules": "^5.2.2",
"@gorhom/portal": "^1.0.14",
"@invertase/react-native-apple-authentication": "^2.2.2",
"@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52",
"@onfido/react-native-sdk": "7.4.0",
"@react-native-async-storage/async-storage": "^1.17.10",
Expand Down
10 changes: 10 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@ const CONST = {
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'http://localhost:',

SIGN_IN_FORM_WIDTH: 300,

APPLE_SIGN_IN_SERVICE_ID: 'com.chat.expensify.chat.AppleSignIn',
APPLE_SIGN_IN_REDIRECT_URI: 'https://new.expensify.com/appleauth',

SIGN_IN_METHOD: {
APPLE: 'Apple',
GOOGLE: 'Google',
},

OPTION_TYPE: {
REPORT: 'report',
PERSONAL_DETAIL: 'personalDetail',
Expand Down
8 changes: 4 additions & 4 deletions src/Expensify.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import React, {useCallback, useState, useEffect, useRef, useLayoutEffect, useMemo} from 'react';
import {AppState, Linking} from 'react-native';
import Onyx, {withOnyx} from 'react-native-onyx';

import * as Report from './libs/actions/Report';
import BootSplash from './libs/BootSplash';
import * as ActiveClientManager from './libs/ActiveClientManager';
Expand All @@ -24,11 +23,11 @@ import withLocalize, {withLocalizePropTypes} from './components/withLocalize';
import * as User from './libs/actions/User';
import NetworkConnection from './libs/NetworkConnection';
import Navigation from './libs/Navigation/Navigation';
import DeeplinkWrapper from './components/DeeplinkWrapper';
import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu';
import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu';
import SplashScreenHider from './components/SplashScreenHider';
import KeyboardShortcutsModal from './components/KeyboardShortcutsModal';
import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';

Expand Down Expand Up @@ -183,7 +182,7 @@ function Expensify(props) {
}

return (
<DeeplinkWrapper>
<>
{shouldInit && (
<>
<KeyboardShortcutsModal />
Expand All @@ -206,6 +205,7 @@ function Expensify(props) {
</>
)}

<AppleAuthWrapper />
{hasAttemptedToOpenPublicRoom && (
<NavigationRoot
onReady={setNavigationReady}
Expand All @@ -214,7 +214,7 @@ function Expensify(props) {
)}

{shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
</DeeplinkWrapper>
</>
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export default {
getGetAssistanceRoute: (taskID) => `get-assistance/${taskID}`,
UNLINK_LOGIN: 'u/:accountID/:validateCode',

APPLE_SIGN_IN: 'sign-in-with-apple',

// This is a special validation URL that will take the user to /workspace/new after validation. This is used
// when linking users from e.com in order to share a session in this app.
ENABLE_PAYMENTS: 'enable-payments',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AdminRoomAvatar from '../../../assets/images/avatars/admin-room.svg';
import Android from '../../../assets/images/android.svg';
import AnnounceRoomAvatar from '../../../assets/images/avatars/announce-room.svg';
import Apple from '../../../assets/images/apple.svg';
import AppleLogo from '../../../assets/images/signIn/apple-logo.svg';
import ArrowRight from '../../../assets/images/arrow-right.svg';
import ArrowRightLong from '../../../assets/images/arrow-right-long.svg';
import ArrowsUpDown from '../../../assets/images/arrows-updown.svg';
Expand Down Expand Up @@ -124,6 +125,7 @@ export {
Android,
AnnounceRoomAvatar,
Apple,
AppleLogo,
ArrowRight,
ArrowRightLong,
ArrowsUpDown,
Expand Down
26 changes: 26 additions & 0 deletions src/components/SignInButtons/AppleAuthWrapper/index.ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {useEffect} from 'react';
import appleAuth from '@invertase/react-native-apple-authentication';
import * as Session from '../../../libs/actions/Session';

/**
* Apple Sign In wrapper for iOS
* revokes the session if the credential is revoked.
*/

function AppleAuthWrapper() {
useEffect(() => {
if (!appleAuth.isSupported) {
return;
}
const listener = appleAuth.onCredentialRevoked(() => {
Session.signOut();
});
return () => {
listener.remove();
};
}, []);

return null;
}

export default AppleAuthWrapper;
5 changes: 5 additions & 0 deletions src/components/SignInButtons/AppleAuthWrapper/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function AppleAuthWrapper() {
return null;
}

export default AppleAuthWrapper;
Loading

0 comments on commit f321998

Please sign in to comment.