An ATproto social media client -- with an independent Appview.

Add email verification prompts throughout the app (#6174)

authored by hailey.at and committed by

GitHub 427f3a8b dd8d14e1

+265 -46
+25 -2
src/components/StarterPack/ProfileStarterPacks.tsx
··· 14 14 15 15 import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' 16 16 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 17 + import {useEmail} from '#/lib/hooks/useEmail' 17 18 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 18 19 import {NavigationProp} from '#/lib/routes/types' 19 20 import {parseStarterPackUri} from '#/lib/strings/starter-pack' ··· 27 28 import {Loader} from '#/components/Loader' 28 29 import * as Prompt from '#/components/Prompt' 29 30 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 31 + import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' 30 32 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' 31 33 32 34 interface SectionRef { ··· 186 188 const followersDialogControl = useDialogControl() 187 189 const errorDialogControl = useDialogControl() 188 190 191 + const {needsEmailVerification} = useEmail() 192 + const verifyEmailControl = useDialogControl() 193 + 189 194 const [isGenerating, setIsGenerating] = React.useState(false) 190 195 191 196 const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ ··· 249 254 color="primary" 250 255 size="small" 251 256 disabled={isGenerating} 252 - onPress={confirmDialogControl.open} 257 + onPress={() => { 258 + if (needsEmailVerification) { 259 + verifyEmailControl.open() 260 + } else { 261 + confirmDialogControl.open() 262 + } 263 + }} 253 264 style={{backgroundColor: 'transparent'}}> 254 265 <ButtonText style={{color: 'white'}}> 255 266 <Trans>Make one for me</Trans> ··· 262 273 color="primary" 263 274 size="small" 264 275 disabled={isGenerating} 265 - onPress={() => navigation.navigate('StarterPackWizard')} 276 + onPress={() => { 277 + if (needsEmailVerification) { 278 + verifyEmailControl.open() 279 + } else { 280 + navigation.navigate('StarterPackWizard') 281 + } 282 + }} 266 283 style={{ 267 284 backgroundColor: 'white', 268 285 borderColor: 'white', ··· 317 334 )} 318 335 onConfirm={generate} 319 336 confirmButtonCta={_(msg`Retry`)} 337 + /> 338 + <VerifyEmailDialog 339 + reasonText={_( 340 + msg`Before creating a starter pack, you must first verify your email.`, 341 + )} 342 + control={verifyEmailControl} 320 343 /> 321 344 </LinearGradientBackground> 322 345 )
+41 -21
src/components/dialogs/VerifyEmailDialog.tsx
··· 18 18 19 19 export function VerifyEmailDialog({ 20 20 control, 21 + onCloseWithoutVerifying, 22 + onCloseAfterVerifying, 23 + reasonText, 21 24 }: { 22 25 control: Dialog.DialogControlProps 26 + onCloseWithoutVerifying?: () => void 27 + onCloseAfterVerifying?: () => void 28 + reasonText?: string 23 29 }) { 24 30 const agent = useAgent() 25 31 ··· 30 36 control={control} 31 37 onClose={async () => { 32 38 if (!didVerify) { 39 + onCloseWithoutVerifying?.() 33 40 return 34 41 } 35 42 36 43 try { 37 44 await agent.resumeSession(agent.session!) 45 + onCloseAfterVerifying?.() 38 46 } catch (e: unknown) { 39 47 logger.error(String(e)) 40 48 return 41 49 } 42 50 }}> 43 51 <Dialog.Handle /> 44 - <Inner control={control} setDidVerify={setDidVerify} /> 52 + <Inner 53 + control={control} 54 + setDidVerify={setDidVerify} 55 + reasonText={reasonText} 56 + /> 45 57 </Dialog.Outer> 46 58 ) 47 59 } ··· 49 61 export function Inner({ 50 62 control, 51 63 setDidVerify, 64 + reasonText, 52 65 }: { 53 66 control: Dialog.DialogControlProps 54 67 setDidVerify: (value: boolean) => void 68 + reasonText?: string 55 69 }) { 56 70 const {_} = useLingui() 57 71 const {currentAccount} = useSession() ··· 135 149 <Text style={[a.text_md, a.leading_snug]}> 136 150 {currentStep === 'StepOne' ? ( 137 151 <> 138 - <Trans> 139 - You'll receive an email at{' '} 140 - <Text style={[a.text_md, a.leading_snug, a.font_bold]}> 141 - {currentAccount?.email} 142 - </Text>{' '} 143 - to verify it's you. 144 - </Trans>{' '} 145 - <InlineLinkText 146 - to="#" 147 - label={_(msg`Change email address`)} 148 - style={[a.text_md, a.leading_snug]} 149 - onPress={e => { 150 - e.preventDefault() 151 - control.close(() => { 152 - openModal({name: 'change-email'}) 153 - }) 154 - return false 155 - }}> 156 - <Trans>Need to change it?</Trans> 157 - </InlineLinkText> 152 + {!reasonText ? ( 153 + <> 154 + <Trans> 155 + You'll receive an email at{' '} 156 + <Text style={[a.text_md, a.leading_snug, a.font_bold]}> 157 + {currentAccount?.email} 158 + </Text>{' '} 159 + to verify it's you. 160 + </Trans>{' '} 161 + <InlineLinkText 162 + to="#" 163 + label={_(msg`Change email address`)} 164 + style={[a.text_md, a.leading_snug]} 165 + onPress={e => { 166 + e.preventDefault() 167 + control.close(() => { 168 + openModal({name: 'change-email'}) 169 + }) 170 + return false 171 + }}> 172 + <Trans>Need to change it?</Trans> 173 + </InlineLinkText> 174 + </> 175 + ) : ( 176 + reasonText 177 + )} 158 178 </> 159 179 ) : ( 160 180 uiStrings[currentStep].message
+41 -15
src/components/dms/MessageProfileButton.tsx
··· 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 + import {useNavigation} from '@react-navigation/native' 6 7 8 + import {useEmail} from '#/lib/hooks/useEmail' 9 + import {NavigationProp} from '#/lib/routes/types' 7 10 import {logEvent} from '#/lib/statsig/statsig' 8 11 import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' 9 12 import {atoms as a, useTheme} from '#/alf' 10 - import {ButtonIcon} from '#/components/Button' 13 + import {Button, ButtonIcon} from '#/components/Button' 11 14 import {canBeMessaged} from '#/components/dms/util' 12 15 import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' 13 - import {Link} from '#/components/Link' 16 + import {useDialogControl} from '../Dialog' 17 + import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' 14 18 15 19 export function MessageProfileButton({ 16 20 profile, ··· 19 23 }) { 20 24 const {_} = useLingui() 21 25 const t = useTheme() 26 + const navigation = useNavigation<NavigationProp>() 27 + const {needsEmailVerification} = useEmail() 28 + const verifyEmailControl = useDialogControl() 22 29 23 30 const {data: convo, isPending} = useMaybeConvoForUser(profile.did) 24 31 25 32 const onPress = React.useCallback(() => { 33 + if (!convo?.id) { 34 + return 35 + } 36 + 37 + if (needsEmailVerification) { 38 + verifyEmailControl.open() 39 + return 40 + } 41 + 26 42 if (convo && !convo.lastMessage) { 27 43 logEvent('chat:create', {logContext: 'ProfileHeader'}) 28 44 } 29 45 logEvent('chat:open', {logContext: 'ProfileHeader'}) 30 - }, [convo]) 46 + 47 + navigation.navigate('MessagesConversation', {conversation: convo.id}) 48 + }, [needsEmailVerification, verifyEmailControl, convo, navigation]) 31 49 32 50 if (isPending) { 33 51 // show pending state based on declaration ··· 53 71 54 72 if (convo) { 55 73 return ( 56 - <Link 57 - testID="dmBtn" 58 - size="small" 59 - color="secondary" 60 - variant="solid" 61 - shape="round" 62 - label={_(msg`Message ${profile.handle}`)} 63 - to={`/messages/${convo.id}`} 64 - style={[a.justify_center]} 65 - onPress={onPress}> 66 - <ButtonIcon icon={Message} size="md" /> 67 - </Link> 74 + <> 75 + <Button 76 + accessibilityRole="button" 77 + testID="dmBtn" 78 + size="small" 79 + color="secondary" 80 + variant="solid" 81 + shape="round" 82 + label={_(msg`Message ${profile.handle}`)} 83 + style={[a.justify_center]} 84 + onPress={onPress}> 85 + <ButtonIcon icon={Message} size="md" /> 86 + </Button> 87 + <VerifyEmailDialog 88 + reasonText={_( 89 + msg`Before you may message another user, you must first verify your email.`, 90 + )} 91 + control={verifyEmailControl} 92 + /> 93 + </> 68 94 ) 69 95 } else { 70 96 return null
+19 -1
src/components/dms/dialogs/NewChatDialog.tsx
··· 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 + import {useEmail} from '#/lib/hooks/useEmail' 5 6 import {logEvent} from '#/lib/statsig/statsig' 6 7 import {logger} from '#/logger' 7 8 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' ··· 9 10 import * as Toast from '#/view/com/util/Toast' 10 11 import {useTheme} from '#/alf' 11 12 import * as Dialog from '#/components/Dialog' 13 + import {useDialogControl} from '#/components/Dialog' 14 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 12 15 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 13 16 import {SearchablePeopleList} from './SearchablePeopleList' 14 17 ··· 21 24 }) { 22 25 const t = useTheme() 23 26 const {_} = useLingui() 27 + const {needsEmailVerification} = useEmail() 28 + const verifyEmailControl = useDialogControl() 24 29 25 30 const {mutate: createChat} = useGetConvoForMembers({ 26 31 onSuccess: data => { ··· 48 53 <> 49 54 <FAB 50 55 testID="newChatFAB" 51 - onPress={control.open} 56 + onPress={() => { 57 + if (needsEmailVerification) { 58 + verifyEmailControl.open() 59 + } else { 60 + control.open() 61 + } 62 + }} 52 63 icon={<Plus size="lg" fill={t.palette.white} />} 53 64 accessibilityRole="button" 54 65 accessibilityLabel={_(msg`New chat`)} ··· 62 73 onSelectChat={onCreateChat} 63 74 /> 64 75 </Dialog.Outer> 76 + 77 + <VerifyEmailDialog 78 + reasonText={_( 79 + msg`Before you may message another user, you must first verify your email.`, 80 + )} 81 + control={verifyEmailControl} 82 + /> 65 83 </> 66 84 ) 67 85 }
+19
src/lib/hooks/useEmail.ts
··· 1 + import {useServiceConfigQuery} from '#/state/queries/email-verification-required' 2 + import {useSession} from '#/state/session' 3 + import {BSKY_SERVICE} from '../constants' 4 + import {getHostnameFromUrl} from '../strings/url-helpers' 5 + 6 + export function useEmail() { 7 + const {currentAccount} = useSession() 8 + 9 + const {data: serviceConfig} = useServiceConfigQuery() 10 + 11 + const isSelfHost = 12 + serviceConfig?.checkEmailConfirmed && 13 + currentAccount && 14 + getHostnameFromUrl(currentAccount.service) !== 15 + getHostnameFromUrl(BSKY_SERVICE) 16 + const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed 17 + 18 + return {needsEmailVerification} 19 + }
+24 -2
src/screens/Messages/Conversation.tsx
··· 4 4 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 5 5 import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 - import {useFocusEffect} from '@react-navigation/native' 7 + import {useFocusEffect, useNavigation} from '@react-navigation/native' 8 8 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 9 10 - import {CommonNavigatorParams} from '#/lib/routes/types' 10 + import {useEmail} from '#/lib/hooks/useEmail' 11 + import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 11 12 import {isWeb} from '#/platform/detection' 12 13 import {useProfileShadow} from '#/state/cache/profile-shadow' 13 14 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' ··· 19 20 import {CenteredView} from '#/view/com/util/Views' 20 21 import {MessagesList} from '#/screens/Messages/components/MessagesList' 21 22 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 23 + import {useDialogControl} from '#/components/Dialog' 24 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 22 25 import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 23 26 import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 24 27 import {Error} from '#/components/Error' ··· 161 164 hasScrolled: boolean 162 165 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 163 166 }) { 167 + const {_} = useLingui() 164 168 const convoState = useConvo() 169 + const navigation = useNavigation<NavigationProp>() 165 170 const recipient = useProfileShadow(recipientUnshadowed) 171 + const verifyEmailControl = useDialogControl() 172 + const {needsEmailVerification} = useEmail() 166 173 167 174 const moderation = React.useMemo(() => { 168 175 return moderateProfile(recipient, moderationOpts) ··· 178 185 userBlock, 179 186 } 180 187 }, [moderation]) 188 + 189 + React.useEffect(() => { 190 + if (needsEmailVerification) { 191 + verifyEmailControl.open() 192 + } 193 + }, [needsEmailVerification, verifyEmailControl]) 181 194 182 195 return ( 183 196 <> ··· 201 214 } 202 215 /> 203 216 )} 217 + <VerifyEmailDialog 218 + reasonText={_( 219 + msg`Before you may message another user, you must first verify your email.`, 220 + )} 221 + control={verifyEmailControl} 222 + onCloseWithoutVerifying={() => { 223 + navigation.navigate('Home') 224 + }} 225 + /> 204 226 </> 205 227 ) 206 228 }
+10 -1
src/screens/Messages/components/MessageInput.tsx
··· 18 18 19 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 20 import {useHaptics} from '#/lib/haptics' 21 + import {useEmail} from '#/lib/hooks/useEmail' 21 22 import {isIOS} from '#/platform/detection' 22 23 import { 23 24 useMessageDraft, ··· 61 62 const [message, setMessage] = React.useState(getDraft) 62 63 const inputRef = useAnimatedRef<TextInput>() 63 64 65 + const {needsEmailVerification} = useEmail() 66 + 64 67 useSaveMessageDraft(message) 65 68 useExtractEmbedFromFacets(message, setEmbed) 66 69 67 70 const onSubmit = React.useCallback(() => { 71 + if (needsEmailVerification) { 72 + return 73 + } 68 74 if (!hasEmbed && message.trim() === '') { 69 75 return 70 76 } ··· 84 90 inputRef.current?.focus() 85 91 }, 100) 86 92 }, [ 93 + needsEmailVerification, 87 94 hasEmbed, 88 95 message, 89 96 clearDraft, ··· 159 166 ref={inputRef} 160 167 hitSlop={HITSLOP_10} 161 168 animatedProps={animatedProps} 169 + editable={!needsEmailVerification} 162 170 /> 163 171 <Pressable 164 172 accessibilityRole="button" ··· 171 179 a.justify_center, 172 180 {height: 30, width: 30, backgroundColor: t.palette.primary_500}, 173 181 ]} 174 - onPress={onSubmit}> 182 + onPress={onSubmit} 183 + disabled={needsEmailVerification}> 175 184 <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} /> 176 185 </Pressable> 177 186 </View>
+25
src/state/queries/email-verification-required.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + interface ServiceConfig { 4 + checkEmailConfirmed: boolean 5 + } 6 + 7 + export function useServiceConfigQuery() { 8 + return useQuery({ 9 + queryKey: ['service-config'], 10 + queryFn: async () => { 11 + const res = await fetch( 12 + 'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig', 13 + ) 14 + if (!res.ok) { 15 + return { 16 + checkEmailConfirmed: false, 17 + } 18 + } 19 + 20 + const json = await res.json() 21 + return json as ServiceConfig 22 + }, 23 + staleTime: 5 * 60 * 1000, 24 + }) 25 + }
+21
src/view/com/composer/Composer.tsx
··· 58 58 import {until} from '#/lib/async/until' 59 59 import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' 60 60 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 61 + import {useEmail} from '#/lib/hooks/useEmail' 61 62 import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 62 63 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 63 64 import {usePalette} from '#/lib/hooks/usePalette' ··· 110 111 import {UserAvatar} from '#/view/com/util/UserAvatar' 111 112 import {atoms as a, native, useTheme} from '#/alf' 112 113 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 114 + import {useDialogControl} from '#/components/Dialog' 115 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 113 116 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 114 117 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 115 118 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' ··· 296 299 backHandler.remove() 297 300 } 298 301 }, [onPressCancel, closeAllDialogs, closeAllModals]) 302 + 303 + const {needsEmailVerification} = useEmail() 304 + const emailVerificationControl = useDialogControl() 305 + 306 + useEffect(() => { 307 + if (needsEmailVerification) { 308 + emailVerificationControl.open() 309 + } 310 + }, [needsEmailVerification, emailVerificationControl]) 299 311 300 312 const missingAltError = useMemo(() => { 301 313 if (!requireAltTextEnabled) { ··· 570 582 const isWebFooterSticky = !isNative && thread.posts.length > 1 571 583 return ( 572 584 <BottomSheetPortalProvider> 585 + <VerifyEmailDialog 586 + control={emailVerificationControl} 587 + onCloseWithoutVerifying={() => { 588 + onClose() 589 + }} 590 + reasonText={_( 591 + msg`Before creating a post, you must first verify your email.`, 592 + )} 593 + /> 573 594 <KeyboardAvoidingView 574 595 testID="composePostView" 575 596 behavior={isIOS ? 'padding' : 'height'}
+20 -2
src/view/screens/Lists.tsx
··· 2 2 import {StyleSheet, View} from 'react-native' 3 3 import {AtUri} from '@atproto/api' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {Trans} from '@lingui/macro' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 6 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 7 8 9 + import {useEmail} from '#/lib/hooks/useEmail' 8 10 import {usePalette} from '#/lib/hooks/usePalette' 9 11 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 16 18 import {Button} from '#/view/com/util/forms/Button' 17 19 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 18 20 import {Text} from '#/view/com/util/text/Text' 21 + import {useDialogControl} from '#/components/Dialog' 22 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 19 23 import * as Layout from '#/components/Layout' 20 24 21 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> 22 26 export function ListsScreen({}: Props) { 27 + const {_} = useLingui() 23 28 const pal = usePalette('default') 24 29 const setMinimalShellMode = useSetMinimalShellMode() 25 30 const {isMobile} = useWebMediaQueries() 26 31 const navigation = useNavigation<NavigationProp>() 27 32 const {openModal} = useModalControls() 33 + const {needsEmailVerification} = useEmail() 34 + const control = useDialogControl() 28 35 29 36 useFocusEffect( 30 37 React.useCallback(() => { ··· 33 40 ) 34 41 35 42 const onPressNewList = React.useCallback(() => { 43 + if (needsEmailVerification) { 44 + control.open() 45 + return 46 + } 47 + 36 48 openModal({ 37 49 name: 'create-or-edit-list', 38 50 purpose: 'app.bsky.graph.defs#curatelist', ··· 46 58 } catch {} 47 59 }, 48 60 }) 49 - }, [openModal, navigation]) 61 + }, [needsEmailVerification, control, openModal, navigation]) 50 62 51 63 return ( 52 64 <Layout.Screen testID="listsScreen"> ··· 87 99 </View> 88 100 </SimpleViewHeader> 89 101 <MyLists filter="curate" style={s.flexGrow1} /> 102 + <VerifyEmailDialog 103 + reasonText={_( 104 + msg`Before creating a list, you must first verify your email.`, 105 + )} 106 + control={control} 107 + /> 90 108 </Layout.Screen> 91 109 ) 92 110 }
+20 -2
src/view/screens/ModerationModlists.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {AtUri} from '@atproto/api' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {Trans} from '@lingui/macro' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 6 7 import {useFocusEffect, useNavigation} from '@react-navigation/native' 7 8 9 + import {useEmail} from '#/lib/hooks/useEmail' 8 10 import {usePalette} from '#/lib/hooks/usePalette' 9 11 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 12 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' ··· 16 18 import {Button} from '#/view/com/util/forms/Button' 17 19 import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 18 20 import {Text} from '#/view/com/util/text/Text' 21 + import {useDialogControl} from '#/components/Dialog' 22 + import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 19 23 import * as Layout from '#/components/Layout' 20 24 21 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> 22 26 export function ModerationModlistsScreen({}: Props) { 27 + const {_} = useLingui() 23 28 const pal = usePalette('default') 24 29 const setMinimalShellMode = useSetMinimalShellMode() 25 30 const {isMobile} = useWebMediaQueries() 26 31 const navigation = useNavigation<NavigationProp>() 27 32 const {openModal} = useModalControls() 33 + const {needsEmailVerification} = useEmail() 34 + const control = useDialogControl() 28 35 29 36 useFocusEffect( 30 37 React.useCallback(() => { ··· 33 40 ) 34 41 35 42 const onPressNewList = React.useCallback(() => { 43 + if (needsEmailVerification) { 44 + control.open() 45 + return 46 + } 47 + 36 48 openModal({ 37 49 name: 'create-or-edit-list', 38 50 purpose: 'app.bsky.graph.defs#modlist', ··· 46 58 } catch {} 47 59 }, 48 60 }) 49 - }, [openModal, navigation]) 61 + }, [needsEmailVerification, control, openModal, navigation]) 50 62 51 63 return ( 52 64 <Layout.Screen testID="moderationModlistsScreen"> ··· 83 95 </View> 84 96 </SimpleViewHeader> 85 97 <MyLists filter="mod" style={s.flexGrow1} /> 98 + <VerifyEmailDialog 99 + reasonText={_( 100 + msg`Before creating a list, you must first verify your email.`, 101 + )} 102 + control={control} 103 + /> 86 104 </Layout.Screen> 87 105 ) 88 106 }