diff --git a/src/App.native.tsx b/src/App.native.tsx
index 4037eedd27..0b46da9ddf 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -29,6 +29,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {Provider as EmailVerificationProvider} from '#/state/email-verification'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
@@ -155,18 +156,20 @@ function InnerApp() {
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 2897aa3f2f..dfa1e7480f 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -18,6 +18,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {Provider as EmailVerificationProvider} from '#/state/email-verification'
import {listenSessionDropped} from '#/state/events'
import {
beginResolveGeolocation,
@@ -136,12 +137,14 @@ function InnerApp() {
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index 2daf4a2683..985d58eecc 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -3,6 +3,7 @@ import {type AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
+import {STALE} from '#/state/queries'
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
import {
usePreferencesQuery,
@@ -56,7 +57,10 @@ export function useNuxDialogContext() {
export function NuxDialogs() {
const {currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
- const {data: profile} = useProfileQuery({did: currentAccount?.did})
+ const {data: profile} = useProfileQuery({
+ did: currentAccount?.did,
+ staleTime: STALE.INFINITY, // createdAt isn't gonna change
+ })
const onboardingActive = useOnboardingState().isActive
const isLoading =
diff --git a/src/lib/hooks/useEmail.ts b/src/lib/hooks/useEmail.ts
deleted file mode 100644
index 1e940ec964..0000000000
--- a/src/lib/hooks/useEmail.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import {STALE} from '#/state/queries'
-import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useSession} from '#/state/session'
-import {BSKY_SERVICE} from '../constants'
-import {getHostnameFromUrl} from '../strings/url-helpers'
-
-export function useEmail() {
- const {currentAccount} = useSession()
-
- const {data: serviceConfig} = useServiceConfigQuery()
- const {data: profile} = useProfileQuery({
- did: currentAccount?.did,
- staleTime: STALE.INFINITY,
- })
-
- const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
-
- // Date set for 11 AM PST on the 18th of November
- const isNewEnough =
- !!profile?.createdAt &&
- Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
-
- const isSelfHost =
- currentAccount &&
- getHostnameFromUrl(currentAccount.service) !==
- getHostnameFromUrl(BSKY_SERVICE)
-
- const needsEmailVerification =
- !isSelfHost &&
- checkEmailConfirmed &&
- !currentAccount?.emailConfirmed &&
- isNewEnough
-
- return {needsEmailVerification}
-}
diff --git a/src/lib/hooks/useOpenComposer.tsx b/src/lib/hooks/useOpenComposer.tsx
index 50c04d1e1f..789fa1f879 100644
--- a/src/lib/hooks/useOpenComposer.tsx
+++ b/src/lib/hooks/useOpenComposer.tsx
@@ -2,10 +2,10 @@ import {useMemo} from 'react'
import {Trans} from '@lingui/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
-import {useOpenComposer as rootUseOpenComposer} from '#/state/shell/composer'
+import {useOpenComposer as useRootOpenComposer} from '#/state/shell/composer'
export function useOpenComposer() {
- const {openComposer} = rootUseOpenComposer()
+ const {openComposer} = useRootOpenComposer()
const requireEmailVerification = useRequireEmailVerification()
return useMemo(() => {
return {
diff --git a/src/lib/hooks/useRequireEmailVerification.tsx b/src/lib/hooks/useRequireEmailVerification.tsx
index 26045847e0..2e5b339783 100644
--- a/src/lib/hooks/useRequireEmailVerification.tsx
+++ b/src/lib/hooks/useRequireEmailVerification.tsx
@@ -1,7 +1,7 @@
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
-import {useEmail} from '#/lib/hooks/useEmail'
+import {useEmail} from '#/state/email-verification'
import {useRequireAuth, useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index 7f3b53b94d..2e820cc124 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -15,7 +15,6 @@ import {
} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useEmail} from '#/lib/hooks/useEmail'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
@@ -24,6 +23,7 @@ import {
} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
+import {useEmail} from '#/state/email-verification'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
import {ConvoStatus} from '#/state/messages/convo/types'
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx
index 6cde1d4fec..87f22b2329 100644
--- a/src/screens/Messages/components/MessageInput.tsx
+++ b/src/screens/Messages/components/MessageInput.tsx
@@ -18,8 +18,8 @@ import Graphemer from 'graphemer'
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
-import {useEmail} from '#/lib/hooks/useEmail'
import {isIOS, isWeb} from '#/platform/detection'
+import {useEmail} from '#/state/email-verification'
import {
useMessageDraft,
useSaveMessageDraft,
diff --git a/src/screens/Messages/components/RequestButtons.tsx b/src/screens/Messages/components/RequestButtons.tsx
index 3490bec0da..560888552b 100644
--- a/src/screens/Messages/components/RequestButtons.tsx
+++ b/src/screens/Messages/components/RequestButtons.tsx
@@ -5,9 +5,9 @@ import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useEmail} from '#/lib/hooks/useEmail'
import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useEmail} from '#/state/email-verification'
import {useAcceptConversation} from '#/state/queries/messages/accept-conversation'
import {precacheConvoQuery} from '#/state/queries/messages/conversation'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
diff --git a/src/state/email-verification.tsx b/src/state/email-verification.tsx
new file mode 100644
index 0000000000..0528d59ea1
--- /dev/null
+++ b/src/state/email-verification.tsx
@@ -0,0 +1,64 @@
+import {createContext, useContext, useMemo} from 'react'
+
+import {BSKY_SERVICE} from '#/lib/constants'
+import {getHostnameFromUrl} from '#/lib/strings/url-helpers'
+import {STALE} from '#/state/queries'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useCheckEmailConfirmed} from '#/state/service-config'
+import {useSession} from '#/state/session'
+
+type EmailVerificationContext = {
+ needsEmailVerification: boolean
+}
+
+const EmailVerificationContext = createContext(
+ null,
+)
+EmailVerificationContext.displayName = 'EmailVerificationContext'
+
+export function Provider({children}: {children: React.ReactNode}) {
+ const {currentAccount} = useSession()
+
+ const {data: profile} = useProfileQuery({
+ did: currentAccount?.did,
+ staleTime: STALE.INFINITY,
+ })
+
+ const checkEmailConfirmed = useCheckEmailConfirmed()
+
+ // Date set for 11 AM PST on the 18th of November
+ const isNewEnough =
+ !!profile?.createdAt &&
+ Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
+
+ const isSelfHost =
+ currentAccount &&
+ getHostnameFromUrl(currentAccount.service) !==
+ getHostnameFromUrl(BSKY_SERVICE)
+
+ const needsEmailVerification =
+ !isSelfHost &&
+ checkEmailConfirmed &&
+ !currentAccount?.emailConfirmed &&
+ isNewEnough
+
+ const value = useMemo(
+ () => ({needsEmailVerification}),
+ [needsEmailVerification],
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+Provider.displayName = 'EmailVerificationProvider'
+
+export function useEmail() {
+ const ctx = useContext(EmailVerificationContext)
+ if (!ctx) {
+ throw new Error('useEmail must be used within a EmailVerificationProvider')
+ }
+ return ctx
+}
diff --git a/src/state/queries/email-verification-required.ts b/src/state/queries/email-verification-required.ts
deleted file mode 100644
index 94ff5cbc68..0000000000
--- a/src/state/queries/email-verification-required.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import {useQuery} from '@tanstack/react-query'
-
-interface ServiceConfig {
- checkEmailConfirmed: boolean
-}
-
-export function useServiceConfigQuery() {
- return useQuery({
- queryKey: ['service-config'],
- queryFn: async () => {
- const res = await fetch(
- 'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig',
- )
- if (!res.ok) {
- return {
- checkEmailConfirmed: false,
- }
- }
-
- const json = await res.json()
- return json as ServiceConfig
- },
- staleTime: 5 * 60 * 1000,
- })
-}
diff --git a/src/state/service-config.tsx b/src/state/service-config.tsx
index 94be5b445c..242022b60d 100644
--- a/src/state/service-config.tsx
+++ b/src/state/service-config.tsx
@@ -21,6 +21,8 @@ TrendingContext.displayName = 'TrendingContext'
const LiveNowContext = createContext(null)
LiveNowContext.displayName = 'LiveNowContext'
+const CheckEmailConfirmedContext = createContext(null)
+
export function Provider({children}: {children: React.ReactNode}) {
const langPrefs = useLanguagePrefs()
const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery()
@@ -61,10 +63,16 @@ export function Provider({children}: {children: React.ReactNode}) {
const liveNow = useMemo(() => config?.liveNow ?? [], [config])
+ // probably true, so default to true when loading
+ // if the call fails, the query will set it to false for us
+ const checkEmailConfirmed = config?.checkEmailConfirmed ?? true
+
return (
- {children}
+
+ {children}
+
)
@@ -78,7 +86,7 @@ export function useLiveNowConfig() {
const ctx = useContext(LiveNowContext)
if (!ctx) {
throw new Error(
- 'useLiveNowConfig must be used within a LiveNowConfigProvider',
+ 'useLiveNowConfig must be used within a ServiceConfigManager',
)
}
return ctx
@@ -88,3 +96,13 @@ export function useCanGoLive(did?: string) {
const config = useLiveNowConfig()
return !!config.find(cfg => cfg.did === did)
}
+
+export function useCheckEmailConfirmed() {
+ const ctx = useContext(CheckEmailConfirmedContext)
+ if (ctx === null) {
+ throw new Error(
+ 'useCheckEmailConfirmed must be used within a ServiceConfigManager',
+ )
+ }
+ return ctx
+}