1
1
/* @flow strict-local */
2
2
3
3
import React , { PureComponent } from 'react' ;
4
- import { Linking } from 'react-native' ;
4
+ import { Linking , Platform } from 'react-native' ;
5
5
import type { NavigationScreenProp } from 'react-navigation' ;
6
+ import * as AppleAuthentication from 'expo-apple-authentication' ;
6
7
7
8
import type {
8
9
AuthenticationMethods ,
9
10
Dispatch ,
10
11
ExternalAuthenticationMethod ,
11
12
ApiResponseServerSettings ,
12
13
} 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' ;
14
22
import type { IconType } from '../common/Icons' ;
15
23
import { connect } from '../react-redux' ;
16
24
import styles from '../styles' ;
17
25
import { Centerer , Screen , ZulipButton } from '../common' ;
18
26
import { getCurrentRealm } from '../selectors' ;
19
27
import RealmInfo from './RealmInfo' ;
20
- import { getFullUrl } from '../utils/url' ;
28
+ import { getFullUrl , encodeParamsForUrl } from '../utils/url' ;
21
29
import * as webAuth from './webAuth' ;
22
30
import { loginSuccess , navigateToDev , navigateToPassword } from '../actions' ;
31
+ import IosCompliantAppleAuthButton from './IosCompliantAppleAuthButton' ;
32
+ import openLink from '../utils/openLink' ;
23
33
34
+ const KANDRA_APPLE_KID = 'asdfjkl;' ; // TODO, of course (likely won't live here)
24
35
/**
25
36
* Describes a method for authenticating to the server.
26
37
*
@@ -99,6 +110,7 @@ const externalMethodIcons = new Map([
99
110
[ 'google' , IconGoogle ] ,
100
111
[ 'github' , IconGitHub ] ,
101
112
[ 'azuread' , IconWindows ] ,
113
+ [ 'apple' , IconApple ] ,
102
114
] ) ;
103
115
104
116
/** Exported for tests only. */
@@ -229,12 +241,37 @@ class AuthScreen extends PureComponent<Props> {
229
241
this . props . dispatch ( navigateToPassword ( serverSettings . require_email_format_usernames ) ) ;
230
242
} ;
231
243
232
- handleAuth = ( method : AuthenticationMethodDetails ) => {
244
+ handleNativeAppleAuth = async ( ) => {
245
+ const state = await webAuth . generateRandomToken ( ) ;
246
+ const credential = await AppleAuthentication . signInAsync ( { state } ) ;
247
+ if ( credential . state !== state ) {
248
+ throw new Error ( '`state` mismatch' ) ;
249
+ }
250
+
251
+ otp = await webAuth . generateOtp ( ) ;
252
+
253
+ const params = encodeParamsForUrl ( {
254
+ mobile_flow_otp : otp ,
255
+ native_flow : true ,
256
+ id_token : credential . identityToken ,
257
+ } ) ;
258
+
259
+ openLink ( `${ this . props . realm } /complete/apple/?${ params } ` ) ;
260
+ } ;
261
+
262
+ handleAuth = async ( method : AuthenticationMethodDetails ) => {
233
263
const { action } = method ;
264
+ const shouldUseNativeAppleFlow =
265
+ method . name === 'apple'
266
+ && method . apple_kid === KANDRA_APPLE_KID
267
+ && ( await AppleAuthentication . isAvailableAsync ( ) ) ;
268
+
234
269
if ( action === 'dev' ) {
235
270
this . handleDevAuth ( ) ;
236
271
} else if ( action === 'password' ) {
237
272
this . handlePassword ( ) ;
273
+ } else if ( shouldUseNativeAppleFlow ) {
274
+ this . handleNativeAppleAuth ( ) ;
238
275
} else {
239
276
this . beginWebAuth ( action . url ) ;
240
277
}
@@ -253,16 +290,24 @@ class AuthScreen extends PureComponent<Props> {
253
290
{ activeAuthentications (
254
291
serverSettings . authentication_methods ,
255
292
serverSettings . external_authentication_methods ,
256
- ) . map ( auth => (
257
- < ZulipButton
258
- key = { auth . name }
259
- style = { styles . halfMarginTop }
260
- secondary
261
- text = { `Sign in with ${ auth . displayName } ` }
262
- Icon = { auth . Icon }
263
- onPress = { ( ) => this . handleAuth ( auth ) }
264
- />
265
- ) ) }
293
+ ) . map ( auth =>
294
+ auth . name === 'apple' && Platform . OS === 'ios' ? (
295
+ < IosCompliantAppleAuthButton
296
+ key = { auth . name }
297
+ style = { styles . halfMarginTop }
298
+ onPress = { ( ) => this . handleAuth ( auth ) }
299
+ />
300
+ ) : (
301
+ < ZulipButton
302
+ key = { auth . name }
303
+ style = { styles . halfMarginTop }
304
+ secondary
305
+ text = { `Sign in with ${ auth . displayName } ` }
306
+ Icon = { auth . Icon }
307
+ onPress = { ( ) => this . handleAuth ( auth ) }
308
+ />
309
+ ) ,
310
+ ) }
266
311
</ Centerer >
267
312
</ Screen >
268
313
) ;
0 commit comments