diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..184addc8e8
--- /dev/null
+++ b/assets/icons/ticket_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index fef68ecaba..33803b6fb0 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -300,6 +300,9 @@ export const atoms = {
/*
* Padding
*/
+ p_0: {
+ padding: 0,
+ },
p_2xs: {
padding: tokens.space._2xs,
},
@@ -330,6 +333,10 @@ export const atoms = {
p_5xl: {
padding: tokens.space._5xl,
},
+ px_0: {
+ paddingLeft: 0,
+ paddingRight: 0,
+ },
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
@@ -370,6 +377,10 @@ export const atoms = {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
+ py_0: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
@@ -410,6 +421,9 @@ export const atoms = {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
+ pt_0: {
+ paddingTop: 0,
+ },
pt_2xs: {
paddingTop: tokens.space._2xs,
},
@@ -440,6 +454,9 @@ export const atoms = {
pt_5xl: {
paddingTop: tokens.space._5xl,
},
+ pb_0: {
+ paddingBottom: 0,
+ },
pb_2xs: {
paddingBottom: tokens.space._2xs,
},
@@ -470,6 +487,9 @@ export const atoms = {
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
+ pl_0: {
+ paddingLeft: 0,
+ },
pl_2xs: {
paddingLeft: tokens.space._2xs,
},
@@ -500,6 +520,9 @@ export const atoms = {
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
+ pr_0: {
+ paddingRight: 0,
+ },
pr_2xs: {
paddingRight: tokens.space._2xs,
},
@@ -534,6 +557,9 @@ export const atoms = {
/*
* Margin
*/
+ m_0: {
+ margin: 0,
+ },
m_2xs: {
margin: tokens.space._2xs,
},
@@ -564,6 +590,13 @@ export const atoms = {
m_5xl: {
margin: tokens.space._5xl,
},
+ m_auto: {
+ margin: 'auto',
+ },
+ mx_0: {
+ marginLeft: 0,
+ marginRight: 0,
+ },
mx_2xs: {
marginLeft: tokens.space._2xs,
marginRight: tokens.space._2xs,
@@ -604,6 +637,14 @@ export const atoms = {
marginLeft: tokens.space._5xl,
marginRight: tokens.space._5xl,
},
+ mx_auto: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+ my_0: {
+ marginTop: 0,
+ marginBottom: 0,
+ },
my_2xs: {
marginTop: tokens.space._2xs,
marginBottom: tokens.space._2xs,
@@ -644,6 +685,13 @@ export const atoms = {
marginTop: tokens.space._5xl,
marginBottom: tokens.space._5xl,
},
+ my_auto: {
+ marginTop: 'auto',
+ marginBottom: 'auto',
+ },
+ mt_0: {
+ marginTop: 0,
+ },
mt_2xs: {
marginTop: tokens.space._2xs,
},
@@ -674,6 +722,12 @@ export const atoms = {
mt_5xl: {
marginTop: tokens.space._5xl,
},
+ mt_auto: {
+ marginTop: 'auto',
+ },
+ mb_0: {
+ marginBottom: 0,
+ },
mb_2xs: {
marginBottom: tokens.space._2xs,
},
@@ -704,6 +758,12 @@ export const atoms = {
mb_5xl: {
marginBottom: tokens.space._5xl,
},
+ mb_auto: {
+ marginBottom: 'auto',
+ },
+ ml_0: {
+ marginLeft: 0,
+ },
ml_2xs: {
marginLeft: tokens.space._2xs,
},
@@ -734,6 +794,12 @@ export const atoms = {
ml_5xl: {
marginLeft: tokens.space._5xl,
},
+ ml_auto: {
+ marginLeft: 'auto',
+ },
+ mr_0: {
+ marginRight: 0,
+ },
mr_2xs: {
marginRight: tokens.space._2xs,
},
@@ -764,4 +830,7 @@ export const atoms = {
mr_5xl: {
marginRight: tokens.space._5xl,
},
+ mr_auto: {
+ marginRight: 'auto',
+ },
} as const
diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx
new file mode 100644
index 0000000000..df506b77c1
--- /dev/null
+++ b/src/components/forms/HostingProvider.tsx
@@ -0,0 +1,69 @@
+import React from 'react'
+import {TouchableOpacity, View} from 'react-native'
+
+import {isAndroid} from '#/platform/detection'
+import {atoms as a, useTheme} from '#/alf'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
+import * as TextField from './TextField'
+import {useDialogControl} from '../Dialog'
+import {Text} from '../Typography'
+import {ServerInputDialog} from '#/view/com/auth/server-input'
+import {toNiceDomain} from '#/lib/strings/url-helpers'
+
+export function HostingProvider({
+ serviceUrl,
+ onSelectServiceUrl,
+ onOpenDialog,
+}: {
+ serviceUrl: string
+ onSelectServiceUrl: (provider: string) => void
+ onOpenDialog?: () => void
+}) {
+ const serverInputControl = useDialogControl()
+ const t = useTheme()
+
+ const onPressSelectService = React.useCallback(() => {
+ serverInputControl.open()
+ if (onOpenDialog) {
+ onOpenDialog()
+ }
+ }, [onOpenDialog, serverInputControl])
+
+ return (
+ <>
+
+
+
+ {toNiceDomain(serviceUrl)}
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx
new file mode 100644
index 0000000000..0df6b81204
--- /dev/null
+++ b/src/components/icons/Ticket.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12c0-1.296.704-2.426 1.75-3.032a.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
+})
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index f5b3c2a861..7a3a4555b2 100644
--- a/src/screens/Login/ChooseAccountForm.tsx
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {ScrollView, TouchableOpacity, View} from 'react-native'
+import {TouchableOpacity, View} from 'react-native'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import flattenReactChildren from 'react-keyed-flatten-children'
@@ -7,16 +7,17 @@ import flattenReactChildren from 'react-keyed-flatten-children'
import {useAnalytics} from 'lib/analytics/analytics'
import {UserAvatar} from '../../view/com/util/UserAvatar'
import {colors} from 'lib/styles'
-import {styles} from '../../view/com/auth/login/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'
import {Button} from '#/components/Button'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import * as TextField from '#/components/forms/TextField'
+import {FormContainer} from './FormContainer'
function Group({children}: {children: React.ReactNode}) {
const t = useTheme()
@@ -106,7 +107,6 @@ export const ChooseAccountForm = ({
const {accounts, currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
- const {gtMobile} = useBreakpoints()
React.useEffect(() => {
screen('Choose Account')
@@ -133,12 +133,13 @@ export const ChooseAccountForm = ({
)
return (
-
-
-
+ Select account}>
+
+
Sign in as...
-
+
{accounts.map(account => (
-
-
-
-
-
+
+
+
+
+
)
}
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
new file mode 100644
index 0000000000..fa674155ab
--- /dev/null
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -0,0 +1,183 @@
+import React, {useState, useEffect} from 'react'
+import {ActivityIndicator, Keyboard, View} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
+import * as EmailValidator from 'email-validator'
+import {BskyAgent} from '@atproto/api'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import * as TextField from '#/components/forms/TextField'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
+import {atoms as a, useTheme} from '#/alf'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {isNetworkError} from 'lib/strings/errors'
+import {cleanError} from 'lib/strings/errors'
+import {logger} from '#/logger'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {FormContainer} from './FormContainer'
+import {FormError} from './FormError'
+
+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 t = useTheme()
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [email, setEmail] = useState('')
+ const {screen} = useAnalytics()
+ const {_} = useLingui()
+
+ useEffect(() => {
+ screen('Signin:ForgotPassword')
+ }, [screen])
+
+ const onPressSelectService = React.useCallback(() => {
+ Keyboard.dismiss()
+ }, [])
+
+ 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 (
+ Reset password}>
+
+
+ Hosting provider
+
+
+
+
+
+ Email address
+
+
+
+
+
+
+
+
+
+ Enter the email you used to create your account. We'll send you a
+ "reset code" so you can set a new password.
+
+
+
+
+
+
+
+ {!serviceDescription || isProcessing ? (
+
+ ) : (
+
+ )}
+ {!serviceDescription || isProcessing ? (
+
+ Processing...
+
+ ) : undefined}
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Login/FormContainer.tsx b/src/screens/Login/FormContainer.tsx
new file mode 100644
index 0000000000..a08aa05b0f
--- /dev/null
+++ b/src/screens/Login/FormContainer.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import {
+ ScrollView,
+ StyleSheet,
+ View,
+ type StyleProp,
+ type ViewStyle,
+} from 'react-native'
+
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {isWeb} from '#/platform/detection'
+
+export function FormContainer({
+ testID,
+ title,
+ children,
+ style,
+ contentContainerStyle,
+}: {
+ testID?: string
+ title?: React.ReactNode
+ children: React.ReactNode
+ style?: StyleProp
+ contentContainerStyle?: StyleProp
+}) {
+ const {gtMobile} = useBreakpoints()
+ const t = useTheme()
+ return (
+
+
+ {title && !gtMobile && (
+
+ {title}
+
+ )}
+ {children}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ maxHeight: {
+ // @ts-ignore web only -prf
+ maxHeight: isWeb ? '100vh' : undefined,
+ height: !isWeb ? '100%' : undefined,
+ },
+})
diff --git a/src/screens/Login/FormError.tsx b/src/screens/Login/FormError.tsx
new file mode 100644
index 0000000000..3c6a8649d9
--- /dev/null
+++ b/src/screens/Login/FormError.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+
+import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import {Text} from '#/components/Typography'
+import {atoms as a, useTheme} from '#/alf'
+import {colors} from '#/lib/styles'
+
+export function FormError({error}: {error?: string}) {
+ const t = useTheme()
+
+ if (!error) return null
+
+ return (
+
+
+
+ {error}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ error: {
+ backgroundColor: colors.red4,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 15,
+ borderRadius: 8,
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ },
+})
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 3089b38871..5801552811 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -2,36 +2,29 @@ import React, {useState, useRef} from 'react'
import {
ActivityIndicator,
Keyboard,
- ScrollView,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
import {useAnalytics} from 'lib/analytics/analytics'
-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 {useSessionApi} from '#/state/session'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
-import {styles} from '../../view/com/auth/login/styles'
-import {useLingui} from '@lingui/react'
-import {useDialogControl} from '#/components/Dialog'
-import {ServerInputDialog} from '../../view/com/auth/server-input'
import {Button, ButtonText} from '#/components/Button'
-import {isAndroid} from '#/platform/detection'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
-import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
-import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
-import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
+import {HostingProvider} from '#/components/forms/HostingProvider'
+import {FormContainer} from './FormContainer'
+import {FormError} from './FormError'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -64,14 +57,11 @@ export const LoginForm = ({
const passwordInputRef = useRef(null)
const {_} = useLingui()
const {login} = useSessionApi()
- const serverInputControl = useDialogControl()
- const {gtMobile} = useBreakpoints()
- const onPressSelectService = () => {
- serverInputControl.open()
+ const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
track('Signin:PressedSelectService')
- }
+ }, [track])
const onPressNext = async () => {
Keyboard.dismiss()
@@ -131,171 +121,138 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password
return (
-
-
- Sign in}>
+
+
+ Hosting provider
+
+
-
-
-
- Hosting provider
-
+
+
+
+ Account
+
+
+
+ {
+ passwordInputRef.current?.focus()
+ }}
+ blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
+ value={identifier}
+ onChangeText={str =>
+ setIdentifier((str || '').toLowerCase().trim())
+ }
+ editable={!isProcessing}
+ accessibilityHint={_(
+ msg`Input the username or email address you used at signup`,
+ )}
+ />
+
+
+
+
+
+
-
- {toNiceDomain(serviceUrl)}
-
-
-
+ t.atoms.bg_contrast_100,
+ {marginLeft: 'auto', left: 6, padding: 6},
+ a.z_10,
+ ]}>
+
+ Forgot?
+
-
-
-
- Account
-
-
-
- {
- passwordInputRef.current?.focus()
- }}
- blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
- value={identifier}
- onChangeText={str =>
- setIdentifier((str || '').toLowerCase().trim())
- }
- editable={!isProcessing}
- accessibilityHint={_(
- msg`Input the username or email address you used at signup`,
- )}
- />
-
-
-
-
-
-
-
-
- Forgot?
-
-
-
-
- {error ? (
-
-
-
- {error}
-
-
- ) : undefined}
-
+
+
+
+
+
+
+ {!serviceDescription && error ? (
-
- {!serviceDescription && error ? (
-
- ) : !serviceDescription ? (
- <>
-
-
- Connecting...
-
- >
- ) : isProcessing ? (
+ ) : !serviceDescription ? (
+ <>
- ) : isReady ? (
-
- ) : undefined}
-
+
+ Connecting...
+
+ >
+ ) : isProcessing ? (
+
+ ) : isReady ? (
+
+ ) : undefined}
-
+
)
}
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
new file mode 100644
index 0000000000..218cab5392
--- /dev/null
+++ b/src/screens/Login/PasswordUpdatedForm.tsx
@@ -0,0 +1,49 @@
+import React, {useEffect} from 'react'
+import {View} from 'react-native'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FormContainer} from './FormContainer'
+import {Button, ButtonText} from '#/components/Button'
+import {Text} from '#/components/Typography'
+import {atoms as a, useBreakpoints} from '#/alf'
+
+export const PasswordUpdatedForm = ({
+ onPressNext,
+}: {
+ onPressNext: () => void
+}) => {
+ const {screen} = useAnalytics()
+ const {_} = useLingui()
+ const {gtMobile} = useBreakpoints()
+
+ useEffect(() => {
+ screen('Signin:PasswordUpdatedForm')
+ }, [screen])
+
+ return (
+
+
+ Password updated!
+
+
+ You can now sign in with your new password.
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
new file mode 100644
index 0000000000..2685ad5ee8
--- /dev/null
+++ b/src/screens/Login/SetNewPasswordForm.tsx
@@ -0,0 +1,189 @@
+import React, {useState, useEffect} from 'react'
+import {ActivityIndicator, View} from 'react-native'
+import {BskyAgent} from '@atproto/api'
+import {useAnalytics} from 'lib/analytics/analytics'
+
+import {isNetworkError} from 'lib/strings/errors'
+import {cleanError} from 'lib/strings/errors'
+import {checkAndFormatResetCode} from 'lib/strings/password'
+import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {FormContainer} from './FormContainer'
+import {Text} from '#/components/Typography'
+import * as TextField from '#/components/forms/TextField'
+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
+import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
+import {Button, ButtonText} from '#/components/Button'
+import {useTheme, atoms as a} from '#/alf'
+import {FormError} from './FormError'
+
+export const SetNewPasswordForm = ({
+ error,
+ serviceUrl,
+ setError,
+ onPressBack,
+ onPasswordSet,
+}: {
+ error: string
+ serviceUrl: string
+ setError: (v: string) => void
+ onPressBack: () => void
+ onPasswordSet: () => void
+}) => {
+ const {screen} = useAnalytics()
+ const {_} = useLingui()
+ const t = useTheme()
+
+ useEffect(() => {
+ screen('Signin:SetNewPasswordForm')
+ }, [screen])
+
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [resetCode, setResetCode] = useState('')
+ const [password, setPassword] = useState('')
+
+ const onPressNext = async () => {
+ onPasswordSet()
+ if (Math.random() > 0) return
+ // 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 (
+ Set new password}>
+
+
+ You will receive an email with a "reset code." Enter that code here,
+ then enter your new password.
+
+
+
+
+ Reset code
+
+
+ setError('')}
+ onBlur={onBlur}
+ editable={!isProcessing}
+ accessibilityHint={_(
+ msg`Input code sent to your email for password reset`,
+ )}
+ />
+
+
+
+
+ New password
+
+
+
+
+
+
+
+
+
+ {isProcessing ? (
+
+ ) : (
+
+ )}
+ {isProcessing ? (
+
+ Updating...
+
+ ) : undefined}
+
+
+ )
+}
diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx
index 028a497d28..10edb3eb60 100644
--- a/src/screens/Login/index.tsx
+++ b/src/screens/Login/index.tsx
@@ -13,9 +13,9 @@ import {msg} from '@lingui/macro'
import {logger} from '#/logger'
import {atoms as a} from '#/alf'
import {ChooseAccountForm} from './ChooseAccountForm'
-import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm'
-import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm'
-import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm'
+import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
+import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
+import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
import {LoginForm} from '#/screens/Login/LoginForm'
enum Forms {
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
deleted file mode 100644
index 322da2b8fd..0000000000
--- 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(false)
- const [email, setEmail] = useState('')
- 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 (
- <>
-
-
-
- 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)}
-
-
-
-
-
-
-
-
-
-
- {error ? (
-
-
-
-
-
- {error}
-
-
- ) : undefined}
-
-
-
- Back
-
-
-
- {!serviceDescription || isProcessing ? (
-
- ) : !email ? (
-
- Next
-
- ) : (
-
-
- Next
-
-
- )}
- {!serviceDescription || isProcessing ? (
-
- Processing...
-
- ) : undefined}
-
-
-
-
-
- Already have a code?
-
-
-
-
- >
- )
-}
diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx
deleted file mode 100644
index 71f750b141..0000000000
--- 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 (
- <>
-
-
- Password updated!
-
-
- You can now sign in with your new password.
-
-
-
-
-
- Okay
-
-
-
-
- >
- )
-}
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
deleted file mode 100644
index 6d1584c86c..0000000000
--- 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(false)
- const [resetCode, setResetCode] = useState('')
- const [password, setPassword] = useState('')
-
- 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 (
- <>
-
-
- Set new password
-
-
-
- You will receive an email with a "reset code." Enter that code here,
- then enter your new password.
-
-
-
-
-
- setError('')}
- onBlur={onBlur}
- editable={!isProcessing}
- accessible={true}
- accessibilityLabel={_(msg`Reset code`)}
- accessibilityHint={_(
- msg`Input code sent to your email for password reset`,
- )}
- />
-
-
-
-
-
-
- {error ? (
-
-
-
-
-
- {error}
-
-
- ) : undefined}
-
-
-
- Back
-
-
-
- {isProcessing ? (
-
- ) : !resetCode || !password ? (
-
- Next
-
- ) : (
-
-
- Next
-
-
- )}
- {isProcessing ? (
-
- Updating...
-
- ) : undefined}
-
-
- >
- )
-}
diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts
deleted file mode 100644
index 9dccc2803b..0000000000
--- 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,
- },
-})