Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apple sign-in #16723

Merged
merged 4 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
<>
Copy link
Contributor

Choose a reason for hiding this comment

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

This will cause regression of not showing open desktop app popup on unauthorized screens (i.e. login page, public chat room)

Copy link
Member

Choose a reason for hiding this comment

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

no. it's just moved up the tree

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I don't find that. can you please share link?

Copy link
Member

Choose a reason for hiding this comment

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

lookup AppNavigator (sorry couldn't link from phone)

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe you meant this but as you see, it wrapped AuthScreens, not PublicScreens

Screenshot 2023-06-07 at 12 12 33 AM

Copy link
Contributor Author

@lindboe lindboe Jun 7, 2023

Choose a reason for hiding this comment

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

This will cause regression of not showing open desktop app popup on unauthorized screens (i.e. login page, public chat room)

This is intended. We asked in Slack and there was no indication that the prompt to open the app in desktop prior to sign-in was essential: https://expensify.slack.com/archives/C01GTK53T8Q/p1683070404415199

Additionally, this behavior leads to problems in this case, mentioned in that thread:

If a user had an issue with the sign in flow with desktop, if they went "back" to the web login form and signed in from there, once signed in they would not be prompted to open the app in desktop. They'd have to reload the app, or we'd have to add a button to open the desktop app. If the user could sign in on web and then only after they are signed in, be prompted to open the desktop app, that would solve this problem.

Sorry that wasn't conveyed more clearly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we allowed access to public room link for all users even not logged in, we should enable it back.
Asked on that slack thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Follow up from Slack thread: It looks like this should still work, because authToken is still set even in this "anonymous user" use case (https://expensify.slack.com/archives/C01GTK53T8Q/p1686181191875739?thread_ts=1683070404.415199&cid=C01GTK53T8Q)

{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