Skip to content

Commit 7fe0171

Browse files
author
Chris Bobbe
committed
[draft] Use "Sign in with Apple".
1 parent 008be3f commit 7fe0171

File tree

3 files changed

+84
-16
lines changed

3 files changed

+84
-16
lines changed

src/api/settings/getServerSettings.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,28 @@ export type AuthenticationMethods = {
1414
...
1515
};
1616

17-
export type ExternalAuthenticationMethod = {|
18-
name: string,
17+
type BaseExternalAuthenticationMethod = {|
1918
display_name: string,
2019
display_icon: string | null,
2120
login_url: string,
2221
signup_url: string,
2322
|};
2423

24+
export type AppleExternalAuthenticationMethod = {|
25+
name: 'apple',
26+
apple_kid: string,
27+
...BaseExternalAuthenticationMethod,
28+
|};
29+
30+
export type OtherExternalAuthenticationMethod = {|
31+
name: string, // I'd like this to be "all strings except 'apple'"
32+
...BaseExternalAuthenticationMethod,
33+
|};
34+
35+
export type ExternalAuthenticationMethod =
36+
| AppleExternalAuthenticationMethod
37+
| OtherExternalAuthenticationMethod;
38+
2539
export type ApiResponseServerSettings = {|
2640
...ApiResponseSuccess,
2741
authentication_methods: AuthenticationMethods,

src/common/Icons.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const IconPin: IconType = props => <SimpleLineIcons name="pin" {...props}
4949
export const IconPrivate: IconType = props => <Feather name="lock" {...props} />;
5050
export const IconPrivateChat: IconType = props => <Feather name="mail" {...props} />;
5151
export const IconDownArrow: IconType = props => <Feather name="chevron-down" {...props} />;
52+
export const IconApple: IconType = props => <IoniconsIcon name="logo-apple" {...props} />;
5253
export const IconGoogle: IconType = props => <IoniconsIcon name="logo-google" {...props} />;
5354
export const IconGitHub: IconType = props => <Feather name="github" {...props} />;
5455
export const IconWindows: IconType = props => <IoniconsIcon name="logo-windows" {...props} />;

src/start/AuthScreen.js

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
/* @flow strict-local */
22

33
import React, { PureComponent } from 'react';
4-
import { Linking } from 'react-native';
4+
import { Linking, Platform } from 'react-native';
55
import type { NavigationScreenProp } from 'react-navigation';
6+
import * as AppleAuthentication from 'expo-apple-authentication';
67

78
import type {
89
AuthenticationMethods,
910
Dispatch,
1011
ExternalAuthenticationMethod,
1112
ApiResponseServerSettings,
1213
} from '../types';
13-
import { IconPrivate, IconGoogle, IconGitHub, IconWindows, IconTerminal } from '../common/Icons';
14+
import {
15+
IconApple,
16+
IconPrivate,
17+
IconGoogle,
18+
IconGitHub,
19+
IconWindows,
20+
IconTerminal,
21+
} from '../common/Icons';
1422
import type { IconType } from '../common/Icons';
1523
import { connect } from '../react-redux';
1624
import styles from '../styles';
1725
import { Centerer, Screen, ZulipButton } from '../common';
1826
import { getCurrentRealm } from '../selectors';
1927
import RealmInfo from './RealmInfo';
20-
import { getFullUrl } from '../utils/url';
28+
import { getFullUrl, encodeParamsForUrl } from '../utils/url';
2129
import * as webAuth from './webAuth';
2230
import { loginSuccess, navigateToDev, navigateToPassword } from '../actions';
31+
import IosCompliantAppleAuthButton from './IosCompliantAppleAuthButton';
2332

33+
const KANDRA_APPLE_KID = 'asdfjkl;'; // TODO, of course (likely won't live here)
2434
/**
2535
* Describes a method for authenticating to the server.
2636
*
@@ -99,6 +109,7 @@ const externalMethodIcons = new Map([
99109
['google', IconGoogle],
100110
['github', IconGitHub],
101111
['azuread', IconWindows],
112+
['apple', IconApple],
102113
]);
103114

104115
/** Exported for tests only. */
@@ -220,12 +231,47 @@ class AuthScreen extends PureComponent<Props> {
220231
this.props.dispatch(navigateToPassword(serverSettings.require_email_format_usernames));
221232
};
222233

223-
handleAuth = (method: AuthenticationMethodDetails) => {
234+
handleNativeAppleAuth = async () => {
235+
const { dispatch, realm } = this.props;
236+
const nativeAppleOTP = await webAuth.generateOtp();
237+
const credential = await AppleAuthentication.signInAsync({
238+
state: nativeAppleOTP,
239+
});
240+
if (credential.state !== nativeAppleOTP) {
241+
throw new Error('OTP mismatch');
242+
}
243+
244+
// TODO: handle errors from this fetch (no Zulip account exists, etc.)
245+
const { url: callbackUrl } = await fetch(
246+
`${this.props.realm}/complete/apple/?mobile_flow_otp=${nativeAppleOTP}`,
247+
{
248+
method: 'POST',
249+
headers: {
250+
'Content-Type': 'application/x-www-form-urlencoded',
251+
},
252+
body: encodeParamsForUrl(credential),
253+
},
254+
);
255+
256+
const auth = webAuth.authFromCallbackUrl(callbackUrl, nativeAppleOTP, realm);
257+
if (auth) {
258+
dispatch(loginSuccess(auth.realm, auth.email, auth.apiKey));
259+
}
260+
};
261+
262+
handleAuth = async (method: AuthenticationMethodDetails) => {
224263
const { action } = method;
264+
const shouldUseNativeAppleFlow =
265+
method.name === 'apple'
266+
&& method.apple_kid === KANDRA_APPLE_KID
267+
&& (await AppleAuthentication.isAvailableAsync());
268+
225269
if (action === 'dev') {
226270
this.handleDevAuth();
227271
} else if (action === 'password') {
228272
this.handlePassword();
273+
} else if (shouldUseNativeAppleFlow) {
274+
this.handleNativeAppleAuth();
229275
} else {
230276
this.beginWebAuth(action.url);
231277
}
@@ -244,16 +290,23 @@ class AuthScreen extends PureComponent<Props> {
244290
{activeAuthentications(
245291
serverSettings.authentication_methods,
246292
serverSettings.external_authentication_methods,
247-
).map(auth => (
248-
<ZulipButton
249-
key={auth.name}
250-
style={styles.halfMarginTop}
251-
secondary
252-
text={`Sign in with ${auth.displayName}`}
253-
Icon={auth.Icon}
254-
onPress={() => this.handleAuth(auth)}
255-
/>
256-
))}
293+
).map(auth =>
294+
auth.name === 'apple' && Platform.OS === 'ios' ? (
295+
<IosCompliantAppleAuthButton
296+
style={styles.halfMarginTop}
297+
onPress={() => this.handleAuth(auth)}
298+
/>
299+
) : (
300+
<ZulipButton
301+
key={auth.name}
302+
style={styles.halfMarginTop}
303+
secondary
304+
text={`Sign in with ${auth.displayName}`}
305+
Icon={auth.Icon}
306+
onPress={() => this.handleAuth(auth)}
307+
/>
308+
),
309+
)}
257310
</Centerer>
258311
</Screen>
259312
);

0 commit comments

Comments
 (0)