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