diff --git a/package.json b/package.json index f6ad2eba..56b0366d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-clipboard/clipboard": "^1.10.0", "@react-native-community/blur": "^4.3.0", + "@react-native-community/datetimepicker": "6.7.3", "@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/drawer": "^6.6.2", "@react-navigation/native": "^6.1.6", diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts index 6cd70498..588b8445 100644 --- a/src/lib/strings/time.ts +++ b/src/lib/strings/time.ts @@ -39,3 +39,13 @@ export function niceDate(date: number | string | Date) { minute: '2-digit', })}` } + +export function getAge(birthDate: Date): number { + var today = new Date() + var age = today.getFullYear() - birthDate.getFullYear() + var m = today.getMonth() - birthDate.getMonth() + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { + age-- + } + return age +} diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index e661cb59..3f83dd6a 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -6,6 +6,9 @@ import {ComAtprotoServerCreateAccount} from '@atproto/api' import * as EmailValidator from 'email-validator' import {createFullHandle} from 'lib/strings/handles' import {cleanError} from 'lib/strings/errors' +import {getAge} from 'lib/strings/time' + +const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago export class CreateAccountModel { step: number = 1 @@ -21,7 +24,7 @@ export class CreateAccountModel { email = '' password = '' handle = '' - is13 = false + birthDate = DEFAULT_DATE constructor(public rootStore: RootStoreModel) { makeAutoObservable(this, {}, {autoBind: true}) @@ -32,6 +35,13 @@ export class CreateAccountModel { next() { this.error = '' + if (this.step === 2) { + if (getAge(this.birthDate) < 13) { + this.error = + 'Unfortunately, you do not meet the requirements to create an account.' + return + } + } this.step++ } @@ -124,8 +134,7 @@ export class CreateAccountModel { return ( (!this.isInviteCodeRequired || this.inviteCode) && !!this.email && - !!this.password && - this.is13 + !!this.password ) } return !!this.handle @@ -186,7 +195,7 @@ export class CreateAccountModel { this.handle = v } - setIs13(v: boolean) { - this.is13 = v + setBirthDate(v: Date) { + this.birthDate = v } } diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index eceee50d..1e014f18 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -1,14 +1,9 @@ import React from 'react' -import { - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' +import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CreateAccountModel} from 'state/models/ui/create-account' import {Text} from 'view/com/util/text/Text' +import {DateInput} from 'view/com/util/forms/DateInput' import {StepHeader} from './StepHeader' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -104,26 +99,20 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { - Legal check + nativeID="birthDate"> + Your birth date - model.setIs13(!model.is13)} - accessibilityRole="checkbox" - accessibilityLabel="Verify age" - accessibilityHint="Verifies that I am at least 13 years of age" - accessibilityLabelledBy="legalCheck"> - - {model.is13 && ( - - )} - - - I am 13 years old or older - - + {model.serviceDescription && ( @@ -144,26 +133,9 @@ const styles = StyleSheet.create({ marginTop: 10, }, - toggleBtn: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', + dateInputButton: { borderWidth: 1, - paddingHorizontal: 10, - paddingVertical: 10, borderRadius: 6, - }, - toggleBtnLabel: { - flex: 1, - paddingHorizontal: 10, - }, - - checkbox: { - borderWidth: 1, - borderRadius: 2, - width: 24, - height: 24, - alignItems: 'center', - justifyContent: 'center', + paddingVertical: 14, }, }) diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index 3b5b0028..1c9b1cf5 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -35,6 +35,9 @@ export function Button({ onPress, children, testID, + accessibilityLabel, + accessibilityHint, + accessibilityLabelledBy, }: React.PropsWithChildren<{ type?: ButtonType label?: string @@ -42,6 +45,9 @@ export function Button({ labelStyle?: StyleProp onPress?: () => void testID?: string + accessibilityLabel?: string + accessibilityHint?: string + accessibilityLabelledBy?: string }>) { const theme = useTheme() const typeOuterStyle = choose>( @@ -133,7 +139,10 @@ export function Button({ style={[typeOuterStyle, styles.outer, style]} onPress={onPressWrapped} testID={testID} - accessibilityRole="button"> + accessibilityRole="button" + accessibilityLabel={accessibilityLabel} + accessibilityHint={accessibilityHint} + accessibilityLabelledBy={accessibilityLabelledBy}> {label ? ( {label} diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx new file mode 100644 index 00000000..4aa5cb61 --- /dev/null +++ b/src/view/com/util/forms/DateInput.tsx @@ -0,0 +1,96 @@ +import React, {useState, useCallback} from 'react' +import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {isIOS, isAndroid} from 'platform/detection' +import {Button, ButtonType} from './Button' +import {Text} from '../text/Text' +import {TypographyVariant} from 'lib/ThemeContext' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' + +interface Props { + testID?: string + value: Date + onChange: (date: Date) => void + buttonType?: ButtonType + buttonStyle?: StyleProp + buttonLabelType?: TypographyVariant + buttonLabelStyle?: StyleProp + accessibilityLabel: string + accessibilityHint: string + accessibilityLabelledBy?: string +} + +export function DateInput(props: Props) { + const [show, setShow] = useState(false) + const theme = useTheme() + const pal = usePalette('default') + + const onChangeInternal = useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + setShow(false) + if (date) { + props.onChange(date) + } + }, + [setShow, props], + ) + + const onPress = useCallback(() => { + setShow(true) + }, [setShow]) + + return ( + + {isAndroid && ( + + )} + {(isIOS || show) && ( + + )} + + ) +} + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, +}) diff --git a/src/view/com/util/forms/DateInput.web.tsx b/src/view/com/util/forms/DateInput.web.tsx new file mode 100644 index 00000000..89dff551 --- /dev/null +++ b/src/view/com/util/forms/DateInput.web.tsx @@ -0,0 +1,92 @@ +import React, {useState, useCallback} from 'react' +import { + StyleProp, + StyleSheet, + TextInput as RNTextInput, + TextStyle, + View, + ViewStyle, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' + +interface Props { + testID?: string + value: Date + onChange: (date: Date) => void + buttonType?: string + buttonStyle?: StyleProp + buttonLabelType?: string + buttonLabelStyle?: StyleProp + accessibilityLabel: string + accessibilityHint: string + accessibilityLabelledBy?: string +} + +export function DateInput(props: Props) { + const theme = useTheme() + const pal = usePalette('default') + const palError = usePalette('error') + const [value, setValue] = useState(props.value.toLocaleDateString()) + const [isValid, setIsValid] = useState(true) + + const onChangeInternal = useCallback( + (v: string) => { + setValue(v) + const d = new Date(v) + if (!isNaN(Number(d))) { + setIsValid(true) + props.onChange(d) + } else { + setIsValid(false) + } + }, + [setValue, setIsValid, props], + ) + + return ( + + + onChangeInternal(v)} + value={value} + accessibilityLabel={props.accessibilityLabel} + accessibilityHint={props.accessibilityHint} + accessibilityLabelledBy={props.accessibilityLabelledBy} + /> + + ) +} + +const styles = StyleSheet.create({ + container: { + borderWidth: 1, + borderRadius: 6, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 4, + }, + icon: { + marginLeft: 10, + }, + textInput: { + flex: 1, + width: '100%', + paddingVertical: 10, + paddingHorizontal: 10, + fontSize: 17, + letterSpacing: 0.25, + fontWeight: '400', + borderRadius: 10, + }, +}) diff --git a/src/view/index.ts b/src/view/index.ts index 8de03586..dd8a585d 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -20,6 +20,7 @@ import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' +import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' @@ -97,6 +98,7 @@ export function setup() { farBell, faBookmark, farBookmark, + farCalendar, faCamera, faCheck, faCircleCheck, diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 5559f036..f98cdc0c 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -440,6 +440,7 @@ export const SettingsScreen = withAuthRequired( function AccountDropdownBtn({handle}: {handle: string}) { const store = useStores() + const pal = usePalette('default') const items = [ { label: 'Remove account', @@ -452,7 +453,10 @@ function AccountDropdownBtn({handle}: {handle: string}) { return ( - + ) diff --git a/yarn.lock b/yarn.lock index 3351f6b1..bf556972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2949,6 +2949,13 @@ prompts "^2.4.0" semver "^6.3.0" +"@react-native-community/datetimepicker@6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-6.7.3.tgz#e6d75a42729265d8404d1d668c86926564abca2f" + integrity sha512-fXWbEdHMLW/e8cts3snEsbOTbnFXfUHeO2pkiDFX3fWpFoDtUrRWvn50xbY13IJUUKHDhoJ+mj24nMRVIXfX1A== + dependencies: + invariant "^2.2.4" + "@react-native-community/eslint-config@^3.0.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d"