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