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