diff options
Diffstat (limited to 'src/view/com/auth')
-rw-r--r-- | src/view/com/auth/create/Step1.tsx | 102 | ||||
-rw-r--r-- | src/view/com/auth/login/ForgotPasswordForm.tsx | 22 | ||||
-rw-r--r-- | src/view/com/auth/login/LoginForm.tsx | 17 | ||||
-rw-r--r-- | src/view/com/auth/server-input/index.tsx | 173 |
4 files changed, 273 insertions, 41 deletions
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index a2663da86..94e03ff7a 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Keyboard, StyleSheet, + TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native' @@ -13,7 +14,6 @@ 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 {Policies} from './Policies' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {isWeb} from 'platform/detection' @@ -21,7 +21,14 @@ import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' import {logger} from '#/logger' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +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') { @@ -43,16 +50,12 @@ export function Step1({ const pal = usePalette('default') const {_} = useLingui() const {openModal} = useModalControls() + const serverInputControl = useDialogControl() const onPressSelectService = React.useCallback(() => { - openModal({ - name: 'server-input', - initialService: uiState.serviceUrl, - onSelect: (url: string) => - uiDispatch({type: 'set-service-url', value: url}), - }) + serverInputControl.open() Keyboard.dismiss() - }, [uiDispatch, uiState.serviceUrl, openModal]) + }, [serverInputControl]) const onPressWaitlist = React.useCallback(() => { openModal({name: 'waitlist'}) @@ -64,23 +67,72 @@ export function Step1({ return ( <View> - <StepHeader uiState={uiState} title={_(msg`Your account`)}> - <View> - <Button - testID="selectServiceButton" - type="default" - style={{ - aspectRatio: 1, - justifyContent: 'center', - alignItems: 'center', - }} - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)} - onPress={onPressSelectService}> - <FontAwesomeIcon icon="server" size={21} color={pal.colors.text} /> - </Button> + <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="loginSelectServiceButton" + 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> - </StepHeader> + </View> {!uiState.serviceDescription ? ( <ActivityIndicator /> diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx index 79399d85d..322da2b8f 100644 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ b/src/view/com/auth/login/ForgotPasswordForm.tsx @@ -1,6 +1,7 @@ import React, {useState, useEffect} from 'react' import { ActivityIndicator, + Keyboard, TextInput, TouchableOpacity, View, @@ -24,7 +25,9 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {styles} from './styles' -import {useModalControls} from '#/state/modals' +import {useDialogControl} from '#/components/Dialog' + +import {ServerInputDialog} from '../server-input' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -51,19 +54,16 @@ export const ForgotPasswordForm = ({ const [email, setEmail] = useState<string>('') const {screen} = useAnalytics() const {_} = useLingui() - const {openModal} = useModalControls() + const serverInputControl = useDialogControl() useEffect(() => { screen('Signin:ForgotPassword') }, [screen]) - const onPressSelectService = () => { - openModal({ - name: 'server-input', - initialService: serviceUrl, - onSelect: setServiceUrl, - }) - } + const onPressSelectService = React.useCallback(() => { + serverInputControl.open() + Keyboard.dismiss() + }, [serverInputControl]) const onPressNext = async () => { if (!EmailValidator.validate(email)) { @@ -96,6 +96,10 @@ export const ForgotPasswordForm = ({ return ( <> <View> + <ServerInputDialog + control={serverInputControl} + onSelect={setServiceUrl} + /> <Text type="title-lg" style={[pal.text, styles.screenTitle]}> <Trans>Reset password</Trans> </Text> diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx index 10608a54b..e480de7a4 100644 --- a/src/view/com/auth/login/LoginForm.tsx +++ b/src/view/com/auth/login/LoginForm.tsx @@ -25,7 +25,9 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {styles} from './styles' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' +import {useDialogControl} from '#/components/Dialog' + +import {ServerInputDialog} from '../server-input' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -58,15 +60,11 @@ export const LoginForm = ({ const [password, setPassword] = useState<string>('') const passwordInputRef = useRef<TextInput>(null) const {_} = useLingui() - const {openModal} = useModalControls() const {login} = useSessionApi() + const serverInputControl = useDialogControl() const onPressSelectService = () => { - openModal({ - name: 'server-input', - initialService: serviceUrl, - onSelect: setServiceUrl, - }) + serverInputControl.open() Keyboard.dismiss() track('Signin:PressedSelectService') } @@ -130,6 +128,11 @@ export const LoginForm = ({ 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> diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx new file mode 100644 index 000000000..a70621973 --- /dev/null +++ b/src/view/com/auth/server-input/index.tsx @@ -0,0 +1,173 @@ +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 * as persisted from '#/state/persisted' + +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {Text, P} from '#/components/Typography' +import {Button, ButtonText} from '#/components/Button' +import * as ToggleButton from '#/components/forms/ToggleButton' +import * as TextField from '#/components/forms/TextField' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' + +export function ServerInputDialog({ + control, + onSelect, +}: { + control: Dialog.DialogOuterProps['control'] + onSelect: (url: string) => void +}) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( + persisted.get('pdsAddressHistory') || [], + ) + const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) + const [customAddress, setCustomAddress] = React.useState('') + + const onClose = React.useCallback(() => { + let url + if (fixedOption[0] === 'custom') { + url = customAddress.trim().toLowerCase() + if (!url) { + return + } + } else { + url = fixedOption[0] + } + if (!url.startsWith('http://') && !url.startsWith('https://')) { + if (url === 'localhost' || url.startsWith('localhost:')) { + url = `http://${url}` + } else { + url = `https://${url}` + } + } + + if (fixedOption[0] === 'custom') { + if (!pdsAddressHistory.includes(url)) { + const newHistory = [url, ...pdsAddressHistory.slice(0, 4)] + setPdsAddressHistory(newHistory) + persisted.write('pdsAddressHistory', newHistory) + } + } + + onSelect(url) + }, [ + fixedOption, + customAddress, + onSelect, + pdsAddressHistory, + setPdsAddressHistory, + ]) + + return ( + <Dialog.Outer + control={control} + nativeOptions={{sheet: {snapPoints: ['100%']}}} + onClose={onClose}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title"> + <View style={[a.relative, a.gap_md, a.w_full]}> + <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> + <Trans>Choose Service</Trans> + </Text> + <P nativeID="dialog-description" style={[a.text_sm]}> + <Trans>Select the service that hosts your data.</Trans> + </P> + + <ToggleButton.Group + label="Preferences" + values={fixedOption} + onChange={setFixedOption}> + <ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> + {_(msg`Bluesky`)} + </ToggleButton.Button> + <ToggleButton.Button + testID="customSelectBtn" + name="custom" + label={_(msg`Custom`)}> + {_(msg`Custom`)} + </ToggleButton.Button> + </ToggleButton.Group> + + {fixedOption[0] === 'custom' && ( + <View + style={[ + a.border, + t.atoms.border_contrast_low, + a.rounded_sm, + a.px_md, + a.py_md, + ]}> + <TextField.Label nativeID="address-input-label"> + <Trans>Server address</Trans> + </TextField.Label> + <TextField.Root> + <TextField.Icon icon={Globe} /> + <Dialog.Input + testID="customServerTextInput" + value={customAddress} + onChangeText={setCustomAddress} + label={_(msg`my-server.com`)} + accessibilityLabelledBy="address-input-label" + autoCapitalize="none" + keyboardType="url" + /> + </TextField.Root> + {pdsAddressHistory.length > 0 && ( + <View style={[a.flex_row, a.flex_wrap, a.mt_xs]}> + {pdsAddressHistory.map(uri => ( + <Button + key={uri} + variant="ghost" + color="primary" + label={uri} + style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} + onPress={() => setCustomAddress(uri)}> + <ButtonText>{uri}</ButtonText> + </Button> + ))} + </View> + )} + </View> + )} + + <View style={[a.py_xs]}> + <P + style={[ + t.atoms.text_contrast_medium, + a.text_sm, + a.leading_snug, + a.flex_1, + ]}> + <Trans> + Bluesky is an open network where you can choose your hosting + provider. Custom hosting is now available in beta for + developers. + </Trans> + </P> + </View> + + <View style={gtMobile && [a.flex_row, a.justify_end]}> + <Button + testID="doneBtn" + variant="outline" + color="primary" + size="small" + onPress={() => control.close()} + label={_(msg`Done`)}> + {_(msg`Done`)} + </Button> + </View> + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} |