diff --git a/src/App.native.tsx b/src/App.native.tsx index 609d316d..780d4058 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 ( - - - - - - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } @@ -184,7 +183,12 @@ function App() { - + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 8531dc88..3017a3a2 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() { - + + + diff --git a/src/components/intents/IntentDialogs.tsx b/src/components/intents/IntentDialogs.tsx new file mode 100644 index 00000000..24485037 --- /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 ( + + {children} + + + ) +} diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx new file mode 100644 index 00000000..4dca8bd9 --- /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 ( + + + + + ) +} + +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 ( + + + + {status === 'loading' ? ( + + + + ) : status === 'success' ? ( + <> + + Email Verified + + + + Thanks, you have successfully verified your email address. + + + + ) : status === 'failure' ? ( + <> + + Invalid Verification Code + + + + The verification code you have provided is invalid. Please make + sure that you have used the correct verification link or request + a new one. + + + + ) : ( + <> + + Email Resent + + + + We have sent another verification email to{' '} + + {currentAccount?.email} + + . + + + + )} + {status !== 'loading' ? ( + + + {status === 'failure' ? ( + + ) : null} + + ) : null} + + + ) +} diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 460df375..8cccda48 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], + ) +}