1import React from 'react'; 2import { Subscription } from '@unimodules/core'; 3import * as AppleAuthentication from 'expo-apple-authentication'; 4import { Alert, AsyncStorage, ScrollView, StyleSheet, View, Text, Button, Slider } from 'react-native'; 5 6import MonoText from '../components/MonoText'; 7 8const { 9 AppleAuthenticationButtonStyle, 10 AppleAuthenticationButtonType, 11 AppleAuthenticationCredentialState, 12 AppleAuthenticationScope, 13} = AppleAuthentication; 14 15type State = { 16 isAvailable: boolean; 17 buttonStyle: AppleAuthentication.AppleAuthenticationButtonStyle; 18 buttonType: AppleAuthentication.AppleAuthenticationButtonType; 19 cornerRadius: number; 20 credentials?: AppleAuthentication.AppleAuthenticationCredential | null; 21 credentialState?: AppleAuthentication.AppleAuthenticationCredentialState; 22} 23 24const USER_CREDENTIAL_KEY = 'ExpoNativeComponentList/AppleAuthentication'; 25 26const CREDENTIAL_MESSAGES = { 27 [AppleAuthenticationCredentialState.REVOKED]: 'Your authorization has been revoked.', 28 [AppleAuthenticationCredentialState.AUTHORIZED]: 'You\'re authorized.', 29 [AppleAuthenticationCredentialState.NOT_FOUND]: 'You\'re not registered yet.', 30 [AppleAuthenticationCredentialState.TRANSFERRED]: 'Credentials transferred.', // Whatever that means... 31}; 32 33export default class AppleAuthenticationScreen extends React.Component<{}, State> { 34 static navigationOptions = { 35 title: 'Apple Authentication', 36 }; 37 38 readonly state: State = { 39 isAvailable: false, 40 buttonStyle: AppleAuthenticationButtonStyle.WHITE, 41 buttonType: AppleAuthenticationButtonType.SIGN_IN, 42 cornerRadius: 5, 43 credentials: null, 44 }; 45 46 _subscription?: Subscription; 47 48 componentDidMount() { 49 this.checkAvailability(); 50 this.checkCredentials(); 51 this._subscription = AppleAuthentication.addRevokeListener(this.revokeListener); 52 } 53 54 componentWillUnmount() { 55 if (this._subscription) { 56 this._subscription.remove(); 57 } 58 } 59 60 revokeListener = () => { 61 this.setState({ credentials: null }); 62 Alert.alert('Credentials revoked!'); 63 }; 64 65 checkAvailability = async () => { 66 const isAvailable = await AppleAuthentication.isAvailableAsync(); 67 this.setState({ isAvailable }); 68 }; 69 70 signIn = async () => { 71 try { 72 const credentials = await AppleAuthentication.signInAsync({ 73 requestedScopes: [ 74 AppleAuthenticationScope.FULL_NAME, 75 AppleAuthenticationScope.EMAIL, 76 ], 77 state: 'this-is-a-test', 78 }); 79 this.setState({ credentials }); 80 if (credentials.user) { 81 await AsyncStorage.setItem(USER_CREDENTIAL_KEY, credentials.user); 82 } 83 await this.checkCredentials(); 84 } catch (error) { 85 alert(error); 86 } 87 } 88 89 refresh = async () => { 90 try { 91 const credentials = await AppleAuthentication.refreshAsync({ 92 requestedScopes: [ 93 AppleAuthenticationScope.FULL_NAME, 94 AppleAuthenticationScope.EMAIL, 95 ], 96 user: await this.getUserIdentifier(), 97 state: 'this-is-a-test', 98 }); 99 this.setState({ credentials }); 100 await this.checkCredentials(); 101 } catch (error) { 102 alert(error); 103 } 104 } 105 106 signOut = async () => { 107 try { 108 const credentials = await AppleAuthentication.signOutAsync({ 109 user: await this.getUserIdentifier(), 110 state: 'this-is-a-test', 111 }); 112 this.setState({ credentials: null, credentialState: null }); 113 } catch (error) { 114 alert(error); 115 } 116 } 117 118 async checkCredentials() { 119 try { 120 const user = await this.getUserIdentifier(); 121 const credentialState = await AppleAuthentication.getCredentialStateAsync(user); 122 this.setState({ credentialState }); 123 } catch (e) { 124 // Obtaining a user or the credentials failed - fallback to not found. 125 this.setState({ credentialState: AppleAuthenticationCredentialState.NOT_FOUND }); 126 } 127 }; 128 129 async getUserIdentifier(): Promise<string> { 130 return this.state.credentials && this.state.credentials.user || await AsyncStorage.getItem(USER_CREDENTIAL_KEY); 131 } 132 133 isAuthorized(): boolean { 134 return this.state.credentialState === AppleAuthenticationCredentialState.AUTHORIZED; 135 } 136 137 render() { 138 if (this.state.isAvailable === undefined) { 139 return ( 140 <View style={styles.container}> 141 <Text>Checking availability ...</Text> 142 </View> 143 ); 144 } 145 146 if (!this.state.isAvailable) { 147 return ( 148 <View style={styles.container}> 149 <Text>SignIn with Apple is not available</Text> 150 </View> 151 ) 152 } 153 154 return ( 155 <ScrollView style={styles.container} contentContainerStyle={styles.scrollViewContainer}> 156 <View style={styles.credentialStateContainer}> 157 <Text style={styles.credentialStateText}> 158 {CREDENTIAL_MESSAGES[this.state.credentialState]} 159 </Text> 160 </View> 161 <View style={styles.buttonContainer}> 162 <AppleAuthentication.AppleAuthenticationButton 163 buttonStyle={this.state.buttonStyle} 164 buttonType={this.state.buttonType} 165 cornerRadius={this.state.cornerRadius} 166 onPress={this.signIn} 167 style={{ width: 250, height: 44, margin: 15 }} 168 /> 169 </View> 170 <View style={styles.controlsContainer}> 171 <Text style={styles.controlsText}> 172 Button Style: 173 </Text> 174 <View style={styles.controlsButtonsContainer}> 175 <Button 176 title={`${AppleAuthenticationButtonStyle[AppleAuthenticationButtonStyle.WHITE]}`} 177 onPress={() => this.setState({ buttonStyle: AppleAuthenticationButtonStyle.WHITE })} 178 /> 179 <Button 180 title={`${AppleAuthenticationButtonStyle[AppleAuthenticationButtonStyle.WHITE_OUTLINE]}`} 181 onPress={() => this.setState({ buttonStyle: AppleAuthenticationButtonStyle.WHITE_OUTLINE })} 182 /> 183 <Button 184 title={`${AppleAuthenticationButtonStyle[AppleAuthenticationButtonStyle.BLACK]}`} 185 onPress={() => this.setState({ buttonStyle: AppleAuthenticationButtonStyle.BLACK })} 186 /> 187 </View> 188 </View> 189 <View style={styles.controlsContainer}> 190 <Text style={styles.controlsText}> 191 Button Type: 192 </Text> 193 <View style={styles.controlsButtonsContainer}> 194 <Button 195 title={`${AppleAuthenticationButtonType[AppleAuthenticationButtonType.SIGN_IN]}`} 196 onPress={() => this.setState({ buttonType: AppleAuthenticationButtonType.SIGN_IN })} 197 /> 198 <Button 199 title={`${AppleAuthenticationButtonType[AppleAuthenticationButtonType.CONTINUE]}`} 200 onPress={() => this.setState({ buttonType: AppleAuthenticationButtonType.CONTINUE })} 201 /> 202 </View> 203 </View> 204 <View style={styles.controlsContainer}> 205 <Text style={styles.controlsText}> 206 Button Corner Radius: {this.state.cornerRadius.toFixed(2)} 207 </Text> 208 <Slider minimumValue={0} maximumValue={20} value={this.state.cornerRadius} onValueChange={cornerRadius => this.setState({ cornerRadius })} /> 209 </View> 210 { this.state.credentials && 211 <View> 212 <Text>Credentials data:</Text> 213 <MonoText> 214 {JSON.stringify(this.state.credentials, null, 2)} 215 </MonoText> 216 </View> 217 } 218 { this.isAuthorized() && 219 <View style={styles.credentialsContainer}> 220 <Button title="Sign out" onPress={this.signOut} /> 221 <Button title="Refresh" onPress={this.refresh} /> 222 </View> 223 } 224 </ScrollView> 225 ); 226 } 227} 228 229const styles = StyleSheet.create({ 230 container: { 231 flex: 1, 232 padding: 10, 233 }, 234 scrollViewContainer: { 235 justifyContent: 'flex-start', 236 alignItems: 'center', 237 paddingBottom: 15, 238 }, 239 credentialStateContainer: { 240 padding: 10, 241 }, 242 credentialStateText: { 243 fontSize: 16, 244 fontWeight: 'bold', 245 }, 246 buttonContainer: { 247 justifyContent: 'center', 248 alignItems: 'center', 249 marginBottom: 20, 250 }, 251 controlsContainer: { 252 marginBottom: 10, 253 justifyContent: 'center', 254 alignItems: 'stretch', 255 }, 256 controlsButtonsContainer: { 257 flexDirection: 'row', 258 justifyContent: 'center', 259 }, 260 controlsText: { 261 fontSize: 16, 262 textAlign: 'center', 263 }, 264 credentialsContainer: { 265 justifyContent: 'center', 266 alignItems: 'center', 267 }, 268}); 269