From 3a44a1cfdcd664ebf71fdf7edc89c460acdaa558 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 15 Dec 2022 17:45:03 -0600 Subject: [PATCH] Implement 'forgot password' flow --- src/view/com/login/Signin.tsx | 378 +++++++++++++++++++++++++++++++++- 1 file changed, 367 insertions(+), 11 deletions(-) diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx index f11a4c6c..8ba66e87 100644 --- a/src/view/com/login/Signin.tsx +++ b/src/view/com/login/Signin.tsx @@ -9,24 +9,37 @@ import { View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import * as EmailValidator from 'email-validator' import {Logo} from './Logo' import {s, colors} from '../../lib/styles' import {createFullHandle, toNiceDomain} from '../../../lib/strings' -import {useStores, DEFAULT_SERVICE} from '../../../state' +import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state' import {ServiceDescription} from '../../../state/models/session' import {ServerInputModal} from '../../../state/models/shell-ui' import {isNetworkError} from '../../../lib/errors' +import {sessionClient as AtpApi} from '../../../third-party/api/index' +import type {SessionServiceClient} from '../../../third-party/api/src/index' + +enum Forms { + Login, + ForgotPassword, + SetNewPassword, + PasswordUpdated, +} export const Signin = ({onPressBack}: {onPressBack: () => void}) => { const store = useStores() - const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') const [serviceUrl, setServiceUrl] = useState(DEFAULT_SERVICE) const [serviceDescription, setServiceDescription] = useState< ServiceDescription | undefined >(undefined) - const [error, setError] = useState('') - const [handle, setHandle] = useState('') - const [password, setPassword] = useState('') + const [currentForm, setCurrentForm] = useState(Forms.Login) + + const gotoForm = (form: Forms) => () => { + setError('') + setCurrentForm(form) + } useEffect(() => { let aborted = false @@ -50,6 +63,75 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { } }, [store.session, serviceUrl]) + return ( + + + + + {currentForm === Forms.Login ? ( + + ) : undefined} + {currentForm === Forms.ForgotPassword ? ( + + ) : undefined} + {currentForm === Forms.SetNewPassword ? ( + + ) : undefined} + {currentForm === Forms.PasswordUpdated ? ( + + ) : undefined} + + ) +} + +const LoginForm = ({ + store, + error, + serviceUrl, + serviceDescription, + setError, + setServiceUrl, + onPressBack, + onPressForgotPassword, +}: { + store: RootStoreModel + error: string + serviceUrl: string + serviceDescription: ServiceDescription | undefined + setError: (v: string) => void + setServiceUrl: (v: string) => void + onPressBack: () => void + onPressForgotPassword: () => void +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const [handle, setHandle] = useState('') + const [password, setPassword] = useState('') + const onPressSelectService = () => { store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) } @@ -58,8 +140,8 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { setError('') setIsProcessing(true) - // try to guess the handle if the user just gave their own username try { + // try to guess the handle if the user just gave their own username let fullHandle = handle if ( serviceDescription && @@ -101,10 +183,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { } return ( - - - - + <> void}) => { onChangeText={setPassword} editable={!isProcessing} /> + + Forgot + {error ? ( @@ -176,11 +260,273 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => { Connecting... ) : undefined} - + + ) +} + +const ForgotPasswordForm = ({ + store, + error, + serviceUrl, + serviceDescription, + setError, + setServiceUrl, + onPressBack, + onEmailSent, +}: { + store: RootStoreModel + error: string + serviceUrl: string + serviceDescription: ServiceDescription | undefined + setError: (v: string) => void + setServiceUrl: (v: string) => void + onPressBack: () => void + onEmailSent: () => void +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const [email, setEmail] = useState('') + + const onPressSelectService = () => { + store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl)) + } + + const onPressNext = async () => { + if (!EmailValidator.validate(email)) { + return setError('Your email appears to be invalid.') + } + + setError('') + setIsProcessing(true) + + try { + const api = AtpApi.service(serviceUrl) as SessionServiceClient + await api.com.atproto.account.requestPasswordReset({email}) + onEmailSent() + } catch (e: any) { + const errMsg = e.toString() + console.log(e) + setIsProcessing(false) + if (isNetworkError(e)) { + setError( + 'Unable to contact your service. Please check your Internet connection.', + ) + } else { + setError(errMsg.replace(/^Error:/, '')) + } + } + } + + return ( + <> + Reset password + + Enter the email you used to create your account. We'll send you a "reset + code" so you can set a new password. + + + + + + {toNiceDomain(serviceUrl)} + + + + Change + + + + + + + + {error ? ( + + + + + + {error} + + + ) : undefined} + + + Back + + + {!serviceDescription || isProcessing ? ( + + ) : !email ? ( + Next + ) : ( + + Next + + )} + {!serviceDescription || isProcessing ? ( + Processing... + ) : undefined} + + + ) +} + +const SetNewPasswordForm = ({ + store, + error, + serviceUrl, + setError, + onPressBack, + onPasswordSet, +}: { + store: RootStoreModel + error: string + serviceUrl: string + setError: (v: string) => void + onPressBack: () => void + onPasswordSet: () => void +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const [resetCode, setResetCode] = useState('') + const [password, setPassword] = useState('') + + const onPressNext = async () => { + setError('') + setIsProcessing(true) + + try { + const api = AtpApi.service(serviceUrl) as SessionServiceClient + await api.com.atproto.account.resetPassword({token: resetCode, password}) + onPasswordSet() + } catch (e: any) { + const errMsg = e.toString() + console.log(e) + setIsProcessing(false) + if (isNetworkError(e)) { + setError( + 'Unable to contact your service. Please check your Internet connection.', + ) + } else { + setError(errMsg.replace(/^Error:/, '')) + } + } + } + + return ( + <> + Set new password + + You will receive an email with a "reset code." Enter that code here, + then enter your new password. + + + + + + + + + + + + {error ? ( + + + + + + {error} + + + ) : undefined} + + + Back + + + {isProcessing ? ( + + ) : !resetCode || !password ? ( + Next + ) : ( + + Next + + )} + {isProcessing ? ( + Updating... + ) : undefined} + + + ) +} + +const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => { + return ( + <> + Password updated! + + You can now sign in with your new password. + + + + + Okay + + + ) } const styles = StyleSheet.create({ + screenTitle: { + color: colors.white, + fontSize: 26, + marginBottom: 10, + marginHorizontal: 20, + }, + instructions: { + color: colors.white, + fontSize: 16, + marginBottom: 20, + marginHorizontal: 20, + }, logoHero: { paddingTop: 30, paddingBottom: 40, @@ -219,6 +565,16 @@ const styles = StyleSheet.create({ fontSize: 18, borderRadius: 10, }, + textInputInnerBtn: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 6, + paddingHorizontal: 8, + marginHorizontal: 6, + }, + textInputInnerBtnLabel: { + color: colors.white, + }, textBtnFakeInnerBtn: { flexDirection: 'row', alignItems: 'center',