about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/App.native.tsx98
-rw-r--r--src/App.web.tsx5
-rw-r--r--src/components/intents/IntentDialogs.tsx37
-rw-r--r--src/components/intents/VerifyEmailIntentDialog.tsx140
-rw-r--r--src/lib/hooks/useIntentHandler.ts34
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],
+  )
+}