Add email verification prompts throughout the app (#6174)

This commit is contained in:
Hailey
2024-11-12 11:18:53 -08:00
committed by GitHub
parent dd8d14e133
commit 427f3a8bd7
11 changed files with 265 additions and 46 deletions
@@ -14,6 +14,7 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useEmail} from '#/lib/hooks/useEmail'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
@@ -27,6 +28,7 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
interface SectionRef {
@@ -186,6 +188,9 @@ function Empty() {
const followersDialogControl = useDialogControl()
const errorDialogControl = useDialogControl()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const [isGenerating, setIsGenerating] = React.useState(false)
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
@@ -249,7 +254,13 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={confirmDialogControl.open}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
confirmDialogControl.open()
}
}}
style={{backgroundColor: 'transparent'}}>
<ButtonText style={{color: 'white'}}>
<Trans>Make one for me</Trans>
@@ -262,7 +273,13 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={() => navigation.navigate('StarterPackWizard')}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
navigation.navigate('StarterPackWizard')
}
}}
style={{
backgroundColor: 'white',
borderColor: 'white',
@@ -318,6 +335,12 @@ function Empty() {
onConfirm={generate}
confirmButtonCta={_(msg`Retry`)}
/>
<VerifyEmailDialog
reasonText={_(
msg`Before creating a starter pack, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</LinearGradientBackground>
)
}
+41 -21
View File
@@ -18,8 +18,14 @@ import {Text} from '#/components/Typography'
export function VerifyEmailDialog({
control,
onCloseWithoutVerifying,
onCloseAfterVerifying,
reasonText,
}: {
control: Dialog.DialogControlProps
onCloseWithoutVerifying?: () => void
onCloseAfterVerifying?: () => void
reasonText?: string
}) {
const agent = useAgent()
@@ -30,18 +36,24 @@ export function VerifyEmailDialog({
control={control}
onClose={async () => {
if (!didVerify) {
onCloseWithoutVerifying?.()
return
}
try {
await agent.resumeSession(agent.session!)
onCloseAfterVerifying?.()
} catch (e: unknown) {
logger.error(String(e))
return
}
}}>
<Dialog.Handle />
<Inner control={control} setDidVerify={setDidVerify} />
<Inner
control={control}
setDidVerify={setDidVerify}
reasonText={reasonText}
/>
</Dialog.Outer>
)
}
@@ -49,9 +61,11 @@ export function VerifyEmailDialog({
export function Inner({
control,
setDidVerify,
reasonText,
}: {
control: Dialog.DialogControlProps
setDidVerify: (value: boolean) => void
reasonText?: string
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
@@ -135,26 +149,32 @@ export function Inner({
<Text style={[a.text_md, a.leading_snug]}>
{currentStep === 'StepOne' ? (
<>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Need to change it?</Trans>
</InlineLinkText>
{!reasonText ? (
<>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Need to change it?</Trans>
</InlineLinkText>
</>
) : (
reasonText
)}
</>
) : (
uiStrings[currentStep].message
+41 -15
View File
@@ -3,14 +3,18 @@ import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
import {atoms as a, useTheme} from '#/alf'
import {ButtonIcon} from '#/components/Button'
import {Button, ButtonIcon} from '#/components/Button'
import {canBeMessaged} from '#/components/dms/util'
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
import {Link} from '#/components/Link'
import {useDialogControl} from '../Dialog'
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
export function MessageProfileButton({
profile,
@@ -19,15 +23,29 @@ export function MessageProfileButton({
}) {
const {_} = useLingui()
const t = useTheme()
const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
const onPress = React.useCallback(() => {
if (!convo?.id) {
return
}
if (needsEmailVerification) {
verifyEmailControl.open()
return
}
if (convo && !convo.lastMessage) {
logEvent('chat:create', {logContext: 'ProfileHeader'})
}
logEvent('chat:open', {logContext: 'ProfileHeader'})
}, [convo])
navigation.navigate('MessagesConversation', {conversation: convo.id})
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
if (isPending) {
// show pending state based on declaration
@@ -53,18 +71,26 @@ export function MessageProfileButton({
if (convo) {
return (
<Link
testID="dmBtn"
size="small"
color="secondary"
variant="solid"
shape="round"
label={_(msg`Message ${profile.handle}`)}
to={`/messages/${convo.id}`}
style={[a.justify_center]}
onPress={onPress}>
<ButtonIcon icon={Message} size="md" />
</Link>
<>
<Button
accessibilityRole="button"
testID="dmBtn"
size="small"
color="secondary"
variant="solid"
shape="round"
label={_(msg`Message ${profile.handle}`)}
style={[a.justify_center]}
onPress={onPress}>
<ButtonIcon icon={Message} size="md" />
</Button>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</>
)
} else {
return null
+19 -1
View File
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useEmail} from '#/lib/hooks/useEmail'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
@@ -9,6 +10,8 @@ import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {SearchablePeopleList} from './SearchablePeopleList'
@@ -21,6 +24,8 @@ export function NewChat({
}) {
const t = useTheme()
const {_} = useLingui()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
@@ -48,7 +53,13 @@ export function NewChat({
<>
<FAB
testID="newChatFAB"
onPress={control.open}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
control.open()
}
}}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
@@ -62,6 +73,13 @@ export function NewChat({
onSelectChat={onCreateChat}
/>
</Dialog.Outer>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</>
)
}
+19
View File
@@ -0,0 +1,19 @@
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
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 isSelfHost =
serviceConfig?.checkEmailConfirmed &&
currentAccount &&
getHostnameFromUrl(currentAccount.service) !==
getHostnameFromUrl(BSKY_SERVICE)
const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed
return {needsEmailVerification}
}
+24 -2
View File
@@ -4,10 +4,11 @@ import {useKeyboardController} from 'react-native-keyboard-controller'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useEmail} from '#/lib/hooks/useEmail'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
@@ -19,6 +20,8 @@ import {useSetMinimalShellMode} from '#/state/shell'
import {CenteredView} from '#/view/com/util/Views'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
import {Error} from '#/components/Error'
@@ -161,8 +164,12 @@ function InnerReady({
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
}) {
const {_} = useLingui()
const convoState = useConvo()
const navigation = useNavigation<NavigationProp>()
const recipient = useProfileShadow(recipientUnshadowed)
const verifyEmailControl = useDialogControl()
const {needsEmailVerification} = useEmail()
const moderation = React.useMemo(() => {
return moderateProfile(recipient, moderationOpts)
@@ -179,6 +186,12 @@ function InnerReady({
}
}, [moderation])
React.useEffect(() => {
if (needsEmailVerification) {
verifyEmailControl.open()
}
}, [needsEmailVerification, verifyEmailControl])
return (
<>
<MessagesListHeader
@@ -201,6 +214,15 @@ function InnerReady({
}
/>
)}
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
onCloseWithoutVerifying={() => {
navigation.navigate('Home')
}}
/>
</>
)
}
@@ -18,6 +18,7 @@ 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} from '#/platform/detection'
import {
useMessageDraft,
@@ -61,10 +62,15 @@ export function MessageInput({
const [message, setMessage] = React.useState(getDraft)
const inputRef = useAnimatedRef<TextInput>()
const {needsEmailVerification} = useEmail()
useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)
const onSubmit = React.useCallback(() => {
if (needsEmailVerification) {
return
}
if (!hasEmbed && message.trim() === '') {
return
}
@@ -84,6 +90,7 @@ export function MessageInput({
inputRef.current?.focus()
}, 100)
}, [
needsEmailVerification,
hasEmbed,
message,
clearDraft,
@@ -159,6 +166,7 @@ export function MessageInput({
ref={inputRef}
hitSlop={HITSLOP_10}
animatedProps={animatedProps}
editable={!needsEmailVerification}
/>
<Pressable
accessibilityRole="button"
@@ -171,7 +179,8 @@ export function MessageInput({
a.justify_center,
{height: 30, width: 30, backgroundColor: t.palette.primary_500},
]}
onPress={onSubmit}>
onPress={onSubmit}
disabled={needsEmailVerification}>
<PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
</Pressable>
</View>
@@ -0,0 +1,25 @@
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,
})
}
+21
View File
@@ -58,6 +58,7 @@ import {EmbeddingDisabledError} from '#/lib/api/resolve'
import {until} from '#/lib/async/until'
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useEmail} from '#/lib/hooks/useEmail'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -110,6 +111,8 @@ import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -297,6 +300,15 @@ export const ComposePost = ({
}
}, [onPressCancel, closeAllDialogs, closeAllModals])
const {needsEmailVerification} = useEmail()
const emailVerificationControl = useDialogControl()
useEffect(() => {
if (needsEmailVerification) {
emailVerificationControl.open()
}
}, [needsEmailVerification, emailVerificationControl])
const missingAltError = useMemo(() => {
if (!requireAltTextEnabled) {
return
@@ -570,6 +582,15 @@ export const ComposePost = ({
const isWebFooterSticky = !isNative && thread.posts.length > 1
return (
<BottomSheetPortalProvider>
<VerifyEmailDialog
control={emailVerificationControl}
onCloseWithoutVerifying={() => {
onClose()
}}
reasonText={_(
msg`Before creating a post, you must first verify your email.`,
)}
/>
<KeyboardAvoidingView
testID="composePostView"
behavior={isIOS ? 'padding' : 'height'}
+20 -2
View File
@@ -2,9 +2,11 @@ import React from 'react'
import {StyleSheet, View} from 'react-native'
import {AtUri} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -16,15 +18,20 @@ import {MyLists} from '#/view/com/lists/MyLists'
import {Button} from '#/view/com/util/forms/Button'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
export function ListsScreen({}: Props) {
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile} = useWebMediaQueries()
const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail()
const control = useDialogControl()
useFocusEffect(
React.useCallback(() => {
@@ -33,6 +40,11 @@ export function ListsScreen({}: Props) {
)
const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({
name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#curatelist',
@@ -46,7 +58,7 @@ export function ListsScreen({}: Props) {
} catch {}
},
})
}, [openModal, navigation])
}, [needsEmailVerification, control, openModal, navigation])
return (
<Layout.Screen testID="listsScreen">
@@ -87,6 +99,12 @@ export function ListsScreen({}: Props) {
</View>
</SimpleViewHeader>
<MyLists filter="curate" style={s.flexGrow1} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen>
)
}
+20 -2
View File
@@ -2,9 +2,11 @@ import React from 'react'
import {View} from 'react-native'
import {AtUri} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -16,15 +18,20 @@ import {MyLists} from '#/view/com/lists/MyLists'
import {Button} from '#/view/com/util/forms/Button'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
export function ModerationModlistsScreen({}: Props) {
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile} = useWebMediaQueries()
const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const {needsEmailVerification} = useEmail()
const control = useDialogControl()
useFocusEffect(
React.useCallback(() => {
@@ -33,6 +40,11 @@ export function ModerationModlistsScreen({}: Props) {
)
const onPressNewList = React.useCallback(() => {
if (needsEmailVerification) {
control.open()
return
}
openModal({
name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#modlist',
@@ -46,7 +58,7 @@ export function ModerationModlistsScreen({}: Props) {
} catch {}
},
})
}, [openModal, navigation])
}, [needsEmailVerification, control, openModal, navigation])
return (
<Layout.Screen testID="moderationModlistsScreen">
@@ -83,6 +95,12 @@ export function ModerationModlistsScreen({}: Props) {
</View>
</SimpleViewHeader>
<MyLists filter="mod" style={s.flexGrow1} />
<VerifyEmailDialog
reasonText={_(
msg`Before creating a list, you must first verify your email.`,
)}
control={control}
/>
</Layout.Screen>
)
}