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