1import { H2, H4 } from '@expo/html-elements';
2import * as AuthSession from 'expo-auth-session';
3import { useAuthRequest } from 'expo-auth-session';
4import * as FacebookAuthSession from 'expo-auth-session/providers/facebook';
5import * as GoogleAuthSession from 'expo-auth-session/providers/google';
6import Constants, { ExecutionEnvironment } from 'expo-constants';
7import { maybeCompleteAuthSession } from 'expo-web-browser';
8import React from 'react';
9import { Platform, ScrollView, View } from 'react-native';
10
11import { AuthSection } from './AuthResult';
12import { getGUID } from '../../api/guid';
13import TitledPicker from '../../components/TitledPicker';
14import TitledSwitch from '../../components/TitledSwitch';
15
16maybeCompleteAuthSession();
17
18const isInClient = Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
19
20const languages = [
21  { key: 'en', value: 'English' },
22  { key: 'pl', value: 'Polish' },
23  { key: 'nl', value: 'Dutch' },
24  { key: 'fi', value: 'Finnish' },
25];
26
27const PROJECT_NAME_FOR_PROXY = '@community/native-component-list';
28
29export default function AuthSessionScreen() {
30  const [usePKCE, setPKCE] = React.useState<boolean>(true);
31  const [prompt, setSwitch] = React.useState<undefined | AuthSession.Prompt>(undefined);
32  const [language, setLanguage] = React.useState<any>(languages[0].key);
33
34  return (
35    <View style={{ flex: 1, alignItems: 'center' }}>
36      <ScrollView
37        contentContainerStyle={{
38          paddingHorizontal: 12,
39          ...Platform.select({
40            default: {
41              maxWidth: '100%',
42            },
43            web: {
44              maxWidth: 640,
45            },
46          }),
47        }}>
48        <View style={{ marginBottom: 8 }}>
49          <H2>Settings</H2>
50          <TitledSwitch
51            title="Switch Accounts"
52            value={!!prompt}
53            setValue={(value) => setSwitch(value ? AuthSession.Prompt.SelectAccount : undefined)}
54          />
55          <TitledSwitch title="Use PKCE" value={usePKCE} setValue={setPKCE} />
56          <TitledPicker
57            items={languages}
58            title="Language"
59            value={language}
60            setValue={setLanguage}
61          />
62          <H4>ID: {PROJECT_NAME_FOR_PROXY}</H4>
63        </View>
64        <H2>Services</H2>
65        <AuthSessionProviders prompt={prompt} usePKCE={usePKCE} language={language} />
66      </ScrollView>
67    </View>
68  );
69}
70
71AuthSessionScreen.navigationOptions = {
72  title: 'AuthSession',
73};
74
75function AuthSessionProviders(props: {
76  usePKCE: boolean;
77  prompt?: AuthSession.Prompt;
78  language: string;
79}) {
80  const { usePKCE, prompt, language } = props;
81
82  const redirectUri = AuthSession.makeRedirectUri({
83    path: 'redirect',
84    preferLocalhost: Platform.select({ android: false, default: true }),
85  });
86
87  const options = {
88    usePKCE,
89    prompt,
90    redirectUri,
91    language,
92  };
93
94  const providers = [
95    Google,
96    GoogleFirebase,
97    Facebook,
98    Imgur,
99    Spotify,
100    Strava,
101    Twitch,
102    Dropbox,
103    Reddit,
104    Github,
105    Coinbase,
106    Uber,
107    Slack,
108    FitBit,
109    Okta,
110    Identity,
111    // Azure,
112  ];
113  return (
114    <View style={{ flex: 1 }}>
115      {providers.map((Provider, index) => (
116        <Provider key={`-${index}`} {...options} />
117      ))}
118    </View>
119  );
120}
121
122function Google({ prompt, language, usePKCE }: any) {
123  const [request, result, promptAsync] = GoogleAuthSession.useAuthRequest(
124    {
125      language,
126      expoClientId: '629683148649-qevd4mfvh06q14i4nl453r62sgd1p85d.apps.googleusercontent.com',
127      clientId: `${getGUID()}.apps.googleusercontent.com`,
128      selectAccount: !!prompt,
129      usePKCE,
130    },
131    {
132      path: 'redirect',
133      preferLocalhost: true,
134    }
135  );
136
137  React.useEffect(() => {
138    if (request && result?.type === 'success') {
139      console.log('Result: ', result.authentication);
140    }
141  }, [result]);
142
143  return <AuthSection request={request} title="google" result={result} promptAsync={promptAsync} />;
144}
145
146function GoogleFirebase({ prompt, language, usePKCE }: any) {
147  const [request, result, promptAsync] = GoogleAuthSession.useIdTokenAuthRequest(
148    {
149      language,
150      expoClientId: '629683148649-qevd4mfvh06q14i4nl453r62sgd1p85d.apps.googleusercontent.com',
151      clientId: `${getGUID()}.apps.googleusercontent.com`,
152      selectAccount: !!prompt,
153      usePKCE,
154    },
155    {
156      path: 'redirect',
157      preferLocalhost: true,
158    }
159  );
160
161  React.useEffect(() => {
162    if (request && result?.type === 'success') {
163      console.log('Result:', result.params.id_token);
164    }
165  }, [result]);
166
167  return (
168    <AuthSection
169      request={request}
170      title="google_firebase"
171      result={result}
172      promptAsync={promptAsync}
173    />
174  );
175}
176
177// Couldn't get this working. API is really confusing.
178// function Azure({ useProxy, prompt, usePKCE }: any) {
179//   const redirectUri = AuthSession.makeRedirectUri({
180//     path: 'redirect',
181//     preferLocalhost: true,
182//     useProxy,
183//     native: Platform.select<string>({
184//       ios: 'msauth.dev.expo.Payments://auth',
185//       android: 'msauth://dev.expo.payments/sZs4aocytGUGvP1%2BgFAavaPMPN0%3D',
186//     }),
187//   });
188
189//   // 'https://login.microsoftonline.com/your-tenant-id/v2.0',
190//   const discovery = AuthSession.useAutoDiscovery(
191//     'https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0'
192//   );
193//   const [request, result, promptAsync] = useAuthRequest(
194//     // config
195//     {
196//       clientId: '96891596-721b-4ae1-8e67-674809373165',
197//       redirectUri,
198//       prompt,
199//       extraParams: {
200//         domain_hint: 'live.com',
201//       },
202//       // redirectUri: 'msauth.{bundleId}://auth',
203//       scopes: ['openid', 'profile', 'email', 'offline_access'],
204//       usePKCE,
205//     },
206//     // discovery
207//     discovery
208//   );
209
210//   return (
211//     <AuthSection
212//       title="azure"
213//       disabled={isInClient}
214//       request={request}
215//       result={result}
216//       promptAsync={promptAsync}
217//       useProxy={useProxy}
218//     />
219//   );
220// }
221
222function Okta({ redirectUri, usePKCE }: any) {
223  const discovery = AuthSession.useAutoDiscovery('https://dev-720924.okta.com/oauth2/default');
224  const [request, result, promptAsync] = useAuthRequest(
225    {
226      clientId: '0oa4su9fhp4F2F4Eg4x6',
227      redirectUri,
228      scopes: ['openid', 'profile'],
229      usePKCE,
230    },
231    discovery
232  );
233
234  return <AuthSection title="okta" request={request} result={result} promptAsync={promptAsync} />;
235}
236
237// Reddit only allows one redirect uri per client Id
238// We'll only support bare, and proxy in this example
239// If the redirect is invalid with http instead of https on web, then the provider
240// will let you authenticate but it will redirect with no data and the page will appear broken.
241function Reddit({ redirectUri, prompt, usePKCE }: any) {
242  let clientId: string;
243
244  if (isInClient) {
245    clientId = 'CPc_adCUQGt9TA';
246  } else {
247    if (Platform.OS === 'web') {
248      // web apps with uri scheme `https://localhost:19006`
249      clientId = '9k_oYNO97ly-5w';
250    } else {
251      // Native bare apps with uri scheme `bareexpo`
252      clientId = '2OFsAA7h63LQJQ';
253    }
254  }
255
256  const [request, result, promptAsync] = useAuthRequest(
257    {
258      clientId,
259      clientSecret: '',
260      redirectUri,
261      prompt,
262      scopes: ['identity'],
263      usePKCE,
264    },
265    {
266      authorizationEndpoint: 'https://www.reddit.com/api/v1/authorize.compact',
267      tokenEndpoint: 'https://www.reddit.com/api/v1/access_token',
268    }
269  );
270
271  return <AuthSection title="reddit" request={request} result={result} promptAsync={promptAsync} />;
272}
273
274// Imgur Docs https://api.imgur.com/oauth2
275// Create app https://api.imgur.com/oauth2/addclient
276function Imgur({ redirectUri, prompt, usePKCE }: any) {
277  let clientId: string;
278
279  if (isInClient) {
280    // Normalize the host to `localhost` for other testers
281    // Expects: exp://127.0.0.1:19000/--/redirect
282    clientId = '7ab2f3cc75427a0';
283  } else {
284    if (Platform.OS === 'web') {
285      // web apps with uri scheme `https://localhost:19006`
286      clientId = '181b22d17a3743e';
287    } else {
288      // Native bare apps with uri scheme `bareexpo`
289      clientId = 'd839d91135a16cc';
290    }
291  }
292
293  const [request, result, promptAsync] = useAuthRequest(
294    {
295      clientId,
296      responseType: AuthSession.ResponseType.Token,
297      redirectUri,
298      scopes: [],
299      usePKCE,
300      prompt,
301    },
302    // discovery
303    {
304      authorizationEndpoint: 'https://api.imgur.com/oauth2/authorize',
305      tokenEndpoint: 'https://api.imgur.com/oauth2/token',
306    }
307  );
308
309  return (
310    <AuthSection
311      title="imgur"
312      request={request}
313      result={result}
314      promptAsync={() =>
315        promptAsync({
316          windowFeatures: { width: 500, height: 750 },
317        })
318      }
319    />
320  );
321}
322
323// TODO: Add button to test using an invalid redirect URI. This is a good example of AuthError.
324// Works for all platforms
325function Github({ redirectUri, prompt, usePKCE }: any) {
326  let clientId: string;
327
328  if (isInClient) {
329    clientId = '7eb5d82d8f160a434564';
330  } else {
331    if (Platform.OS === 'web') {
332      // web apps
333      clientId = 'fd9b07204f9d325e8f0e';
334    } else {
335      // Native bare apps with uri scheme `bareexpo`
336      clientId = '498f1fae3ae16f066f34';
337    }
338  }
339
340  const [request, result, promptAsync] = useAuthRequest(
341    {
342      clientId,
343      redirectUri,
344      scopes: ['identity'],
345      usePKCE,
346      prompt,
347    },
348    // discovery
349    {
350      authorizationEndpoint: 'https://github.com/login/oauth/authorize',
351      tokenEndpoint: 'https://github.com/login/oauth/access_token',
352      revocationEndpoint:
353        'https://github.com/settings/connections/applications/d529db5d7d81c2d50adf',
354    }
355  );
356
357  return (
358    <AuthSection
359      title="github"
360      request={request}
361      result={result}
362      promptAsync={() =>
363        promptAsync({
364          windowFeatures: { width: 500, height: 750 },
365        })
366      }
367    />
368  );
369}
370
371// I couldn't get access to any scopes
372// This never returns to the app after authenticating
373function Uber({ redirectUri, prompt, usePKCE }: any) {
374  // https://developer.uber.com/docs/riders/guides/authentication/introduction
375  const [request, result, promptAsync] = useAuthRequest(
376    {
377      clientId: 'kTpT4xf8afVxifoWjx5Nhn-IFamZKp2x',
378      redirectUri,
379      scopes: [],
380      usePKCE,
381      prompt,
382      // Enable to test invalid_scope error
383      // scopes: ['invalid'],
384    },
385    // discovery
386    {
387      authorizationEndpoint: 'https://login.uber.com/oauth/v2/authorize',
388      tokenEndpoint: 'https://login.uber.com/oauth/v2/token',
389      revocationEndpoint: 'https://login.uber.com/oauth/v2/revoke',
390    }
391  );
392
393  return <AuthSection title="uber" request={request} result={result} promptAsync={promptAsync} />;
394}
395
396// https://dev.fitbit.com/apps/new
397// Easy to setup
398// Only allows one redirect URI per app (clientId)
399// Refresh doesn't seem to return a new access token :[
400function FitBit({ redirectUri, prompt, usePKCE }: any) {
401  let clientId: string;
402
403  if (isInClient) {
404    // Client without proxy
405    clientId = '22BNXX';
406  } else {
407    if (Platform.OS === 'web') {
408      // web apps with uri scheme `https://localhost:19006`
409      clientId = '22BNXQ';
410    } else {
411      // Native bare apps with uri scheme `bareexpo`
412      clientId = '22BGYS';
413    }
414  }
415
416  const [request, result, promptAsync] = useAuthRequest(
417    {
418      clientId,
419      redirectUri,
420      scopes: ['activity', 'sleep'],
421      prompt,
422      usePKCE,
423    },
424    // discovery
425    {
426      authorizationEndpoint: 'https://www.fitbit.com/oauth2/authorize',
427      tokenEndpoint: 'https://api.fitbit.com/oauth2/token',
428      revocationEndpoint: 'https://api.fitbit.com/oauth2/revoke',
429    }
430  );
431
432  return <AuthSection title="fitbit" request={request} result={result} promptAsync={promptAsync} />;
433}
434
435function Facebook({ usePKCE, language }: any) {
436  const [request, result, promptAsync] = FacebookAuthSession.useAuthRequest(
437    {
438      clientId: '145668956753819',
439      usePKCE,
440      language,
441      scopes: ['user_likes'],
442    },
443    {
444      path: 'redirect',
445      preferLocalhost: true,
446    }
447  );
448  // Add fetch user example
449
450  return (
451    <AuthSection title="facebook" request={request} result={result} promptAsync={promptAsync} />
452  );
453}
454
455function Slack({ redirectUri, prompt, usePKCE }: any) {
456  // https://api.slack.com/apps
457  // After you created an app, navigate to [Features > OAuth & Permissions]
458  // - Add a redirect URI Under [Redirect URLs]
459  // - Under [Scopes] add the scopes you want to request from the user
460  // Next go to [App Credentials] to get your client ID and client secret
461  // No refresh token or expiration is returned, assume the token lasts forever.
462  const [request, result, promptAsync] = useAuthRequest(
463    // config
464    {
465      clientId: '58692702102.1023025401076',
466      redirectUri,
467      scopes: ['emoji:read'],
468      prompt,
469      usePKCE,
470    },
471    // discovery
472    {
473      authorizationEndpoint: 'https://slack.com/oauth/authorize',
474      tokenEndpoint: 'https://slack.com/api/oauth.access',
475    }
476  );
477
478  return <AuthSection title="slack" request={request} result={result} promptAsync={promptAsync} />;
479}
480
481// Works on all platforms
482function Spotify({ redirectUri, prompt, usePKCE }: any) {
483  const [request, result, promptAsync] = useAuthRequest(
484    {
485      clientId: 'a946eadd241244fd88d0a4f3d7dea22f',
486      redirectUri,
487      scopes: ['user-read-email', 'playlist-modify-public', 'user-read-private'],
488      usePKCE,
489      extraParams: {
490        show_dialog: 'false',
491      },
492      prompt,
493    },
494    // discovery
495    {
496      authorizationEndpoint: 'https://accounts.spotify.com/authorize',
497      tokenEndpoint: 'https://accounts.spotify.com/api/token',
498    }
499  );
500
501  return (
502    <AuthSection title="spotify" request={request} result={result} promptAsync={promptAsync} />
503  );
504}
505
506function Strava({ redirectUri, prompt, usePKCE }: any) {
507  const discovery = {
508    authorizationEndpoint: 'https://www.strava.com/oauth/mobile/authorize',
509    tokenEndpoint: 'https://www.strava.com/oauth/token',
510  };
511  const [request, result, promptAsync] = useAuthRequest(
512    {
513      clientId: '51935',
514      redirectUri,
515      scopes: ['activity:read_all'],
516      usePKCE,
517      prompt,
518    },
519    discovery
520  );
521
522  React.useEffect(() => {
523    if (request && result?.type === 'success' && result.params.code) {
524      AuthSession.exchangeCodeAsync(
525        {
526          clientId: request?.clientId,
527          redirectUri,
528          code: result.params.code,
529          extraParams: {
530            // You must use the extraParams variation of clientSecret.
531            client_secret: `...`,
532          },
533        },
534        discovery
535      ).then((result) => {
536        console.log('RES: ', result);
537      });
538    }
539  }, [result]);
540
541  return <AuthSection title="strava" request={request} result={result} promptAsync={promptAsync} />;
542}
543
544// Works on all platforms
545function Identity({ redirectUri, prompt }: any) {
546  const discovery = AuthSession.useAutoDiscovery('https://demo.identityserver.io');
547
548  const [request, result, promptAsync] = useAuthRequest(
549    {
550      clientId: 'native.code',
551      redirectUri,
552      prompt,
553      scopes: ['openid', 'profile', 'email', 'offline_access'],
554    },
555    discovery
556  );
557
558  return (
559    <AuthSection title="identity4" request={request} result={result} promptAsync={promptAsync} />
560  );
561}
562
563// Doesn't work with proxy
564function Coinbase({ redirectUri, prompt, usePKCE }: any) {
565  const [request, result, promptAsync] = useAuthRequest(
566    {
567      clientId: '13b2bc8d9114b1cb6d0132cf60c162bc9c2d5ec29c2599003556edf81cc5db4e',
568      redirectUri,
569      prompt,
570      usePKCE,
571      scopes: ['wallet:accounts:read'],
572    },
573    // discovery
574    {
575      authorizationEndpoint: 'https://www.coinbase.com/oauth/authorize',
576      tokenEndpoint: 'https://api.coinbase.com/oauth/token',
577      revocationEndpoint: 'https://api.coinbase.com/oauth/revoke',
578    }
579  );
580
581  return (
582    <AuthSection title="coinbase" request={request} result={result} promptAsync={promptAsync} />
583  );
584}
585
586function Dropbox({ redirectUri, prompt, usePKCE }: any) {
587  const [request, result, promptAsync] = useAuthRequest(
588    {
589      clientId: 'pjvyj0c5kxxrsfs',
590      redirectUri,
591      prompt,
592      usePKCE,
593      scopes: [],
594      responseType: AuthSession.ResponseType.Token,
595    },
596    // discovery
597    {
598      authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize',
599      tokenEndpoint: 'https://www.dropbox.com/oauth2/token',
600    }
601  );
602
603  return (
604    <AuthSection
605      disabled={usePKCE}
606      title="dropbox"
607      request={request}
608      result={result}
609      promptAsync={promptAsync}
610    />
611  );
612}
613
614function Twitch({ redirectUri, prompt, usePKCE }: any) {
615  const [request, result, promptAsync] = useAuthRequest(
616    {
617      clientId: 'r7jomrc4hiz5wm1wgdzmwr1ccb454h',
618      redirectUri,
619      prompt,
620      scopes: ['openid', 'user_read', 'analytics:read:games'],
621      usePKCE,
622    },
623    {
624      authorizationEndpoint: 'https://id.twitch.tv/oauth2/authorize',
625      tokenEndpoint: 'https://id.twitch.tv/oauth2/token',
626      revocationEndpoint: 'https://id.twitch.tv/oauth2/revoke',
627    }
628  );
629
630  return <AuthSection title="twitch" request={request} result={result} promptAsync={promptAsync} />;
631}
632