diff --git a/src/lib/strings/password.ts b/src/lib/strings/password.ts
new file mode 100644
index 0000000000..e7735b90e5
--- /dev/null
+++ b/src/lib/strings/password.ts
@@ -0,0 +1,19 @@
+// Regex for base32 string for testing reset code
+const RESET_CODE_REGEX = /^[A-Z2-7]{5}-[A-Z2-7]{5}$/
+
+export function checkAndFormatResetCode(code: string): string | false {
+ // Trim the reset code
+ let fixed = code.trim().toUpperCase()
+
+ // Add a dash if needed
+ if (fixed.length === 10) {
+ fixed = `${fixed.slice(0, 5)}-${fixed.slice(5, 10)}`
+ }
+
+ // Check that it is a valid format
+ if (!RESET_CODE_REGEX.test(fixed)) {
+ return false
+ }
+
+ return fixed
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index ab710a3d03..e3a4ccd8c3 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -171,6 +171,10 @@ export interface ChangeEmailModal {
name: 'change-email'
}
+export interface ChangePasswordModal {
+ name: 'change-password'
+}
+
export interface SwitchAccountModal {
name: 'switch-account'
}
@@ -202,6 +206,7 @@ export type Modal =
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
+ | ChangePasswordModal
| SwitchAccountModal
// Curation
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
index f9bb64f98e..79399d85d7 100644
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ b/src/view/com/auth/login/ForgotPasswordForm.tsx
@@ -195,6 +195,29 @@ export const ForgotPasswordForm = ({
) : undefined}
+
+
+
+
+ Already have a code?
+
+
+
>
)
diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx
index 630c6afdec..6d1584c86c 100644
--- a/src/view/com/auth/login/SetNewPasswordForm.tsx
+++ b/src/view/com/auth/login/SetNewPasswordForm.tsx
@@ -14,6 +14,7 @@ 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'
@@ -46,14 +47,26 @@ export const SetNewPasswordForm = ({
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})
- const token = resetCode.replace(/\s/g, '')
await agent.com.atproto.server.resetPassword({
- token,
+ token: formattedCode,
password,
})
onPasswordSet()
@@ -71,6 +84,19 @@ export const SetNewPasswordForm = ({
}
}
+ 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 (
<>
@@ -100,9 +126,11 @@ export const SetNewPasswordForm = ({
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
- autoFocus
+ autoComplete="off"
value={resetCode}
onChangeText={setResetCode}
+ onFocus={() => setError('')}
+ onBlur={onBlur}
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Reset code`)}
@@ -123,6 +151,7 @@ export const SetNewPasswordForm = ({
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
+ autoComplete="new-password"
keyboardAppearance={theme.colorScheme}
secureTextEntry
value={password}
@@ -160,6 +189,7 @@ export const SetNewPasswordForm = ({
) : (
(Stages.RequestCode)
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [resetCode, setResetCode] = useState('')
+ const [newPassword, setNewPassword] = useState('')
+ const [error, setError] = useState('')
+ const {isMobile} = useWebMediaQueries()
+ const {closeModal} = useModalControls()
+ const agent = getAgent()
+
+ const onRequestCode = async () => {
+ if (
+ !currentAccount?.email ||
+ !EmailValidator.validate(currentAccount.email)
+ ) {
+ return setError(_(msg`Your email appears to be invalid.`))
+ }
+
+ setError('')
+ setIsProcessing(true)
+ try {
+ await agent.com.atproto.server.requestPasswordReset({
+ email: currentAccount.email,
+ })
+ setStage(Stages.ChangePassword)
+ } catch (e: any) {
+ const errMsg = e.toString()
+ logger.warn('Failed to request password reset', {error: e})
+ if (isNetworkError(e)) {
+ setError(
+ _(
+ msg`Unable to contact your service. Please check your Internet connection.`,
+ ),
+ )
+ } else {
+ setError(cleanError(errMsg))
+ }
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ const onChangePassword = async () => {
+ const formattedCode = checkAndFormatResetCode(resetCode)
+ // TODO Better password strength check
+ if (!formattedCode || !newPassword) {
+ setError(
+ _(
+ msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
+ ),
+ )
+ return
+ }
+
+ setError('')
+ setIsProcessing(true)
+ try {
+ await agent.com.atproto.server.resetPassword({
+ token: formattedCode,
+ password: newPassword,
+ })
+ setStage(Stages.Done)
+ } catch (e: any) {
+ const errMsg = e.toString()
+ logger.warn('Failed to set new password', {error: e})
+ if (isNetworkError(e)) {
+ setError(
+ 'Unable to contact your service. Please check your Internet connection.',
+ )
+ } else {
+ setError(cleanError(errMsg))
+ }
+ } finally {
+ setIsProcessing(false)
+ }
+ }
+
+ 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 (
+
+
+
+
+
+ {stage !== Stages.Done ? 'Change Password' : 'Password Changed'}
+
+
+
+
+ {stage === Stages.RequestCode ? (
+
+ If you want to change your password, we will send you a code to
+ verify that this is your account.
+
+ ) : stage === Stages.ChangePassword ? (
+
+ Enter the code you received to change your password.
+
+ ) : (
+ Your password has been changed successfully!
+ )}
+
+
+ {stage === Stages.RequestCode && (
+
+ setStage(Stages.ChangePassword)}
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`Go to next`)}
+ accessibilityHint={_(msg`Navigates to the next screen`)}>
+
+ Already have a code?
+
+
+
+ )}
+ {stage === Stages.ChangePassword && (
+
+
+
+ setError('')}
+ onBlur={onBlur}
+ accessible={true}
+ accessibilityLabel={_(msg`Reset Code`)}
+ accessibilityHint=""
+ autoCapitalize="none"
+ autoCorrect={false}
+ autoComplete="off"
+ />
+
+
+
+
+
+
+ )}
+ {error ? (
+
+ ) : undefined}
+
+
+ {isProcessing ? (
+
+
+
+ ) : (
+
+ {stage === Stages.RequestCode && (
+
+ )}
+ {stage === Stages.ChangePassword && (
+
+ )}
+
+ )}
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ justifyContent: 'space-between',
+ },
+ containerMobile: {
+ paddingHorizontal: 18,
+ paddingBottom: 35,
+ },
+ titleSection: {
+ paddingTop: isWeb ? 0 : 4,
+ paddingBottom: isWeb ? 14 : 10,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: '600',
+ marginBottom: 5,
+ },
+ error: {
+ borderRadius: 6,
+ },
+ textInput: {
+ width: '100%',
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ fontSize: 16,
+ },
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 32,
+ padding: 14,
+ backgroundColor: colors.blue3,
+ },
+ btnContainer: {
+ paddingTop: 20,
+ },
+ group: {
+ borderWidth: 1,
+ borderRadius: 10,
+ marginVertical: 20,
+ },
+ groupLabel: {
+ paddingHorizontal: 20,
+ paddingBottom: 5,
+ },
+ groupContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ groupBottom: {
+ borderTopWidth: 1,
+ },
+ groupContentIcon: {
+ marginLeft: 10,
+ },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 7f814d9718..4aa10d75bc 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -36,6 +36,7 @@ import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
+import * as ChangePasswordModal from './ChangePassword'
import * as SwitchAccountModal from './SwitchAccount'
import * as LinkWarningModal from './LinkWarning'
import * as EmbedConsentModal from './EmbedConsent'
@@ -172,6 +173,9 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'change-email') {
snapPoints = ChangeEmailModal.snapPoints
element =
+ } else if (activeModal?.name === 'change-password') {
+ snapPoints = ChangePasswordModal.snapPoints
+ element =
} else if (activeModal?.name === 'switch-account') {
snapPoints = SwitchAccountModal.snapPoints
element =
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index d79663746e..384a4772a8 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -34,6 +34,7 @@ import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
+import * as ChangePasswordModal from './ChangePassword'
import * as LinkWarningModal from './LinkWarning'
import * as EmbedConsentModal from './EmbedConsent'
@@ -134,6 +135,8 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'change-email') {
element =
+ } else if (modal.name === 'change-password') {
+ element =
} else if (modal.name === 'link-warning') {
element =
} else if (modal.name === 'embed-consent') {
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 3b50c54493..17e4b45c5b 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -647,7 +647,7 @@ export function SettingsScreen({}: Props) {
/>
- App passwords
+ App Passwords
- Change handle
+ Change Handle
{isNative && (
@@ -684,8 +684,29 @@ export function SettingsScreen({}: Props) {
)}
- Danger Zone
+ Account
+ openModal({name: 'change-password'})}
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`Change password`)}
+ accessibilityHint={_(msg`Change your Bluesky password`)}>
+
+
+
+
+ Change Password
+
+
- Delete my account…
+ Delete My Account…