Add send email dialog

This commit is contained in:
Eric Bailey
2025-06-26 15:00:29 -05:00
parent 7021e8bab2
commit 2bf1c1e0d7
5 changed files with 347 additions and 91 deletions
+48
View File
@@ -0,0 +1,48 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {APP_LANGUAGES} from '#/locale/languages'
import * as Select from '#/components/Select'
export function LanguageSelect({
value,
onChange,
}: {
value: string
onChange: (value: string) => void
}) {
const {_} = useLingui()
const handleOnChange = React.useCallback(
(value: string) => {
if (!value) return
onChange(sanitizeAppLanguageSetting(value))
},
[onChange],
)
return (
<Select.Root
value={sanitizeAppLanguageSetting(value)}
onValueChange={handleOnChange}>
<Select.Trigger label={_(msg`Select language`)}>
<Select.ValueText />
<Select.Icon />
</Select.Trigger>
<Select.Content
renderItem={({label, value}) => (
<Select.Item value={value} label={label}>
<Select.ItemIndicator />
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
)}
items={APP_LANGUAGES.map(l => ({
label: l.name,
value: l.code2,
}))}
/>
</Select.Root>
)
}
@@ -0,0 +1,46 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {atoms as a, select, useTheme} from '#/alf'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {Text} from '#/components/Typography'
export function AgeAssuranceBadge() {
const t = useTheme()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
a.px_sm,
a.py_xs,
a.pr_sm,
a.rounded_full,
{
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dark: t.palette.primary_100,
dim: t.palette.primary_100,
}),
},
]}>
<Shield size="sm" />
<Text
style={[
a.font_bold,
a.leading_snug,
{
color: select(t.name, {
light: t.palette.primary_800,
dark: t.palette.primary_800,
dim: t.palette.primary_800,
}),
},
]}>
<Trans>Age Assurance</Trans>
</Text>
</View>
)
}
@@ -0,0 +1,179 @@
import {useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {validate as validateEmail} from 'email-validator'
import {useLanguagePrefs} from '#/state/preferences'
import {useSession} from '#/state/session'
import {atoms as a, useTheme, web} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {urls} from '#/components/ageAssurance/const'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import * as TextField from '#/components/forms/TextField'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {LanguageSelect} from '#/components/LanguageSelect'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export {useDialogControl} from '#/components/Dialog/context'
export function AgeAssuranceInitDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const {_} = useLingui()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(
'Begin the age assurance process by completing the fields below.',
)}
style={[
web({
maxWidth: 400,
}),
]}>
<Inner />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
function Inner() {
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
const langPrefs = useLanguagePrefs()
const [email, setEmail] = useState(currentAccount?.email || '')
const [emailValid, setEmailValid] = useState(validateEmail(email))
const [language, setLanguage] = useState(langPrefs.appLanguage)
const onSubmit = async () => {
try {
if (!validateEmail(email)) {
setEmailValid(false)
return
}
} catch (e) {}
}
return (
<View>
<View style={[a.align_start]}>
<AgeAssuranceBadge />
<Text style={[a.text_xl, a.font_heavy, a.pt_xl, a.pb_md]}>
<Trans>Verify your age</Trans>
</Text>
<View style={[a.pb_xl, a.gap_sm]}>
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
We use{' '}
<InlineLinkText
label={urls.kwsHome}
to={urls.kwsHome}
style={[a.text_sm, a.leading_snug]}>
KWS
</InlineLinkText>{' '}
to verify that youre an adult. When you click "Get an email"
below, Bluesky will share your email address with KWS.
</Trans>
</Text>
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
If KWS cannot use your email address to confirm that you
previously verified your age, youll be taken to the KWS website
to provide more details.
</Trans>
</Text>
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>This should only take a few minutes.</Trans>
</Text>
</View>
<Divider />
<View style={[a.w_full, a.pt_xl, a.gap_md]}>
<View>
<TextField.LabelText>
<Trans>Your email</Trans>
</TextField.LabelText>
<TextField.Root isInvalid={!emailValid}>
<TextField.Input
label={_(msg`Your email`)}
placeholder={_(msg`Your email`)}
value={email}
onChangeText={setEmail}
onFocus={() => setEmailValid(true)}
onBlur={() => {
setEmailValid(validateEmail(email))
}}
returnKeyType="done"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
onSubmitEditing={onSubmit}
/>
</TextField.Root>
</View>
<View>
<TextField.LabelText>
<Trans>Your preferred language</Trans>
</TextField.LabelText>
<LanguageSelect value={language} onChange={setLanguage} />
</View>
<Button
label={_(msg`Get an email`)}
size="large"
variant="solid"
color="primary">
<ButtonText>
<Trans>Get an email</Trans>
</ButtonText>
<ButtonIcon icon={Envelope} position="right" />
</Button>
</View>
<Text
style={[
a.text_xs,
a.leading_snug,
a.pt_lg,
t.atoms.text_contrast_medium,
]}>
<Trans>
By continuing, you agree to the{' '}
<InlineLinkText
label={urls.kwsTermsOfUse}
to={urls.kwsTermsOfUse}
style={[a.text_xs, a.leading_snug]}>
KWS Terms of Use
</InlineLinkText>{' '}
and acknowledge that KWS will store your verified status alongside
your hashed email address in accordance with the{' '}
<InlineLinkText
label={urls.kwsPrivacyPolicy}
to={urls.kwsPrivacyPolicy}
style={[a.text_xs, a.leading_snug]}>
KWS Privacy Policy
</InlineLinkText>
. This means you wont need to verify again the next time you use
this email for other apps, games, and services powered by KWS
technology.
</Trans>
</Text>
</View>
</View>
)
}
@@ -4,120 +4,98 @@ import {useLingui} from '@lingui/react'
import {useAgeAssuranceContext} from '#/state/ageAssurance'
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {
AgeAssuranceInitDialog,
useDialogControl,
} from '#/components/ageAssurance/AgeAssuranceInitDialog'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {Text} from '#/components/Typography'
export function AgeAssurancePrompt({style}: ViewStyleProp & {}) {
const t = useTheme()
const {_} = useLingui()
const {hasInitiated} = useAgeAssuranceContext()
const control = useDialogControl()
return hasInitiated ? null : (
<View style={style}>
<View
style={[
a.p_lg,
a.rounded_md,
a.border,
{
backgroundColor: select(t.name, {
light: t.palette.primary_25,
dark: t.palette.primary_25,
dim: t.palette.primary_25,
}),
borderColor: select(t.name, {
light: t.palette.primary_100,
dark: t.palette.primary_100,
dim: t.palette.primary_100,
}),
},
]}>
<View style={[a.align_start, a.pb_md]}>
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
a.px_sm,
a.py_xs,
a.pr_sm,
a.rounded_full,
{
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dark: t.palette.primary_100,
dim: t.palette.primary_100,
}),
},
]}>
<Shield size="sm" />
<Text
style={[
a.font_bold,
a.leading_snug,
{
color: select(t.name, {
light: t.palette.primary_800,
dark: t.palette.primary_800,
dim: t.palette.primary_800,
}),
},
]}>
<Trans>Age Assurance</Trans>
</Text>
</View>
</View>
<Text style={[a.text_md, a.leading_snug]}>
You're currently accessing Bluesky from a location that by law
requires you to verify your age prior to accessing certain content and
features.
</Text>
<>
<AgeAssuranceInitDialog control={control} />
<Divider
<View style={style}>
<View
style={[
a.mt_lg,
a.p_lg,
a.rounded_md,
a.border,
{
backgroundColor: select(t.name, {
light: t.palette.primary_25,
dark: t.palette.primary_25,
dim: t.palette.primary_25,
}),
borderColor: select(t.name, {
light: t.palette.primary_100,
dark: t.palette.primary_100,
dim: t.palette.primary_100,
}),
},
]}
/>
<View
style={[
a.flex_row,
a.justify_between,
a.align_center,
a.pt_md,
a.gap_lg,
]}>
<Text
style={{
color: select(t.name, {
light: t.palette.primary_800,
dark: t.palette.primary_800,
dim: t.palette.primary_800,
}),
}}>
<Trans>Verification takes only a few minutes</Trans>
<View style={[a.align_start, a.pb_md]}>
<AgeAssuranceBadge />
</View>
<Text style={[a.text_md, a.leading_snug]}>
You're currently accessing Bluesky from a location that by law
requires you to verify your age prior to accessing certain content
and features.
</Text>
<Button
label={_(msg`Verify now`)}
size="small"
variant="solid"
color="primary">
<ButtonText>
<Trans>Verify now</Trans>
</ButtonText>
</Button>
<Divider
style={[
a.mt_lg,
{
borderColor: select(t.name, {
light: t.palette.primary_100,
dark: t.palette.primary_100,
dim: t.palette.primary_100,
}),
},
]}
/>
<View
style={[
a.flex_row,
a.justify_between,
a.align_center,
a.pt_md,
a.gap_lg,
]}>
<Text
style={{
color: select(t.name, {
light: t.palette.primary_800,
dark: t.palette.primary_800,
dim: t.palette.primary_800,
}),
}}>
<Trans>Verification takes only a few minutes</Trans>
</Text>
<Button
label={_(msg`Verify now`)}
size="small"
variant="solid"
color="primary"
onPress={() => control.open()}>
<ButtonText>
<Trans>Verify now</Trans>
</ButtonText>
</Button>
</View>
</View>
</View>
</View>
</>
)
}
+5
View File
@@ -0,0 +1,5 @@
export const urls = {
kwsHome: 'https://dev.epicgames.com/docs/kids-web-services',
kwsTermsOfUse: 'https://www.kidswebservices.com/en-US/terms-of-use',
kwsPrivacyPolicy: 'https://www.kidswebservices.com/en-US/privacy-policy',
}