diff options
Diffstat (limited to 'src/view/com/auth')
21 files changed, 316 insertions, 2841 deletions
diff --git a/src/view/com/auth/HomeLoggedOutCTA.tsx b/src/view/com/auth/HomeLoggedOutCTA.tsx index f796d8bae..4c8c35da7 100644 --- a/src/view/com/auth/HomeLoggedOutCTA.tsx +++ b/src/view/com/auth/HomeLoggedOutCTA.tsx @@ -1,14 +1,15 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import {ScrollView} from '../util/Views' -import {Text} from '../util/text/Text' + import {usePalette} from '#/lib/hooks/usePalette' -import {colors, s} from '#/lib/styles' -import {TextLink} from '../util/Link' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {colors, s} from '#/lib/styles' import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {TextLink} from '../util/Link' +import {Text} from '../util/text/Text' +import {ScrollView} from '../util/Views' export function HomeLoggedOutCTA() { const pal = usePalette('default') @@ -52,7 +53,9 @@ export function HomeLoggedOutCTA() { onPress={showCreateAccount} accessibilityRole="button" accessibilityLabel={_(msg`Create new account`)} - accessibilityHint="Opens flow to create a new Bluesky account"> + accessibilityHint={_( + msg`Opens flow to create a new Bluesky account`, + )}> <Text style={[ s.white, @@ -68,14 +71,16 @@ export function HomeLoggedOutCTA() { onPress={showSignIn} accessibilityRole="button" accessibilityLabel={_(msg`Sign in`)} - accessibilityHint="Opens flow to sign into your existing Bluesky account"> + accessibilityHint={_( + msg`Opens flow to sign into your existing Bluesky account`, + )}> <Text style={[ pal.text, styles.btnLabel, isMobile && styles.btnLabelMobile, ]}> - <Trans>Sign In</Trans> + <Trans>Sign in</Trans> </Text> </TouchableOpacity> </View> diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 603abbab2..c8c81dd77 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -1,27 +1,28 @@ import React from 'react' -import {View, Pressable} from 'react-native' +import {Pressable, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' import {useNavigation} from '@react-navigation/native' -import {isIOS, isNative} from 'platform/detection' -import {Login} from 'view/com/auth/login/Login' -import {CreateAccount} from 'view/com/auth/create/CreateAccount' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {SplashScreen} from './SplashScreen' -import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useAnalytics} from '#/lib/analytics/analytics' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {logEvent} from '#/lib/statsig/statsig' +import {s} from '#/lib/styles' +import {isIOS, isNative} from '#/platform/detection' +import {useSession} from '#/state/session' import { useLoggedOutView, useLoggedOutViewControls, } from '#/state/shell/logged-out' -import {useSession} from '#/state/session' -import {Text} from '#/view/com/util/text/Text' +import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' import {NavigationProp} from 'lib/routes/types' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' +import {Text} from '#/view/com/util/text/Text' +import {Login} from '#/screens/Login' +import {Signup} from '#/screens/Signup' +import {SplashScreen} from './SplashScreen' enum ScreenState { S_LoginOrCreateAccount, @@ -133,10 +134,14 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { {screenState === ScreenState.S_LoginOrCreateAccount ? ( <SplashScreen - onPressSignin={() => setScreenState(ScreenState.S_Login)} - onPressCreateAccount={() => + onPressSignin={() => { + setScreenState(ScreenState.S_Login) + logEvent('splash:signInPressed', {}) + }} + onPressCreateAccount={() => { setScreenState(ScreenState.S_CreateAccount) - } + logEvent('splash:createAccountPressed', {}) + }} /> ) : undefined} {screenState === ScreenState.S_Login ? ( @@ -148,7 +153,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { /> ) : undefined} {screenState === ScreenState.S_CreateAccount ? ( - <CreateAccount + <Signup onPressBack={() => setScreenState(ScreenState.S_LoginOrCreateAccount) } diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index ffd07d945..763b01dfa 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -1,14 +1,21 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Text} from 'view/com/util/text/Text' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {CenteredView} from '../util/Views' -import {Trans, msg} from '@lingui/macro' +import {View} from 'react-native' +import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' + +import {sanitizeAppLanguageSetting} from '#/locale/helpers' +import {APP_LANGUAGES} from '#/locale/languages' +import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' +import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' +import {Text} from '#/components/Typography' +import {CenteredView} from '../util/Views' export const SplashScreen = ({ onPressSignin, @@ -17,82 +24,121 @@ export const SplashScreen = ({ onPressSignin: () => void onPressCreateAccount: () => void }) => { - const pal = usePalette('default') + const t = useTheme() const {_} = useLingui() + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() + const insets = useSafeAreaInsets() + + const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) + + const onChangeAppLanguage = React.useCallback( + (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { + if (!value) return + if (sanitizedLang !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) + } + }, + [sanitizedLang, setLangPrefs], + ) + return ( - <CenteredView style={[styles.container, pal.view]}> + <CenteredView style={[a.h_full, a.flex_1]}> <ErrorBoundary> - <View style={styles.hero}> + <View style={[{flex: 1}, a.justify_center, a.align_center]}> <Logo width={92} fill="sky" /> - <View style={{paddingTop: 40, paddingBottom: 6}}> - <Logotype width={161} fill={pal.text.color} /> + <View style={[a.pb_sm, a.pt_5xl]}> + <Logotype width={161} fill={t.atoms.text.color} /> </View> - <Text type="lg-medium" style={[pal.textLight]}> + <Text + style={[a.text_md, a.font_semibold, t.atoms.text_contrast_medium]}> <Trans>What's up?</Trans> </Text> </View> - <View testID="signinOrCreateAccount" style={styles.btns}> - <TouchableOpacity + <View testID="signinOrCreateAccount"> + <Button testID="createAccountButton" - style={[styles.btn, {backgroundColor: colors.blue3}]} onPress={onPressCreateAccount} accessibilityRole="button" - accessibilityLabel={_(msg`Create new account`)} - accessibilityHint="Opens flow to create a new Bluesky account"> - <Text style={[s.white, styles.btnLabel]}> + label={_(msg`Create new account`)} + accessibilityHint={_( + msg`Opens flow to create a new Bluesky account`, + )} + style={[a.mx_xl, a.mb_xl]} + size="large" + variant="solid" + color="primary"> + <ButtonText> <Trans>Create a new account</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity + </ButtonText> + </Button> + <Button testID="signInButton" - style={[styles.btn, pal.btn]} onPress={onPressSignin} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign in`)} - accessibilityHint="Opens flow to sign into your existing Bluesky account"> - <Text style={[pal.text, styles.btnLabel]}> - <Trans>Sign In</Trans> - </Text> - </TouchableOpacity> + label={_(msg`Sign in`)} + accessibilityHint={_( + msg`Opens flow to sign into your existing Bluesky account`, + )} + style={[a.mx_xl, a.mb_xl]} + size="large" + variant="solid" + color="secondary"> + <ButtonText> + <Trans>Sign in</Trans> + </ButtonText> + </Button> </View> + <View + style={[ + a.px_lg, + a.pt_md, + a.pb_2xl, + a.justify_center, + a.align_center, + ]}> + <View style={a.relative}> + <RNPickerSelect + placeholder={{}} + value={sanitizedLang} + onValueChange={onChangeAppLanguage} + items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ + label: l.name, + value: l.code2, + key: l.code2, + }))} + useNativeAndroidPickerStyle={false} + style={{ + inputAndroid: { + color: t.atoms.text_contrast_medium.color, + fontSize: 16, + paddingRight: 12 + 4, + }, + inputIOS: { + color: t.atoms.text.color, + fontSize: 16, + paddingRight: 12 + 4, + }, + }} + /> + + <View + style={[ + a.absolute, + a.inset_0, + {left: 'auto'}, + {pointerEvents: 'none'}, + a.align_center, + a.justify_center, + ]}> + <ChevronDown fill={t.atoms.text.color} size="xs" /> + </View> + </View> + </View> + <View style={{height: insets.bottom}} /> </ErrorBoundary> </CenteredView> ) } - -const styles = StyleSheet.create({ - container: { - height: '100%', - }, - hero: { - flex: 2, - justifyContent: 'center', - alignItems: 'center', - }, - btns: { - paddingBottom: 40, - }, - title: { - textAlign: 'center', - fontSize: 68, - fontWeight: 'bold', - }, - subtitle: { - textAlign: 'center', - fontSize: 42, - fontWeight: 'bold', - }, - btn: { - borderRadius: 32, - paddingVertical: 16, - marginBottom: 20, - marginHorizontal: 20, - }, - btnLabel: { - textAlign: 'center', - fontSize: 21, - }, -}) diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index 8ef64099f..cdb72cc04 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -1,17 +1,22 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native' +import {Pressable, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from 'view/com/util/text/Text' -import {TextLink} from '../util/Link' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {CenteredView} from '../util/Views' -import {isWeb} from 'platform/detection' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {sanitizeAppLanguageSetting} from '#/locale/helpers' +import {APP_LANGUAGES} from '#/locale/languages' +import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {Trans} from '@lingui/macro' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' +import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' +import {InlineLink} from '#/components/Link' +import {Text} from '#/components/Typography' +import {CenteredView} from '../util/Views' export const SplashScreen = ({ onDismiss, @@ -22,10 +27,9 @@ export const SplashScreen = ({ onPressSignin: () => void onPressCreateAccount: () => void }) => { - const pal = usePalette('default') - const {isTabletOrMobile} = useWebMediaQueries() - const styles = useStyles() - const isMobileWeb = isWeb && isTabletOrMobile + const {_} = useLingui() + const t = useTheme() + const {isTabletOrMobile: isMobileWeb} = useWebMediaQueries() return ( <> @@ -44,153 +48,165 @@ export const SplashScreen = ({ icon="x" size={24} style={{ - color: String(pal.text.color), + color: String(t.atoms.text.color), }} /> </Pressable> )} - <CenteredView style={[styles.container, pal.view]}> + <CenteredView style={[a.h_full, a.flex_1]}> <View testID="noSessionView" style={[ - styles.containerInner, - isMobileWeb && styles.containerInnerMobile, - pal.border, - {alignItems: 'center'}, + a.h_full, + a.justify_center, + // @ts-ignore web only + {paddingBottom: '20vh'}, + isMobileWeb && a.pb_5xl, + t.atoms.border_contrast_medium, + a.align_center, + a.gap_5xl, ]}> <ErrorBoundary> - <Logo width={92} fill="sky" /> + <View style={[a.justify_center, a.align_center]}> + <Logo width={92} fill="sky" /> + + <View style={[a.pb_sm, a.pt_5xl]}> + <Logotype width={161} fill={t.atoms.text.color} /> + </View> - <View style={{paddingTop: 40, paddingBottom: 20}}> - <Logotype width={161} fill={pal.text.color} /> + <Text + style={[ + a.text_md, + a.font_semibold, + t.atoms.text_contrast_medium, + ]}> + <Trans>What's up?</Trans> + </Text> </View> - <View testID="signinOrCreateAccount" style={styles.btns}> - <TouchableOpacity + <View + testID="signinOrCreateAccount" + style={[a.w_full, {maxWidth: 320}]}> + <Button testID="createAccountButton" - style={[styles.btn, {backgroundColor: colors.blue3}]} onPress={onPressCreateAccount} - // TODO: web accessibility - accessibilityRole="button"> - <Text style={[s.white, styles.btnLabel]}> + accessibilityRole="button" + label={_(msg`Create new account`)} + accessibilityHint={_( + msg`Opens flow to create a new Bluesky account`, + )} + style={[a.mx_xl, a.mb_xl]} + size="large" + variant="solid" + color="primary"> + <ButtonText> <Trans>Create a new account</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity + </ButtonText> + </Button> + <Button testID="signInButton" - style={[styles.btn, pal.btn]} onPress={onPressSignin} - // TODO: web accessibility - accessibilityRole="button"> - <Text style={[pal.text, styles.btnLabel]}> - <Trans>Sign In</Trans> - </Text> - </TouchableOpacity> + label={_(msg`Sign in`)} + accessibilityHint={_( + msg`Opens flow to sign into your existing Bluesky account`, + )} + style={[a.mx_xl, a.mb_xl]} + size="large" + variant="solid" + color="secondary"> + <ButtonText> + <Trans>Sign in</Trans> + </ButtonText> + </Button> </View> </ErrorBoundary> </View> - <Footer styles={styles} /> + <Footer /> </CenteredView> </> ) } -function Footer({styles}: {styles: ReturnType<typeof useStyles>}) { - const pal = usePalette('default') +function Footer() { + const t = useTheme() + + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() + + const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) + + const onChangeAppLanguage = React.useCallback( + (ev: React.ChangeEvent<HTMLSelectElement>) => { + const value = ev.target.value + + if (!value) return + if (sanitizedLang !== value) { + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) + } + }, + [sanitizedLang, setLangPrefs], + ) return ( - <View style={[styles.footer, pal.view, pal.border]}> - <TextLink - href="https://bsky.social" - text="Business" - style={[styles.footerLink, pal.link]} - /> - <TextLink - href="https://bsky.social/about/blog" - text="Blog" - style={[styles.footerLink, pal.link]} - /> - <TextLink - href="https://bsky.social/about/join" - text="Jobs" - style={[styles.footerLink, pal.link]} - /> + <View + style={[ + a.absolute, + a.inset_0, + {top: 'auto'}, + a.p_xl, + a.border_t, + a.flex_row, + a.flex_wrap, + a.gap_xl, + a.flex_1, + t.atoms.border_contrast_medium, + ]}> + <InlineLink to="https://bsky.social"> + <Trans>Business</Trans> + </InlineLink> + <InlineLink to="https://bsky.social/about/blog"> + <Trans>Blog</Trans> + </InlineLink> + <InlineLink to="https://bsky.social/about/join"> + <Trans>Jobs</Trans> + </InlineLink> + + <View style={a.flex_1} /> + + <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}> + <Text aria-hidden={true} style={t.atoms.text_contrast_medium}> + {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} + </Text> + <ChevronDown + fill={t.atoms.text.color} + size="xs" + style={a.flex_shrink} + /> + + <select + value={sanitizedLang} + onChange={onChangeAppLanguage} + style={{ + cursor: 'pointer', + MozAppearance: 'none', + WebkitAppearance: 'none', + appearance: 'none', + position: 'absolute', + inset: 0, + width: '100%', + color: 'transparent', + background: 'transparent', + border: 0, + padding: 0, + }}> + {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ( + <option key={l.code2} value={l.code2}> + {l.name} + </option> + ))} + </select> + </View> </View> ) } -const useStyles = () => { - return StyleSheet.create({ - container: { - height: '100%', - }, - containerInner: { - height: '100%', - justifyContent: 'center', - // @ts-ignore web only - paddingBottom: '20vh', - paddingHorizontal: 20, - }, - containerInnerMobile: { - paddingBottom: 50, - }, - title: { - textAlign: 'center', - color: colors.blue3, - fontSize: 68, - fontWeight: 'bold', - paddingBottom: 10, - }, - titleMobile: { - textAlign: 'center', - color: colors.blue3, - fontSize: 58, - fontWeight: 'bold', - }, - subtitle: { - textAlign: 'center', - color: colors.gray5, - fontSize: 52, - fontWeight: 'bold', - paddingBottom: 30, - }, - subtitleMobile: { - textAlign: 'center', - color: colors.gray5, - fontSize: 42, - fontWeight: 'bold', - paddingBottom: 30, - }, - btns: { - gap: 10, - justifyContent: 'center', - paddingBottom: 40, - }, - btn: { - borderRadius: 30, - paddingHorizontal: 24, - paddingVertical: 12, - minWidth: 220, - }, - btnLabel: { - textAlign: 'center', - fontSize: 18, - }, - notice: { - paddingHorizontal: 40, - textAlign: 'center', - }, - footer: { - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - padding: 20, - borderTopWidth: 1, - flexDirection: 'row', - }, - footerLink: { - marginRight: 20, - }, - }) -} diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx deleted file mode 100644 index 5d452736a..000000000 --- a/src/view/com/auth/create/CreateAccount.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - ScrollView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useOnboardingDispatch} from '#/state/shell' -import {useSessionApi} from '#/state/session' -import {useCreateAccount, submit} from './state' -import {useServiceQuery} from '#/state/queries/service' -import { - usePreferencesSetBirthDateMutation, - useSetSaveFeedsMutation, - DEFAULT_PROD_FEEDS, -} from '#/state/queries/preferences' -import {FEEDBACK_FORM_URL, HITSLOP_10, IS_PROD} from '#/lib/constants' - -import {Step1} from './Step1' -import {Step2} from './Step2' -import {Step3} from './Step3' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {TextLink} from '../../util/Link' - -export function CreateAccount({onPressBack}: {onPressBack: () => void}) { - const {screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const [uiState, uiDispatch] = useCreateAccount() - const onboardingDispatch = useOnboardingDispatch() - const {createAccount} = useSessionApi() - const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() - const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() - const {isTabletOrDesktop} = useWebMediaQueries() - - React.useEffect(() => { - screen('CreateAccount') - }, [screen]) - - // fetch service info - // = - - const { - data: serviceInfo, - isFetching: serviceInfoIsFetching, - error: serviceInfoError, - refetch: refetchServiceInfo, - } = useServiceQuery(uiState.serviceUrl) - - React.useEffect(() => { - if (serviceInfo) { - uiDispatch({type: 'set-service-description', value: serviceInfo}) - uiDispatch({type: 'set-error', value: ''}) - } else if (serviceInfoError) { - uiDispatch({ - type: 'set-error', - value: _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - }) - } - }, [_, uiDispatch, serviceInfo, serviceInfoError]) - - // event handlers - // = - - const onPressBackInner = React.useCallback(() => { - if (uiState.canBack) { - uiDispatch({type: 'back'}) - } else { - onPressBack() - } - }, [uiState, uiDispatch, onPressBack]) - - const onPressNext = React.useCallback(async () => { - if (!uiState.canNext) { - return - } - if (uiState.step < 3) { - uiDispatch({type: 'next'}) - } else { - try { - await submit({ - onboardingDispatch, - createAccount, - uiState, - uiDispatch, - _, - }) - setBirthDate({birthDate: uiState.birthDate}) - if (IS_PROD(uiState.serviceUrl)) { - setSavedFeeds(DEFAULT_PROD_FEEDS) - } - } catch { - // dont need to handle here - } - } - }, [ - uiState, - uiDispatch, - onboardingDispatch, - createAccount, - setBirthDate, - setSavedFeeds, - _, - ]) - - // rendering - // = - - return ( - <LoggedOutLayout - leadin="" - title={_(msg`Create Account`)} - description={_(msg`We're so excited to have you join us!`)}> - <ScrollView - testID="createAccount" - style={pal.view} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag"> - <View style={styles.stepContainer}> - {uiState.step === 1 && ( - <Step1 uiState={uiState} uiDispatch={uiDispatch} /> - )} - {uiState.step === 2 && ( - <Step2 uiState={uiState} uiDispatch={uiDispatch} /> - )} - {uiState.step === 3 && ( - <Step3 uiState={uiState} uiDispatch={uiDispatch} /> - )} - </View> - <View style={[s.flexRow, s.pl20, s.pr20]}> - <TouchableOpacity - onPress={onPressBackInner} - testID="backBtn" - accessibilityRole="button" - hitSlop={HITSLOP_10}> - <Text type="xl" style={pal.link}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {uiState.canNext ? ( - <TouchableOpacity - testID="nextBtn" - onPress={onPressNext} - accessibilityRole="button" - hitSlop={HITSLOP_10}> - {uiState.isProcessing ? ( - <ActivityIndicator /> - ) : ( - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - )} - </TouchableOpacity> - ) : serviceInfoError ? ( - <TouchableOpacity - testID="retryConnectBtn" - onPress={() => refetchServiceInfo()} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry`)} - accessibilityHint="" - accessibilityLiveRegion="polite" - hitSlop={HITSLOP_10}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Retry</Trans> - </Text> - </TouchableOpacity> - ) : serviceInfoIsFetching ? ( - <> - <ActivityIndicator color="#fff" /> - <Text type="xl" style={[pal.text, s.pr5]}> - <Trans>Connecting...</Trans> - </Text> - </> - ) : undefined} - </View> - - <View style={styles.stepContainer}> - <View - style={[ - s.flexRow, - s.alignCenter, - pal.viewLight, - {borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12}, - ]}> - <Text type="md" style={pal.textLight}> - <Trans>Having trouble?</Trans>{' '} - </Text> - <TextLink - type="md" - style={pal.link} - text={_(msg`Contact support`)} - href={FEEDBACK_FORM_URL({email: uiState.email})} - /> - </View> - </View> - - <View style={{height: isTabletOrDesktop ? 50 : 400}} /> - </ScrollView> - </LoggedOutLayout> - ) -} - -const styles = StyleSheet.create({ - stepContainer: { - paddingHorizontal: 20, - paddingVertical: 20, - }, -}) diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx deleted file mode 100644 index 2c7d60818..000000000 --- a/src/view/com/auth/create/Policies.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {TextLink} from '../../util/Link' -import {Text} from '../../util/text/Text' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const Policies = ({ - serviceDescription, - needsGuardian, -}: { - serviceDescription: ServiceDescription - needsGuardian: boolean -}) => { - const pal = usePalette('default') - if (!serviceDescription) { - return <View /> - } - const tos = validWebLink(serviceDescription.links?.termsOfService) - const pp = validWebLink(serviceDescription.links?.privacyPolicy) - if (!tos && !pp) { - return ( - <View style={[styles.policies, {flexDirection: 'row'}]}> - <View - style={[ - styles.errorIcon, - {borderColor: pal.colors.text, marginTop: 1}, - ]}> - <FontAwesomeIcon - icon="exclamation" - style={pal.textLight as FontAwesomeIconStyle} - size={10} - /> - </View> - <Text style={[pal.textLight, s.pl5, s.flex1]}> - This service has not provided terms of service or a privacy policy. - </Text> - </View> - ) - } - const els = [] - if (tos) { - els.push( - <TextLink - key="tos" - href={tos} - text="Terms of Service" - style={[pal.link, s.underline]} - />, - ) - } - if (pp) { - els.push( - <TextLink - key="pp" - href={pp} - text="Privacy Policy" - style={[pal.link, s.underline]} - />, - ) - } - if (els.length === 2) { - els.splice( - 1, - 0, - <Text key="and" style={pal.textLight}> - {' '} - and{' '} - </Text>, - ) - } - return ( - <View style={styles.policies}> - <Text style={pal.textLight}> - By creating an account you agree to the {els}. - </Text> - {needsGuardian && ( - <Text style={[pal.textLight, s.bold]}> - If you are not yet an adult according to the laws of your country, - your parent or legal guardian must read these Terms on your behalf. - </Text> - )} - </View> - ) -} - -function validWebLink(url?: string): string | undefined { - return url && (url.startsWith('http://') || url.startsWith('https://')) - ? url - : undefined -} - -const styles = StyleSheet.create({ - policies: { - flexDirection: 'column', - gap: 8, - }, - errorIcon: { - borderWidth: 1, - borderColor: colors.white, - borderRadius: 30, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - }, -}) diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx deleted file mode 100644 index a7abbfaa8..000000000 --- a/src/view/com/auth/create/Step1.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - Keyboard, - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' -import {CreateAccountState, CreateAccountDispatch, is18} from './state' -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' -import {TextInput} from '../util/TextInput' -import {Policies} from './Policies' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {isWeb} from 'platform/detection' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import {logger} from '#/logger' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' -import {toNiceDomain} from '#/lib/strings/url-helpers' - -function sanitizeDate(date: Date): Date { - if (!date || date.toString() === 'Invalid Date') { - logger.error(`Create account: handled invalid date for birthDate`, { - hasDate: !!date, - }) - return new Date() - } - return date -} - -export function Step1({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {openModal} = useModalControls() - const serverInputControl = useDialogControl() - - const onPressSelectService = React.useCallback(() => { - serverInputControl.open() - Keyboard.dismiss() - }, [serverInputControl]) - - const onPressWaitlist = React.useCallback(() => { - openModal({name: 'waitlist'}) - }, [openModal]) - - const birthDate = React.useMemo(() => { - return sanitizeDate(uiState.birthDate) - }, [uiState.birthDate]) - - return ( - <View> - <ServerInputDialog - control={serverInputControl} - onSelect={url => uiDispatch({type: 'set-service-url', value: url})} - /> - <StepHeader uiState={uiState} title={_(msg`Your account`)} /> - - <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]}> - <Trans>Hosting provider</Trans> - </Text> - <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}> - <View - style={[ - pal.borderDark, - {flexDirection: 'row', alignItems: 'center'}, - ]}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, {marginLeft: 14}]} - /> - <TouchableOpacity - testID="selectServiceButton" - style={{ - flexDirection: 'row', - flex: 1, - alignItems: 'center', - }} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)}> - <Text - type="xl" - style={[ - pal.text, - { - flex: 1, - paddingVertical: 10, - paddingRight: 12, - paddingLeft: 10, - }, - ]}> - {toNiceDomain(uiState.serviceUrl)} - </Text> - <View - style={[ - pal.btn, - { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 6, - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - ]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.textLight as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - </View> - </View> - </View> - - {!uiState.serviceDescription ? ( - <ActivityIndicator /> - ) : ( - <> - {uiState.isInviteCodeRequired && ( - <View style={s.pb20}> - <Text type="md-medium" style={[pal.text, s.mb2]}> - <Trans>Invite code</Trans> - </Text> - <TextInput - testID="inviteCodeInput" - icon="ticket" - placeholder={_(msg`Required for this provider`)} - value={uiState.inviteCode} - editable - onChange={value => uiDispatch({type: 'set-invite-code', value})} - accessibilityLabel={_(msg`Invite code`)} - accessibilityHint={_(msg`Input invite code to proceed`)} - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - autoFocus={true} - /> - </View> - )} - - {!uiState.inviteCode && uiState.isInviteCodeRequired ? ( - <View style={[s.flexRow, s.alignCenter]}> - <Text style={pal.text}> - <Trans>Don't have an invite code?</Trans>{' '} - </Text> - <TouchableWithoutFeedback - onPress={onPressWaitlist} - accessibilityLabel={_(msg`Join the waitlist.`)} - accessibilityHint=""> - <View style={styles.touchable}> - <Text style={pal.link}> - <Trans>Join the waitlist.</Trans> - </Text> - </View> - </TouchableWithoutFeedback> - </View> - ) : ( - <> - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="email"> - <Trans>Email address</Trans> - </Text> - <TextInput - testID="emailInput" - icon="envelope" - placeholder={_(msg`Enter your email address`)} - value={uiState.email} - editable - onChange={value => uiDispatch({type: 'set-email', value})} - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_(msg`Input email for Bluesky account`)} - accessibilityLabelledBy="email" - autoCapitalize="none" - autoComplete="email" - autoCorrect={false} - autoFocus={!uiState.isInviteCodeRequired} - /> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="password"> - <Trans>Password</Trans> - </Text> - <TextInput - testID="passwordInput" - icon="lock" - placeholder={_(msg`Choose your password`)} - value={uiState.password} - editable - secureTextEntry - onChange={value => uiDispatch({type: 'set-password', value})} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Set password`)} - accessibilityLabelledBy="password" - autoCapitalize="none" - autoComplete="new-password" - autoCorrect={false} - /> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="birthDate"> - <Trans>Your birth date</Trans> - </Text> - <DateInput - handleAsUTC - testID="birthdayInput" - value={birthDate} - onChange={value => - uiDispatch({type: 'set-birth-date', value}) - } - buttonType="default-light" - buttonStyle={[pal.border, styles.dateInputButton]} - buttonLabelType="lg" - accessibilityLabel={_(msg`Birthday`)} - accessibilityHint={_(msg`Enter your birth date`)} - accessibilityLabelledBy="birthDate" - /> - </View> - - {uiState.serviceDescription && ( - <Policies - serviceDescription={uiState.serviceDescription} - needsGuardian={!is18(uiState)} - /> - )} - </> - )} - </> - )} - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} - </View> - ) -} - -const styles = StyleSheet.create({ - error: { - borderRadius: 6, - marginTop: 10, - }, - dateInputButton: { - borderWidth: 1, - borderRadius: 6, - paddingVertical: 14, - }, - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. - touchable: { - ...(isWeb && {cursor: 'pointer'}), - }, -}) diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx deleted file mode 100644 index 2e16b13bb..000000000 --- a/src/view/com/auth/create/Step2.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native' -import RNPickerSelect from 'react-native-picker-select' -import { - CreateAccountState, - CreateAccountDispatch, - requestVerificationCode, -} from './state' -import {Text} from 'view/com/util/text/Text' -import {StepHeader} from './StepHeader' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {TextInput} from '../util/TextInput' -import {Button} from '../../util/forms/Button' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {isAndroid, isWeb} from 'platform/detection' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import parsePhoneNumber from 'libphonenumber-js' -import {COUNTRY_CODES} from '#/lib/country-codes' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {HITSLOP_10} from '#/lib/constants' - -export function Step2({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - - const onPressRequest = React.useCallback(() => { - const phoneNumber = parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - ) - if (phoneNumber && phoneNumber.isValid()) { - requestVerificationCode({uiState, uiDispatch, _}) - } else { - uiDispatch({ - type: 'set-error', - value: _( - msg`There's something wrong with this number. Please choose your country and enter your full phone number!`, - ), - }) - } - }, [uiState, uiDispatch, _]) - - const onPressRetry = React.useCallback(() => { - uiDispatch({type: 'set-has-requested-verification-code', value: false}) - }, [uiDispatch]) - - const phoneNumberFormatted = React.useMemo( - () => - uiState.hasRequestedVerificationCode - ? parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - )?.formatInternational() - : '', - [ - uiState.hasRequestedVerificationCode, - uiState.verificationPhone, - uiState.phoneCountry, - ], - ) - - return ( - <View> - <StepHeader uiState={uiState} title={_(msg`SMS verification`)} /> - - {!uiState.hasRequestedVerificationCode ? ( - <> - <View style={s.pb10}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="phoneCountry"> - <Trans>Country</Trans> - </Text> - <View - style={[ - {position: 'relative'}, - isAndroid && { - borderWidth: 1, - borderColor: pal.border.borderColor, - borderRadius: 4, - }, - ]}> - <RNPickerSelect - placeholder={{}} - value={uiState.phoneCountry} - onValueChange={value => - uiDispatch({type: 'set-phone-country', value}) - } - items={COUNTRY_CODES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 21, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 4, - }, - inputIOS: { - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderWidth: 1, - borderColor: pal.border.borderColor, - borderRadius: 4, - }, - inputWeb: { - // @ts-ignore web only - cursor: 'pointer', - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 1, - borderColor: pal.border.borderColor, - backgroundColor: pal.view.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '500', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 4, - }, - }} - accessibilityLabel={_(msg`Select your phone's country`)} - accessibilityHint="" - accessibilityLabelledBy="phoneCountry" - /> - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View style={s.pb20}> - <Text - type="md-medium" - style={[pal.text, s.mb2]} - nativeID="phoneNumber"> - <Trans>Phone number</Trans> - </Text> - <TextInput - testID="phoneInput" - icon="phone" - placeholder={_(msg`Enter your phone number`)} - value={uiState.verificationPhone} - editable - onChange={value => - uiDispatch({type: 'set-verification-phone', value}) - } - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_( - msg`Input phone number for SMS verification`, - )} - accessibilityLabelledBy="phoneNumber" - keyboardType="phone-pad" - autoCapitalize="none" - autoComplete="tel" - autoCorrect={false} - autoFocus={true} - /> - <Text type="sm" style={[pal.textLight, s.mt5]}> - <Trans> - Please enter a phone number that can receive SMS text messages. - </Trans> - </Text> - </View> - - <View style={isMobile ? {} : {flexDirection: 'row'}}> - {uiState.isProcessing ? ( - <ActivityIndicator /> - ) : ( - <Button - testID="requestCodeBtn" - type="primary" - label={_(msg`Request code`)} - labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []} - style={ - isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {} - } - onPress={onPressRequest} - /> - )} - </View> - </> - ) : ( - <> - <View style={s.pb20}> - <View - style={[ - s.flexRow, - s.mb5, - s.alignCenter, - {justifyContent: 'space-between'}, - ]}> - <Text - type="md-medium" - style={pal.text} - nativeID="verificationCode"> - <Trans>Verification code</Trans>{' '} - </Text> - <TouchableWithoutFeedback - onPress={onPressRetry} - accessibilityLabel={_(msg`Retry.`)} - accessibilityHint="" - hitSlop={HITSLOP_10}> - <View style={styles.touchable}> - <Text - type="md-medium" - style={pal.link} - nativeID="verificationCode"> - <Trans>Retry</Trans> - </Text> - </View> - </TouchableWithoutFeedback> - </View> - <TextInput - testID="codeInput" - icon="hashtag" - placeholder={_(msg`XXXXXX`)} - value={uiState.verificationCode} - editable - onChange={value => - uiDispatch({type: 'set-verification-code', value}) - } - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_( - msg`Input the verification code we have texted to you`, - )} - accessibilityLabelledBy="verificationCode" - keyboardType="phone-pad" - autoCapitalize="none" - autoComplete="one-time-code" - textContentType="oneTimeCode" - autoCorrect={false} - autoFocus={true} - /> - <Text type="sm" style={[pal.textLight, s.mt5]}> - <Trans> - Please enter the verification code sent to{' '} - {phoneNumberFormatted}. - </Trans> - </Text> - </View> - </> - )} - - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} - </View> - ) -} - -const styles = StyleSheet.create({ - error: { - borderRadius: 6, - marginTop: 10, - }, - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. - touchable: { - ...(isWeb && {cursor: 'pointer'}), - }, -}) diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx deleted file mode 100644 index 3a52abf80..000000000 --- a/src/view/com/auth/create/Step3.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {CreateAccountState, CreateAccountDispatch} from './state' -import {Text} from 'view/com/util/text/Text' -import {StepHeader} from './StepHeader' -import {s} from 'lib/styles' -import {TextInput} from '../util/TextInput' -import {createFullHandle} from 'lib/strings/handles' -import {usePalette} from 'lib/hooks/usePalette' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -/** STEP 3: Your user handle - * @field User handle - */ -export function Step3({ - uiState, - uiDispatch, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch -}) { - const pal = usePalette('default') - const {_} = useLingui() - return ( - <View> - <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> - <View style={s.pb10}> - <TextInput - testID="handleInput" - icon="at" - placeholder="e.g. alice" - value={uiState.handle} - editable - autoFocus - autoComplete="off" - autoCorrect={false} - onChange={value => uiDispatch({type: 'set-handle', value})} - // TODO: Add explicit text label - accessibilityLabel={_(msg`User handle`)} - accessibilityHint={_(msg`Input your user handle`)} - /> - <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> - <Trans>Your full handle will be</Trans>{' '} - <Text type="lg-bold" style={pal.text}> - @{createFullHandle(uiState.handle, uiState.userDomain)} - </Text> - </Text> - </View> - {uiState.error ? ( - <ErrorMessage message={uiState.error} style={styles.error} /> - ) : undefined} - </View> - ) -} - -const styles = StyleSheet.create({ - error: { - borderRadius: 6, - }, -}) diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx deleted file mode 100644 index af6bf5478..000000000 --- a/src/view/com/auth/create/StepHeader.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Text} from 'view/com/util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {Trans} from '@lingui/macro' -import {CreateAccountState} from './state' - -export function StepHeader({ - uiState, - title, - children, -}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) { - const pal = usePalette('default') - const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2 - return ( - <View style={styles.container}> - <View> - <Text type="lg" style={[pal.textLight]}> - {uiState.step === 3 ? ( - <Trans>Last step!</Trans> - ) : ( - <Trans> - Step {uiState.step} of {numSteps} - </Trans> - )} - </Text> - - <Text style={[pal.text]} type="title-xl"> - {title} - </Text> - </View> - {children} - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 20, - }, -}) diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts deleted file mode 100644 index e8a7cd4ed..000000000 --- a/src/view/com/auth/create/state.ts +++ /dev/null @@ -1,350 +0,0 @@ -import {useReducer} from 'react' -import { - ComAtprotoServerDescribeServer, - ComAtprotoServerCreateAccount, - BskyAgent, -} from '@atproto/api' -import {I18nContext, useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' -import * as EmailValidator from 'email-validator' -import {getAge} from 'lib/strings/time' -import {logger} from '#/logger' -import {createFullHandle} from '#/lib/strings/handles' -import {cleanError} from '#/lib/strings/errors' -import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding' -import {ApiContext as SessionApiContext} from '#/state/session' -import {DEFAULT_SERVICE} from '#/lib/constants' -import parsePhoneNumber, {CountryCode} from 'libphonenumber-js' - -export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema -const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago - -export type CreateAccountAction = - | {type: 'set-step'; value: number} - | {type: 'set-error'; value: string | undefined} - | {type: 'set-processing'; value: boolean} - | {type: 'set-service-url'; value: string} - | {type: 'set-service-description'; value: ServiceDescription | undefined} - | {type: 'set-user-domain'; value: string} - | {type: 'set-invite-code'; value: string} - | {type: 'set-email'; value: string} - | {type: 'set-password'; value: string} - | {type: 'set-phone-country'; value: CountryCode} - | {type: 'set-verification-phone'; value: string} - | {type: 'set-verification-code'; value: string} - | {type: 'set-has-requested-verification-code'; value: boolean} - | {type: 'set-handle'; value: string} - | {type: 'set-birth-date'; value: Date} - | {type: 'next'} - | {type: 'back'} - -export interface CreateAccountState { - // state - step: number - error: string | undefined - isProcessing: boolean - serviceUrl: string - serviceDescription: ServiceDescription | undefined - userDomain: string - inviteCode: string - email: string - password: string - phoneCountry: CountryCode - verificationPhone: string - verificationCode: string - hasRequestedVerificationCode: boolean - handle: string - birthDate: Date - - // computed - canBack: boolean - canNext: boolean - isInviteCodeRequired: boolean - isPhoneVerificationRequired: boolean -} - -export type CreateAccountDispatch = (action: CreateAccountAction) => void - -export function useCreateAccount() { - const {_} = useLingui() - return useReducer(createReducer({_}), { - step: 1, - error: undefined, - isProcessing: false, - serviceUrl: DEFAULT_SERVICE, - serviceDescription: undefined, - userDomain: '', - inviteCode: '', - email: '', - password: '', - phoneCountry: 'US', - verificationPhone: '', - verificationCode: '', - hasRequestedVerificationCode: false, - handle: '', - birthDate: DEFAULT_DATE, - - canBack: false, - canNext: false, - isInviteCodeRequired: false, - isPhoneVerificationRequired: false, - }) -} - -export async function requestVerificationCode({ - uiState, - uiDispatch, - _, -}: { - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch - _: I18nContext['_'] -}) { - const phoneNumber = parsePhoneNumber( - uiState.verificationPhone, - uiState.phoneCountry, - )?.number - if (!phoneNumber) { - return - } - uiDispatch({type: 'set-error', value: ''}) - uiDispatch({type: 'set-processing', value: true}) - uiDispatch({type: 'set-verification-phone', value: phoneNumber}) - try { - const agent = new BskyAgent({service: uiState.serviceUrl}) - await agent.com.atproto.temp.requestPhoneVerification({ - phoneNumber, - }) - uiDispatch({type: 'set-has-requested-verification-code', value: true}) - } catch (e: any) { - logger.error( - `Failed to request sms verification code (${e.status} status)`, - {message: e}, - ) - uiDispatch({type: 'set-error', value: cleanError(e.toString())}) - } - uiDispatch({type: 'set-processing', value: false}) -} - -export async function submit({ - createAccount, - onboardingDispatch, - uiState, - uiDispatch, - _, -}: { - createAccount: SessionApiContext['createAccount'] - onboardingDispatch: OnboardingDispatchContext - uiState: CreateAccountState - uiDispatch: CreateAccountDispatch - _: I18nContext['_'] -}) { - if (!uiState.email) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please enter your email.`), - }) - } - if (!EmailValidator.validate(uiState.email)) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Your email appears to be invalid.`), - }) - } - if (!uiState.password) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your password.`), - }) - } - if ( - uiState.isPhoneVerificationRequired && - (!uiState.verificationPhone || !uiState.verificationCode) - ) { - uiDispatch({type: 'set-step', value: 2}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please enter the code you received by SMS.`), - }) - } - if (!uiState.handle) { - uiDispatch({type: 'set-step', value: 3}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your handle.`), - }) - } - uiDispatch({type: 'set-error', value: ''}) - uiDispatch({type: 'set-processing', value: true}) - - try { - onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view - await createAccount({ - service: uiState.serviceUrl, - email: uiState.email, - handle: createFullHandle(uiState.handle, uiState.userDomain), - password: uiState.password, - inviteCode: uiState.inviteCode.trim(), - verificationPhone: uiState.verificationPhone.trim(), - verificationCode: uiState.verificationCode.trim(), - }) - } catch (e: any) { - onboardingDispatch({type: 'skip'}) // undo starting the onboard - let errMsg = e.toString() - if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { - errMsg = _( - msg`Invite code not accepted. Check that you input it correctly and try again.`, - ) - uiDispatch({type: 'set-step', value: 1}) - } else if (e.error === 'InvalidPhoneVerification') { - uiDispatch({type: 'set-step', value: 2}) - } - - if ([400, 429].includes(e.status)) { - logger.warn('Failed to create account', {message: e}) - } else { - logger.error(`Failed to create account (${e.status} status)`, { - message: e, - }) - } - - uiDispatch({type: 'set-processing', value: false}) - uiDispatch({type: 'set-error', value: cleanError(errMsg)}) - throw e - } -} - -export function is13(state: CreateAccountState) { - return getAge(state.birthDate) >= 13 -} - -export function is18(state: CreateAccountState) { - return getAge(state.birthDate) >= 18 -} - -function createReducer({_}: {_: I18nContext['_']}) { - return function reducer( - state: CreateAccountState, - action: CreateAccountAction, - ): CreateAccountState { - switch (action.type) { - case 'set-step': { - return compute({...state, step: action.value}) - } - case 'set-error': { - return compute({...state, error: action.value}) - } - case 'set-processing': { - return compute({...state, isProcessing: action.value}) - } - case 'set-service-url': { - return compute({ - ...state, - serviceUrl: action.value, - serviceDescription: - state.serviceUrl !== action.value - ? undefined - : state.serviceDescription, - }) - } - case 'set-service-description': { - return compute({ - ...state, - serviceDescription: action.value, - userDomain: action.value?.availableUserDomains[0] || '', - }) - } - case 'set-user-domain': { - return compute({...state, userDomain: action.value}) - } - case 'set-invite-code': { - return compute({...state, inviteCode: action.value}) - } - case 'set-email': { - return compute({...state, email: action.value}) - } - case 'set-password': { - return compute({...state, password: action.value}) - } - case 'set-phone-country': { - return compute({...state, phoneCountry: action.value}) - } - case 'set-verification-phone': { - return compute({ - ...state, - verificationPhone: action.value, - hasRequestedVerificationCode: false, - }) - } - case 'set-verification-code': { - return compute({...state, verificationCode: action.value.trim()}) - } - case 'set-has-requested-verification-code': { - return compute({...state, hasRequestedVerificationCode: action.value}) - } - case 'set-handle': { - return compute({...state, handle: action.value}) - } - case 'set-birth-date': { - return compute({...state, birthDate: action.value}) - } - case 'next': { - if (state.step === 1) { - if (!is13(state)) { - return compute({ - ...state, - error: _( - msg`Unfortunately, you do not meet the requirements to create an account.`, - ), - }) - } - } - let increment = 1 - if (state.step === 1 && !state.isPhoneVerificationRequired) { - increment = 2 - } - return compute({...state, error: '', step: state.step + increment}) - } - case 'back': { - let decrement = 1 - if (state.step === 3 && !state.isPhoneVerificationRequired) { - decrement = 2 - } - return compute({...state, error: '', step: state.step - decrement}) - } - } - } -} - -function compute(state: CreateAccountState): CreateAccountState { - let canNext = true - if (state.step === 1) { - canNext = - !!state.serviceDescription && - (!state.isInviteCodeRequired || !!state.inviteCode) && - !!state.email && - !!state.password - } else if (state.step === 2) { - canNext = - !state.isPhoneVerificationRequired || - (!!state.verificationPhone && - isValidVerificationCode(state.verificationCode)) - } else if (state.step === 3) { - canNext = !!state.handle - } - return { - ...state, - canBack: state.step > 1, - canNext, - isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, - isPhoneVerificationRequired: - !!state.serviceDescription?.phoneVerificationRequired, - } -} - -function isValidVerificationCode(str: string): boolean { - return /[0-9]{6}/.test(str) -} diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx deleted file mode 100644 index 32cd8315d..000000000 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react' -import {ScrollView, TouchableOpacity, View} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {UserAvatar} from '../../util/UserAvatar' -import {s, colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {styles} from './styles' -import {useSession, useSessionApi, SessionAccount} from '#/state/session' -import {useProfileQuery} from '#/state/queries/profile' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import * as Toast from '#/view/com/util/Toast' - -function AccountItem({ - account, - onSelect, - isCurrentAccount, -}: { - account: SessionAccount - onSelect: (account: SessionAccount) => void - isCurrentAccount: boolean -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {data: profile} = useProfileQuery({did: account.did}) - - const onPress = React.useCallback(() => { - onSelect(account) - }, [account, onSelect]) - - return ( - <TouchableOpacity - testID={`chooseAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, pal.border, styles.account]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign in as ${account.handle}`)} - accessibilityHint={_(msg`Double tap to sign in`)}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <View style={s.p10}> - <UserAvatar avatar={profile?.avatar} size={30} /> - </View> - <Text style={styles.accountText}> - <Text type="lg-bold" style={pal.text}> - {profile?.displayName || account.handle}{' '} - </Text> - <Text type="lg" style={[pal.textLight]}> - {account.handle} - </Text> - </Text> - {isCurrentAccount ? ( - <FontAwesomeIcon - icon="check" - size={16} - style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]} - /> - ) : ( - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - )} - </View> - </TouchableOpacity> - ) -} -export const ChooseAccountForm = ({ - onSelectAccount, - onPressBack, -}: { - onSelectAccount: (account?: SessionAccount) => void - onPressBack: () => void -}) => { - const {track, screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const {accounts, currentAccount} = useSession() - const {initSession} = useSessionApi() - const {setShowLoggedOut} = useLoggedOutViewControls() - - React.useEffect(() => { - screen('Choose Account') - }, [screen]) - - const onSelect = React.useCallback( - async (account: SessionAccount) => { - if (account.accessJwt) { - if (account.did === currentAccount?.did) { - setShowLoggedOut(false) - Toast.show(_(msg`Already signed in as @${account.handle}`)) - } else { - await initSession(account) - track('Sign In', {resumedSession: true}) - setTimeout(() => { - Toast.show(_(msg`Signed in as @${account.handle}`)) - }, 100) - } - } else { - onSelectAccount(account) - } - }, - [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _], - ) - - return ( - <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> - <Text - type="2xl-medium" - style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}> - <Trans>Sign in as...</Trans> - </Text> - {accounts.map(account => ( - <AccountItem - key={account.did} - account={account} - onSelect={onSelect} - isCurrentAccount={account.did === currentAccount?.did} - /> - ))} - <TouchableOpacity - testID="chooseNewAccountBtn" - style={[pal.view, pal.border, styles.account, styles.accountLast]} - onPress={() => onSelectAccount(undefined)} - accessibilityRole="button" - accessibilityLabel={_(msg`Login to account that is not listed`)} - accessibilityHint=""> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <Text style={[styles.accountText, styles.accountTextOther]}> - <Text type="lg" style={pal.text}> - <Trans>Other account</Trans> - </Text> - </Text> - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - </View> - </TouchableOpacity> - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - </View> - </ScrollView> - ) -} diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx deleted file mode 100644 index 322da2b8f..000000000 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React, {useState, useEffect} from 'react' -import { - ActivityIndicator, - Keyboard, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import * as EmailValidator from 'email-validator' -import {BskyAgent} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {cleanError} from 'lib/strings/errors' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {styles} from './styles' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const ForgotPasswordForm = ({ - error, - serviceUrl, - serviceDescription, - setError, - setServiceUrl, - onPressBack, - onEmailSent, -}: { - error: string - serviceUrl: string - serviceDescription: ServiceDescription | undefined - setError: (v: string) => void - setServiceUrl: (v: string) => void - onPressBack: () => void - onEmailSent: () => void -}) => { - const pal = usePalette('default') - const theme = useTheme() - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [email, setEmail] = useState<string>('') - const {screen} = useAnalytics() - const {_} = useLingui() - const serverInputControl = useDialogControl() - - useEffect(() => { - screen('Signin:ForgotPassword') - }, [screen]) - - const onPressSelectService = React.useCallback(() => { - serverInputControl.open() - Keyboard.dismiss() - }, [serverInputControl]) - - const onPressNext = async () => { - if (!EmailValidator.validate(email)) { - return setError(_(msg`Your email appears to be invalid.`)) - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.requestPasswordReset({email}) - onEmailSent() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to request password reset', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - return ( - <> - <View> - <ServerInputDialog - control={serverInputControl} - onSelect={setServiceUrl} - /> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Reset password</Trans> - </Text> - <Text type="md" style={[pal.text, styles.instructions]}> - <Trans> - Enter the email you used to create your account. We'll send you a - "reset code" so you can set a new password. - </Trans> - </Text> - <View - testID="forgotPasswordView" - style={[pal.borderDark, pal.view, styles.group]}> - <TouchableOpacity - testID="forgotPasswordSelectServiceButton" - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Hosting provider`)} - accessibilityHint={_( - msg`Sets hosting provider for password reset`, - )}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, styles.groupContentIcon]} - /> - <Text style={[pal.text, styles.textInput]} numberOfLines={1}> - {toNiceDomain(serviceUrl)} - </Text> - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="envelope" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="forgotPasswordEmail" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Email address`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoFocus - autoCorrect={false} - keyboardAppearance={theme.colorScheme} - value={email} - onChangeText={setEmail} - editable={!isProcessing} - accessibilityLabel={_(msg`Email`)} - accessibilityHint={_(msg`Sets email for password reset`)} - /> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {!serviceDescription || isProcessing ? ( - <ActivityIndicator /> - ) : !email ? ( - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> - <Trans>Next</Trans> - </Text> - ) : ( - <TouchableOpacity - testID="newPasswordButton" - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - )} - {!serviceDescription || isProcessing ? ( - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Processing...</Trans> - </Text> - ) : undefined} - </View> - <View - style={[ - s.flexRow, - s.alignCenter, - s.mt20, - s.mb20, - pal.border, - s.borderBottom1, - {alignSelf: 'center', width: '90%'}, - ]} - /> - <View style={[s.flexRow, s.justifyCenter]}> - <TouchableOpacity - testID="skipSendEmailButton" - onPress={onEmailSent} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl" style={[pal.link, s.pr5]}> - <Trans>Already have a code?</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx deleted file mode 100644 index bc931ac04..000000000 --- a/src/view/com/auth/login/Login.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, {useState, useEffect} from 'react' -import {KeyboardAvoidingView} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {DEFAULT_SERVICE} from '#/lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {logger} from '#/logger' -import {ChooseAccountForm} from './ChooseAccountForm' -import {LoginForm} from './LoginForm' -import {ForgotPasswordForm} from './ForgotPasswordForm' -import {SetNewPasswordForm} from './SetNewPasswordForm' -import {PasswordUpdatedForm} from './PasswordUpdatedForm' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' -import {useSession, SessionAccount} from '#/state/session' -import {useServiceQuery} from '#/state/queries/service' -import {useLoggedOutView} from '#/state/shell/logged-out' - -enum Forms { - Login, - ChooseAccount, - ForgotPassword, - SetNewPassword, - PasswordUpdated, -} - -export const Login = ({onPressBack}: {onPressBack: () => void}) => { - const {_} = useLingui() - const pal = usePalette('default') - - const {accounts} = useSession() - const {track} = useAnalytics() - const {requestedAccountSwitchTo} = useLoggedOutView() - const requestedAccount = accounts.find( - a => a.did === requestedAccountSwitchTo, - ) - - const [error, setError] = useState<string>('') - const [serviceUrl, setServiceUrl] = useState<string>( - requestedAccount?.service || DEFAULT_SERVICE, - ) - const [initialHandle, setInitialHandle] = useState<string>( - requestedAccount?.handle || '', - ) - const [currentForm, setCurrentForm] = useState<Forms>( - requestedAccount - ? Forms.Login - : accounts.length - ? Forms.ChooseAccount - : Forms.Login, - ) - - const { - data: serviceDescription, - error: serviceError, - refetch: refetchService, - } = useServiceQuery(serviceUrl) - - const onSelectAccount = (account?: SessionAccount) => { - if (account?.service) { - setServiceUrl(account.service) - } - setInitialHandle(account?.handle || '') - setCurrentForm(Forms.Login) - } - - const gotoForm = (form: Forms) => () => { - setError('') - setCurrentForm(form) - } - - useEffect(() => { - if (serviceError) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - logger.warn(`Failed to fetch service description for ${serviceUrl}`, { - error: String(serviceError), - }) - } else { - setError('') - } - }, [serviceError, serviceUrl, _]) - - const onPressRetryConnect = () => refetchService() - const onPressForgotPassword = () => { - track('Signin:PressedForgotPassword') - setCurrentForm(Forms.ForgotPassword) - } - - return ( - <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}> - {currentForm === Forms.Login ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in`)} - description={_(msg`Enter your username and password`)}> - <LoginForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - initialHandle={initialHandle} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={onPressBack} - onPressForgotPassword={onPressForgotPassword} - onPressRetryConnect={onPressRetryConnect} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ChooseAccount ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Sign in as...`)} - description={_(msg`Select from an existing account`)}> - <ChooseAccountForm - onSelectAccount={onSelectAccount} - onPressBack={onPressBack} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.ForgotPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <ForgotPasswordForm - error={error} - serviceUrl={serviceUrl} - serviceDescription={serviceDescription} - setError={setError} - setServiceUrl={setServiceUrl} - onPressBack={gotoForm(Forms.Login)} - onEmailSent={gotoForm(Forms.SetNewPassword)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.SetNewPassword ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Forgot Password`)} - description={_(msg`Let's get your password reset!`)}> - <SetNewPasswordForm - error={error} - serviceUrl={serviceUrl} - setError={setError} - onPressBack={gotoForm(Forms.ForgotPassword)} - onPasswordSet={gotoForm(Forms.PasswordUpdated)} - /> - </LoggedOutLayout> - ) : undefined} - {currentForm === Forms.PasswordUpdated ? ( - <LoggedOutLayout - leadin="" - title={_(msg`Password updated`)} - description={_(msg`You can now sign in with your new password.`)}> - <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} /> - </LoggedOutLayout> - ) : undefined} - </KeyboardAvoidingView> - ) -} diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx deleted file mode 100644 index e480de7a4..000000000 --- a/src/view/com/auth/login/LoginForm.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, {useState, useRef} from 'react' -import { - ActivityIndicator, - Keyboard, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {createFullHandle} from 'lib/strings/handles' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {useSessionApi} from '#/state/session' -import {cleanError} from 'lib/strings/errors' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' -import {styles} from './styles' -import {useLingui} from '@lingui/react' -import {useDialogControl} from '#/components/Dialog' - -import {ServerInputDialog} from '../server-input' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const LoginForm = ({ - error, - serviceUrl, - serviceDescription, - initialHandle, - setError, - setServiceUrl, - onPressRetryConnect, - onPressBack, - onPressForgotPassword, -}: { - error: string - serviceUrl: string - serviceDescription: ServiceDescription | undefined - initialHandle: string - setError: (v: string) => void - setServiceUrl: (v: string) => void - onPressRetryConnect: () => void - onPressBack: () => void - onPressForgotPassword: () => void -}) => { - const {track} = useAnalytics() - const pal = usePalette('default') - const theme = useTheme() - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [identifier, setIdentifier] = useState<string>(initialHandle) - const [password, setPassword] = useState<string>('') - const passwordInputRef = useRef<TextInput>(null) - const {_} = useLingui() - const {login} = useSessionApi() - const serverInputControl = useDialogControl() - - const onPressSelectService = () => { - serverInputControl.open() - Keyboard.dismiss() - track('Signin:PressedSelectService') - } - - const onPressNext = async () => { - Keyboard.dismiss() - setError('') - setIsProcessing(true) - - try { - // try to guess the handle if the user just gave their own username - let fullIdent = identifier - if ( - !identifier.includes('@') && // not an email - !identifier.includes('.') && // not a domain - serviceDescription && - serviceDescription.availableUserDomains.length > 0 - ) { - let matched = false - for (const domain of serviceDescription.availableUserDomains) { - if (fullIdent.endsWith(domain)) { - matched = true - } - } - if (!matched) { - fullIdent = createFullHandle( - identifier, - serviceDescription.availableUserDomains[0], - ) - } - } - - // TODO remove double login - await login({ - service: serviceUrl, - identifier: fullIdent, - password, - }) - } catch (e: any) { - const errMsg = e.toString() - setIsProcessing(false) - if (errMsg.includes('Authentication Required')) { - logger.info('Failed to login due to invalid credentials', { - error: errMsg, - }) - setError(_(msg`Invalid username or password`)) - } else if (isNetworkError(e)) { - logger.warn('Failed to login due to network error', {error: errMsg}) - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - logger.warn('Failed to login', {error: errMsg}) - setError(cleanError(errMsg)) - } - } - } - - const isReady = !!serviceDescription && !!identifier && !!password - return ( - <View testID="loginForm"> - <ServerInputDialog - control={serverInputControl} - onSelect={setServiceUrl} - /> - - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> - <Trans>Sign into</Trans> - </Text> - <View style={[pal.borderDark, styles.group]}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="globe" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TouchableOpacity - testID="loginSelectServiceButton" - style={styles.textBtn} - onPress={onPressSelectService} - accessibilityRole="button" - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)}> - <Text type="xl" style={[pal.text, styles.textBtnLabel]}> - {toNiceDomain(serviceUrl)} - </Text> - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> - <FontAwesomeIcon - icon="pen" - size={12} - style={pal.textLight as FontAwesomeIconStyle} - /> - </View> - </TouchableOpacity> - </View> - </View> - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> - <Trans>Account</Trans> - </Text> - <View style={[pal.borderDark, styles.group]}> - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="at" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="loginUsernameInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Username or email address`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoFocus - autoCorrect={false} - autoComplete="username" - returnKeyType="next" - textContentType="username" - onSubmitEditing={() => { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - keyboardAppearance={theme.colorScheme} - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityLabel={_(msg`Username or email address`)} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - </View> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="lock" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="loginPasswordInput" - ref={passwordInputRef} - style={[pal.text, styles.textInput]} - placeholder="Password" - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - autoComplete="password" - returnKeyType="done" - enablesReturnKeyAutomatically={true} - keyboardAppearance={theme.colorScheme} - secureTextEntry={true} - textContentType="password" - clearButtonMode="while-editing" - value={password} - onChangeText={setPassword} - onSubmitEditing={onPressNext} - blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing - editable={!isProcessing} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={ - identifier === '' - ? _(msg`Input your password`) - : _(msg`Input the password tied to ${identifier}`) - } - /> - <TouchableOpacity - testID="forgotPasswordButton" - style={styles.textInputInnerBtn} - onPress={onPressForgotPassword} - accessibilityRole="button" - accessibilityLabel={_(msg`Forgot password`)} - accessibilityHint={_(msg`Opens password reset form`)}> - <Text style={pal.link}> - <Trans>Forgot</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {!serviceDescription && error ? ( - <TouchableOpacity - testID="loginRetryButton" - onPress={onPressRetryConnect} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry`)} - accessibilityHint={_(msg`Retries login`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Retry</Trans> - </Text> - </TouchableOpacity> - ) : !serviceDescription ? ( - <> - <ActivityIndicator /> - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Connecting...</Trans> - </Text> - </> - ) : isProcessing ? ( - <ActivityIndicator /> - ) : isReady ? ( - <TouchableOpacity - testID="loginNextButton" - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - ) : undefined} - </View> - </View> - ) -} diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx deleted file mode 100644 index 71f750b14..000000000 --- a/src/view/com/auth/login/PasswordUpdatedForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, {useEffect} from 'react' -import {TouchableOpacity, View} from 'react-native' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {styles} from './styles' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -export const PasswordUpdatedForm = ({ - onPressNext, -}: { - onPressNext: () => void -}) => { - const {screen} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - - useEffect(() => { - screen('Signin:PasswordUpdatedForm') - }, [screen]) - - return ( - <> - <View> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Password updated!</Trans> - </Text> - <Text type="lg" style={[pal.text, styles.instructions]}> - <Trans>You can now sign in with your new password.</Trans> - </Text> - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <View style={s.flex1} /> - <TouchableOpacity - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Close alert`)} - accessibilityHint={_(msg`Closes password update alert`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Okay</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx deleted file mode 100644 index 6d1584c86..000000000 --- a/src/view/com/auth/login/SetNewPasswordForm.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, {useState, useEffect} from 'react' -import { - ActivityIndicator, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {BskyAgent} from '@atproto/api' -import {useAnalytics} from 'lib/analytics/analytics' -import {Text} from '../../util/text/Text' -import {s} from 'lib/styles' -import {isNetworkError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {cleanError} from 'lib/strings/errors' -import {checkAndFormatResetCode} from 'lib/strings/password' -import {logger} from '#/logger' -import {styles} from './styles' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -export const SetNewPasswordForm = ({ - error, - serviceUrl, - setError, - onPressBack, - onPasswordSet, -}: { - error: string - serviceUrl: string - setError: (v: string) => void - onPressBack: () => void - onPasswordSet: () => void -}) => { - const pal = usePalette('default') - const theme = useTheme() - const {screen} = useAnalytics() - const {_} = useLingui() - - useEffect(() => { - screen('Signin:SetNewPasswordForm') - }, [screen]) - - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [resetCode, setResetCode] = useState<string>('') - const [password, setPassword] = useState<string>('') - - const onPressNext = async () => { - // Check that the code is correct. We do this again just incase the user enters the code after their pw and we - // don't get to call onBlur first - const formattedCode = checkAndFormatResetCode(resetCode) - // TODO Better password strength check - if (!formattedCode || !password) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ - token: formattedCode, - password, - }) - onPasswordSet() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to set new password', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - 'Unable to contact your service. Please check your Internet connection.', - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - const onBlur = () => { - const formattedCode = checkAndFormatResetCode(resetCode) - if (!formattedCode) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - setResetCode(formattedCode) - } - - return ( - <> - <View> - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> - <Trans>Set new password</Trans> - </Text> - <Text type="lg" style={[pal.text, styles.instructions]}> - <Trans> - You will receive an email with a "reset code." Enter that code here, - then enter your new password. - </Trans> - </Text> - <View - testID="newPasswordView" - style={[pal.view, pal.borderDark, styles.group]}> - <View - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <FontAwesomeIcon - icon="ticket" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="resetCodeInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`Reset code`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - keyboardAppearance={theme.colorScheme} - autoComplete="off" - value={resetCode} - onChangeText={setResetCode} - onFocus={() => setError('')} - onBlur={onBlur} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Reset code`)} - accessibilityHint={_( - msg`Input code sent to your email for password reset`, - )} - /> - </View> - <View style={[pal.borderDark, styles.groupContent]}> - <FontAwesomeIcon - icon="lock" - style={[pal.textLight, styles.groupContentIcon]} - /> - <TextInput - testID="newPasswordInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`New password`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - autoCorrect={false} - autoComplete="new-password" - keyboardAppearance={theme.colorScheme} - secureTextEntry - value={password} - onChangeText={setPassword} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Password`)} - accessibilityHint={_(msg`Input new password`)} - /> - </View> - </View> - {error ? ( - <View style={styles.error}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> - </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> - </View> - </View> - ) : undefined} - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> - <Text type="xl" style={[pal.link, s.pl5]}> - <Trans>Back</Trans> - </Text> - </TouchableOpacity> - <View style={s.flex1} /> - {isProcessing ? ( - <ActivityIndicator /> - ) : !resetCode || !password ? ( - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> - <Trans>Next</Trans> - </Text> - ) : ( - <TouchableOpacity - testID="setNewPasswordButton" - // Check the code before running the callback - onPress={onPressNext} - accessibilityRole="button" - accessibilityLabel={_(msg`Go to next`)} - accessibilityHint={_(msg`Navigates to the next screen`)}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - <Trans>Next</Trans> - </Text> - </TouchableOpacity> - )} - {isProcessing ? ( - <Text type="xl" style={[pal.textLight, s.pl10]}> - <Trans>Updating...</Trans> - </Text> - ) : undefined} - </View> - </View> - </> - ) -} diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts deleted file mode 100644 index 9dccc2803..000000000 --- a/src/view/com/auth/login/styles.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {StyleSheet} from 'react-native' -import {colors} from 'lib/styles' -import {isWeb} from '#/platform/detection' - -export const styles = StyleSheet.create({ - screenTitle: { - marginBottom: 10, - marginHorizontal: 20, - }, - instructions: { - marginBottom: 20, - marginHorizontal: 20, - }, - group: { - borderWidth: 1, - borderRadius: 10, - marginBottom: 20, - marginHorizontal: 20, - }, - groupLabel: { - paddingHorizontal: 20, - paddingBottom: 5, - }, - groupContent: { - borderTopWidth: 1, - flexDirection: 'row', - alignItems: 'center', - }, - noTopBorder: { - borderTopWidth: 0, - }, - groupContentIcon: { - marginLeft: 10, - }, - account: { - borderTopWidth: 1, - paddingHorizontal: 20, - paddingVertical: 4, - }, - accountLast: { - borderBottomWidth: 1, - marginBottom: 20, - paddingVertical: 8, - }, - textInput: { - flex: 1, - width: '100%', - paddingVertical: 10, - paddingHorizontal: 12, - fontSize: 17, - letterSpacing: 0.25, - fontWeight: '400', - borderRadius: 10, - }, - textInputInnerBtn: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - textBtn: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - }, - textBtnLabel: { - flex: 1, - paddingVertical: 10, - paddingHorizontal: 12, - }, - textBtnFakeInnerBtn: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 6, - paddingVertical: 6, - paddingHorizontal: 8, - marginHorizontal: 6, - }, - accountText: { - flex: 1, - flexDirection: 'row', - alignItems: 'baseline', - paddingVertical: 10, - }, - accountTextOther: { - paddingLeft: 12, - }, - error: { - backgroundColor: colors.red4, - flexDirection: 'row', - alignItems: 'center', - marginTop: -5, - marginHorizontal: 20, - marginBottom: 15, - borderRadius: 8, - paddingHorizontal: 8, - paddingVertical: 8, - }, - errorIcon: { - borderWidth: 1, - borderColor: colors.white, - color: colors.white, - borderRadius: 30, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - marginRight: 5, - }, - dimmed: {opacity: 0.5}, - - maxHeight: { - // @ts-ignore web only -prf - maxHeight: isWeb ? '100vh' : undefined, - height: !isWeb ? '100%' : undefined, - }, -}) diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx index 07001068c..dba3f8c56 100644 --- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx +++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx @@ -1,6 +1,6 @@ import React from 'react' import {View, StyleSheet, ActivityIndicator} from 'react-native' -import {ProfileModeration, AppBskyActorDefs} from '@atproto/api' +import {ModerationDecision, AppBskyActorDefs} from '@atproto/api' import {Button} from '#/view/com/util/forms/Button' import {usePalette} from 'lib/hooks/usePalette' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -11,14 +11,15 @@ import {Text} from 'view/com/util/text/Text' import Animated, {FadeInRight} from 'react-native-reanimated' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useAnalytics} from 'lib/analytics/analytics' -import {Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {logger} from '#/logger' type Props = { profile: AppBskyActorDefs.ProfileViewBasic - moderation: ProfileModeration + moderation: ModerationDecision onFollowStateChange: (props: { did: string following: boolean @@ -56,13 +57,13 @@ export function RecommendedFollowsItem({ ) } -export function ProfileCard({ +function ProfileCard({ profile, onFollowStateChange, moderation, }: { profile: Shadow<AppBskyActorDefs.ProfileViewBasic> - moderation: ProfileModeration + moderation: ModerationDecision onFollowStateChange: (props: { did: string following: boolean @@ -70,9 +71,13 @@ export function ProfileCard({ }) { const {track} = useAnalytics() const pal = usePalette('default') + const {_} = useLingui() const [addingMoreSuggestions, setAddingMoreSuggestions] = React.useState(false) - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( + profile, + 'RecommendedFollowsItem', + ) const onToggleFollow = React.useCallback(async () => { try { @@ -110,7 +115,7 @@ export function ProfileCard({ <UserAvatar size={40} avatar={profile.avatar} - moderation={moderation.avatar} + moderation={moderation.ui('avatar')} /> </View> <View style={styles.layoutContent}> @@ -121,7 +126,7 @@ export function ProfileCard({ lineHeight={1.2}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), - moderation.profile, + moderation.ui('displayName'), )} </Text> <Text type="xl" style={[pal.textLight]} numberOfLines={1}> @@ -133,7 +138,7 @@ export function ProfileCard({ type={profile.viewer?.following ? 'default' : 'inverted'} labelStyle={styles.followButton} onPress={onToggleFollow} - label={profile.viewer?.following ? 'Unfollow' : 'Follow'} + label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)} /> </View> {profile.description ? ( diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx index 5de1a7817..b8659d56c 100644 --- a/src/view/com/auth/onboarding/WelcomeMobile.tsx +++ b/src/view/com/auth/onboarding/WelcomeMobile.tsx @@ -6,7 +6,8 @@ import {usePalette} from 'lib/hooks/usePalette' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Button} from 'view/com/util/forms/Button' import {ViewHeader} from 'view/com/util/ViewHeader' -import {Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' type Props = { next: () => void @@ -15,6 +16,7 @@ type Props = { export function WelcomeMobile({next, skip}: Props) { const pal = usePalette('default') + const {_} = useLingui() return ( <View style={[styles.container]} testID="welcomeOnboarding"> @@ -91,7 +93,7 @@ export function WelcomeMobile({next, skip}: Props) { <Button onPress={next} - label="Continue" + label={_(msg`Continue`)} testID="continueBtn" style={[styles.buttonContainer]} labelStyle={styles.buttonText} diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index a70621973..b26ac1dcb 100644 --- a/src/view/com/auth/server-input/index.tsx +++ b/src/view/com/auth/server-input/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import {View} from 'react-native' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' -import {PROD_SERVICE} from 'lib/constants' +import {BSKY_SERVICE} from 'lib/constants' import * as persisted from '#/state/persisted' import {atoms as a, useBreakpoints, useTheme} from '#/alf' @@ -26,7 +26,7 @@ export function ServerInputDialog({ const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( persisted.get('pdsAddressHistory') || [], ) - const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) + const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE]) const [customAddress, setCustomAddress] = React.useState('') const onClose = React.useCallback(() => { @@ -86,7 +86,7 @@ export function ServerInputDialog({ label="Preferences" values={fixedOption} onChange={setFixedOption}> - <ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> + <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}> {_(msg`Bluesky`)} </ToggleButton.Button> <ToggleButton.Button @@ -115,7 +115,7 @@ export function ServerInputDialog({ testID="customServerTextInput" value={customAddress} onChangeText={setCustomAddress} - label={_(msg`my-server.com`)} + label="my-server.com" accessibilityLabelledBy="address-input-label" autoCapitalize="none" keyboardType="url" |