diff options
author | Hailey <me@haileyok.com> | 2024-09-07 11:54:39 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-07 11:54:39 -0700 |
commit | 2842f661db8aeb0154dd362a6b61b3edb808bef9 (patch) | |
tree | 6de50442adac91debfba0fd89dcbef44488dd43a | |
parent | 45a719b256173f98b20457cc80b4288e84f1c33f (diff) | |
download | voidsky-2842f661db8aeb0154dd362a6b61b3edb808bef9.tar.zst |
Add intent for verifying email (#5120)
-rw-r--r-- | src/App.native.tsx | 98 | ||||
-rw-r--r-- | src/App.web.tsx | 5 | ||||
-rw-r--r-- | src/components/intents/IntentDialogs.tsx | 37 | ||||
-rw-r--r-- | src/components/intents/VerifyEmailIntentDialog.tsx | 140 | ||||
-rw-r--r-- | src/lib/hooks/useIntentHandler.ts | 34 |
5 files changed, 264 insertions, 50 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 609d316d4..780d4058f 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -58,6 +58,7 @@ import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' +import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -105,52 +106,50 @@ function InnerApp() { }, [_]) return ( - <SafeAreaProvider initialMetrics={initialWindowMetrics}> - <Alf theme={theme}> - <ThemeProvider theme={theme}> - <Splash isReady={isReady && hasCheckedReferrer}> - <ActiveVideoProvider> - <RootSiblingParent> - <React.Fragment - // Resets the entire tree below when it changes: - key={currentAccount?.did}> - <QueryProvider currentDid={currentAccount?.did}> - <StatsigProvider> - <MessagesProvider> - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - <LabelDefsProvider> - <ModerationOptsProvider> - <LoggedOutViewProvider> - <SelectedFeedProvider> - <HiddenRepliesProvider> - <UnreadNotifsProvider> - <BackgroundNotificationPreferencesProvider> - <MutedThreadsProvider> - <ProgressGuideProvider> - <GestureHandlerRootView - style={s.h100pct}> - <TestCtrls /> - <Shell /> - </GestureHandlerRootView> - </ProgressGuideProvider> - </MutedThreadsProvider> - </BackgroundNotificationPreferencesProvider> - </UnreadNotifsProvider> - </HiddenRepliesProvider> - </SelectedFeedProvider> - </LoggedOutViewProvider> - </ModerationOptsProvider> - </LabelDefsProvider> - </MessagesProvider> - </StatsigProvider> - </QueryProvider> - </React.Fragment> - </RootSiblingParent> - </ActiveVideoProvider> - </Splash> - </ThemeProvider> - </Alf> - </SafeAreaProvider> + <Alf theme={theme}> + <ThemeProvider theme={theme}> + <Splash isReady={isReady && hasCheckedReferrer}> + <ActiveVideoProvider> + <RootSiblingParent> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <QueryProvider currentDid={currentAccount?.did}> + <StatsigProvider> + <MessagesProvider> + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + <LabelDefsProvider> + <ModerationOptsProvider> + <LoggedOutViewProvider> + <SelectedFeedProvider> + <HiddenRepliesProvider> + <UnreadNotifsProvider> + <BackgroundNotificationPreferencesProvider> + <MutedThreadsProvider> + <ProgressGuideProvider> + <GestureHandlerRootView + style={s.h100pct}> + <TestCtrls /> + <Shell /> + </GestureHandlerRootView> + </ProgressGuideProvider> + </MutedThreadsProvider> + </BackgroundNotificationPreferencesProvider> + </UnreadNotifsProvider> + </HiddenRepliesProvider> + </SelectedFeedProvider> + </LoggedOutViewProvider> + </ModerationOptsProvider> + </LabelDefsProvider> + </MessagesProvider> + </StatsigProvider> + </QueryProvider> + </React.Fragment> + </RootSiblingParent> + </ActiveVideoProvider> + </Splash> + </ThemeProvider> + </Alf> ) } @@ -184,7 +183,12 @@ function App() { <LightboxStateProvider> <PortalProvider> <StarterPackProvider> - <InnerApp /> + <SafeAreaProvider + initialMetrics={initialWindowMetrics}> + <IntentDialogProvider> + <InnerApp /> + </IntentDialogProvider> + </SafeAreaProvider> </StarterPackProvider> </PortalProvider> </LightboxStateProvider> diff --git a/src/App.web.tsx b/src/App.web.tsx index 8531dc88d..3017a3a26 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -47,6 +47,7 @@ import {Shell} from '#/view/shell/index' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' +import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -162,7 +163,9 @@ function App() { <LightboxStateProvider> <PortalProvider> <StarterPackProvider> - <InnerApp /> + <IntentDialogProvider> + <InnerApp /> + </IntentDialogProvider> </StarterPackProvider> </PortalProvider> </LightboxStateProvider> diff --git a/src/components/intents/IntentDialogs.tsx b/src/components/intents/IntentDialogs.tsx new file mode 100644 index 000000000..244850370 --- /dev/null +++ b/src/components/intents/IntentDialogs.tsx @@ -0,0 +1,37 @@ +import React from 'react' + +import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' +import {VerifyEmailIntentDialog} from '#/components/intents/VerifyEmailIntentDialog' + +interface Context { + verifyEmailDialogControl: DialogControlProps + verifyEmailState: {code: string} | undefined + setVerifyEmailState: (state: {code: string} | undefined) => void +} + +const Context = React.createContext({} as Context) +export const useIntentDialogs = () => React.useContext(Context) + +export function Provider({children}: {children: React.ReactNode}) { + const verifyEmailDialogControl = Dialog.useDialogControl() + const [verifyEmailState, setVerifyEmailState] = React.useState< + {code: string} | undefined + >() + + const value = React.useMemo( + () => ({ + verifyEmailDialogControl, + verifyEmailState, + setVerifyEmailState, + }), + [verifyEmailDialogControl, verifyEmailState, setVerifyEmailState], + ) + + return ( + <Context.Provider value={value}> + {children} + <VerifyEmailIntentDialog /> + </Context.Provider> + ) +} diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx new file mode 100644 index 000000000..4dca8bd90 --- /dev/null +++ b/src/components/intents/VerifyEmailIntentDialog.tsx @@ -0,0 +1,140 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useAgent, useSession} from 'state/session' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export function VerifyEmailIntentDialog() { + const {verifyEmailDialogControl: control} = useIntentDialogs() + + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + <Inner control={control} /> + </Dialog.Outer> + ) +} + +function Inner({control}: {control: DialogControlProps}) { + const {_} = useLingui() + const {verifyEmailState: state} = useIntentDialogs() + const [status, setStatus] = React.useState< + 'loading' | 'success' | 'failure' | 'resent' + >('loading') + const [sending, setSending] = React.useState(false) + const agent = useAgent() + const {currentAccount} = useSession() + + React.useEffect(() => { + ;(async () => { + if (!state?.code) { + return + } + try { + await agent.com.atproto.server.confirmEmail({ + email: (currentAccount?.email || '').trim(), + token: state.code.trim(), + }) + setStatus('success') + } catch (e) { + setStatus('failure') + } + })() + }, [agent.com.atproto.server, currentAccount?.email, state?.code]) + + const onPressResendEmail = async () => { + setSending(true) + await agent.com.atproto.server.requestEmailConfirmation() + setSending(false) + setStatus('resent') + } + + return ( + <Dialog.ScrollableInner label={_(msg`Verify email dialog`)}> + <Dialog.Close /> + <View style={[a.gap_xl]}> + {status === 'loading' ? ( + <View style={[a.py_2xl, a.align_center, a.justify_center]}> + <Loader size="xl" /> + </View> + ) : status === 'success' ? ( + <> + <Text style={[a.font_bold, a.text_2xl]}> + <Trans>Email Verified</Trans> + </Text> + <Text style={[a.text_md, a.leading_tight]}> + <Trans> + Thanks, you have successfully verified your email address. + </Trans> + </Text> + </> + ) : status === 'failure' ? ( + <> + <Text style={[a.font_bold, a.text_2xl]}> + <Trans>Invalid Verification Code</Trans> + </Text> + <Text style={[a.text_md, a.leading_tight]}> + <Trans> + The verification code you have provided is invalid. Please make + sure that you have used the correct verification link or request + a new one. + </Trans> + </Text> + </> + ) : ( + <> + <Text style={[a.font_bold, a.text_2xl]}> + <Trans>Email Resent</Trans> + </Text> + <Text style={[a.text_md, a.leading_tight]}> + <Trans> + We have sent another verification email to{' '} + <Text style={[a.text_md, a.font_bold]}> + {currentAccount?.email} + </Text> + . + </Trans> + </Text> + </> + )} + {status !== 'loading' ? ( + <View style={[a.w_full, a.flex_row, a.gap_sm, {marginLeft: 'auto'}]}> + <Button + label={_(msg`Close`)} + onPress={() => control.close()} + variant="solid" + color={status === 'failure' ? 'secondary' : 'primary'} + size="medium" + style={{marginLeft: 'auto'}}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + {status === 'failure' ? ( + <Button + label={_(msg`Resend Verification Email`)} + onPress={onPressResendEmail} + variant="solid" + color="primary" + size="medium" + disabled={sending}> + <ButtonText> + <Trans>Resend Email</Trans> + </ButtonText> + {sending ? <Loader size="sm" style={{color: 'white'}} /> : null} + </Button> + ) : null} + </View> + ) : null} + </View> + </Dialog.ScrollableInner> + ) +} diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 460df3753..8cccda48f 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -6,15 +6,17 @@ import {isNative} from 'platform/detection' import {useSession} from 'state/session' import {useComposerControls} from 'state/shell' import {useCloseAllActiveElements} from 'state/util' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {Referrer} from '../../../modules/expo-bluesky-swiss-army' -type IntentType = 'compose' +type IntentType = 'compose' | 'verify-email' const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ export function useIntentHandler() { const incomingUrl = Linking.useURL() const composeIntent = useComposeIntent() + const verifyEmailIntent = useVerifyEmailIntent() React.useEffect(() => { const handleIncomingURL = (url: string) => { @@ -51,12 +53,22 @@ export function useIntentHandler() { text: params.get('text'), imageUrisStr: params.get('imageUris'), }) + return + } + case 'verify-email': { + const code = params.get('code') + if (!code) return + verifyEmailIntent(code) + return + } + default: { + return } } } if (incomingUrl) handleIncomingURL(incomingUrl) - }, [incomingUrl, composeIntent]) + }, [incomingUrl, composeIntent, verifyEmailIntent]) } function useComposeIntent() { @@ -103,3 +115,21 @@ function useComposeIntent() { [hasSession, closeAllActiveElements, openComposer], ) } + +function useVerifyEmailIntent() { + const closeAllActiveElements = useCloseAllActiveElements() + const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = + useIntentDialogs() + return React.useCallback( + (code: string) => { + closeAllActiveElements() + setState({ + code, + }) + setTimeout(() => { + control.open() + }, 1000) + }, + [closeAllActiveElements, control, setState], + ) +} |