Files
bsky-social-app/src/components/dialogs/BirthDateSettings.tsx
T
2026-03-11 11:32:43 -05:00

210 lines
6.7 KiB
TypeScript

import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {isAppPassword} from '#/lib/jwt'
import {getAge, getDateAgo} from '#/lib/strings/time'
import {logger} from '#/logger'
import {
useBirthdateMutation,
useIsBirthdateUpdateAllowed,
} from '#/state/birthdate'
import {
usePreferencesQuery,
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {atoms as a, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DateField} from '#/components/forms/DateField'
import {SimpleInlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB} from '#/env'
export function BirthDateSettingsDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const t = useTheme()
const {_} = useLingui()
const {isLoading, error, data: preferences} = usePreferencesQuery()
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
const {currentAccount} = useSession()
const isUsingAppPassword = isAppPassword(currentAccount?.accessJwt || '')
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
{isBirthdateUpdateAllowed ? (
<Dialog.ScrollableInner
label={_(msg`My Birthdate`)}
style={web({maxWidth: 400})}>
<View style={[a.gap_md]}>
<Text style={[a.text_xl, a.font_semi_bold]}>
<Trans>My Birthdate</Trans>
</Text>
<Text
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
This information is private and not shared with other users.
</Trans>
</Text>
{isLoading ? (
<Loader size="xl" />
) : error || !preferences ? (
<ErrorMessage
message={
error?.toString() ||
_(
msg`We were unable to load your birthdate preferences. Please try again.`,
)
}
style={[a.rounded_sm]}
/>
) : isUsingAppPassword ? (
<Admonition type="info">
<Trans>
Hmm, it looks like you're signed in with an{' '}
<Span style={[a.italic]}>App Password</Span>. To set your
birthdate, you'll need to sign in with your main account
password, or ask whomever controls this account to do so.
</Trans>
</Admonition>
) : (
<BirthdayInner control={control} preferences={preferences} />
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
) : (
<Dialog.ScrollableInner
label={_(msg`You recently changed your birthdate`)}
style={web({maxWidth: 400})}>
<View style={[a.gap_sm]}>
<Text
style={[
a.text_xl,
a.font_semi_bold,
a.leading_snug,
{paddingRight: 32},
]}>
<Trans>You recently changed your birthdate</Trans>
</Text>
<Text
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
There is a limit to how often you can change your birthdate. You
may need to wait a day or two before updating it again.
</Trans>
</Text>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)}
</Dialog.Outer>
)
}
function BirthdayInner({
control,
preferences,
}: {
control: Dialog.DialogControlProps
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const cleanError = useCleanError()
const [date, setDate] = useState(preferences.birthDate || getDateAgo(18))
const {isPending, error, mutateAsync: setBirthDate} = useBirthdateMutation()
const hasChanged = date !== preferences.birthDate
const errorMessage = useMemo(() => {
if (error) {
const {raw, clean} = cleanError(error)
return clean || raw || error.toString()
}
}, [error, cleanError])
const age = getAge(new Date(date))
const isUnder13 = age < 13
const isUnder18 = age >= 13 && age < 18
const onSave = useCallback(async () => {
try {
// skip if date is the same
if (hasChanged) {
await setBirthDate({birthDate: date})
}
control.close()
} catch (e: any) {
logger.error(`setBirthDate failed`, {message: e.message})
}
}, [date, setBirthDate, control, hasChanged])
return (
<View style={a.gap_lg} testID="birthDateSettingsDialog">
<View style={IS_IOS && [a.w_full, a.align_center]}>
<DateField
testID="birthdayInput"
value={date}
onChangeDate={newDate => setDate(new Date(newDate))}
label={_(msg`Birthdate`)}
accessibilityHint={_(msg`Enter your birthdate`)}
/>
</View>
{isUnder18 && hasChanged && (
<Admonition type="info">
<Trans>
The birthdate you've entered means you are under 18 years old.
Certain content and features may be unavailable to you.
</Trans>
</Admonition>
)}
{isUnder13 && (
<Admonition type="error">
<Trans>
You must be at least 13 years old to use Bluesky. Read our{' '}
<SimpleInlineLinkText
to="https://bsky.social/about/support/tos"
label={_(msg`Terms of Service`)}>
Terms of Service
</SimpleInlineLinkText>{' '}
for more information.
</Trans>
</Admonition>
)}
{errorMessage ? (
<ErrorMessage message={errorMessage} style={[a.rounded_sm]} />
) : undefined}
<View style={IS_WEB && [a.flex_row, a.justify_end]}>
<Button
label={hasChanged ? _(msg`Save birthdate`) : _(msg`Done`)}
size="large"
onPress={onSave}
variant="solid"
color="primary"
disabled={isUnder13}>
<ButtonText>
{hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
)
}