Files
RN_Template/RN_TEMPLATE/app/screens/ChangePasswordScreen.tsx
2026-02-05 13:16:05 +08:00

217 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FC, useCallback, useState } from "react"
import { TextStyle, View, ViewStyle } from "react-native"
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
import { Button } from "@/components/Button"
import { Dialog } from "@/components/Dialog"
import { PressableIcon } from "@/components/Icon"
import { Screen } from "@/components/Screen"
import { Text } from "@/components/Text"
import { TextField } from "@/components/TextField"
import { Switch } from "@/components/Toggle/Switch"
import { useAuth } from "@/context/AuthContext"
import { translate } from "@/i18n/translate"
import { AppStackScreenProps } from "@/navigators/navigationTypes"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"
import { s } from "@/utils/responsive"
import { useHeader } from "@/utils/useHeader"
export const ChangePasswordScreen: FC<AppStackScreenProps<"ChangePassword">> =
function ChangePasswordScreen({ navigation }) {
const { changePassword, isLoading, error, clearError } = useAuth()
const {
themed,
theme: { colors },
} = useAppTheme()
const [oldPassword, setOldPassword] = useState("")
const [newPassword, setNewPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [logoutOtherDevices, setLogoutOtherDevices] = useState(false)
const [isOldPasswordHidden, setIsOldPasswordHidden] = useState(true)
const [isNewPasswordHidden, setIsNewPasswordHidden] = useState(true)
const [isConfirmPasswordHidden, setIsConfirmPasswordHidden] = useState(true)
const [localError, setLocalError] = useState("")
const [successDialogVisible, setSuccessDialogVisible] = useState(false)
const handleChangePassword = useCallback(async () => {
clearError()
setLocalError("")
// Validate
if (!oldPassword) {
setLocalError(translate("changePasswordScreen:oldPasswordRequired"))
return
}
if (!newPassword) {
setLocalError(translate("changePasswordScreen:newPasswordRequired"))
return
}
if (newPassword.length < 6) {
setLocalError(translate("changePasswordScreen:passwordTooShort"))
return
}
if (newPassword !== confirmPassword) {
setLocalError(translate("changePasswordScreen:passwordMismatch"))
return
}
if (oldPassword === newPassword) {
setLocalError(translate("changePasswordScreen:samePassword"))
return
}
const success = await changePassword(oldPassword, newPassword, logoutOtherDevices)
if (success) {
setSuccessDialogVisible(true)
}
}, [oldPassword, newPassword, confirmPassword, logoutOtherDevices, changePassword, clearError])
const displayError = localError || error
useHeader(
{
title: translate("changePasswordScreen:title"),
leftIcon: "back",
onLeftPress: () => navigation.goBack(),
},
[],
)
return (
<Screen preset="fixed" safeAreaEdges={["bottom"]} contentContainerStyle={$styles.flex1}>
<KeyboardAwareScrollView
contentContainerStyle={[$styles.container, themed($container)]}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Text tx="changePasswordScreen:description" style={themed($description)} />
<TextField
labelTx="changePasswordScreen:oldPassword"
value={oldPassword}
onChangeText={setOldPassword}
containerStyle={themed($inputContainer)}
secureTextEntry={isOldPasswordHidden}
RightAccessory={(props) => (
<PressableIcon
icon={isOldPasswordHidden ? "view" : "hidden"}
color={colors.palette.neutral800}
containerStyle={props.style}
size={s(20)}
onPress={() => setIsOldPasswordHidden(!isOldPasswordHidden)}
/>
)}
/>
<TextField
labelTx="changePasswordScreen:newPassword"
value={newPassword}
onChangeText={setNewPassword}
containerStyle={themed($inputContainer)}
secureTextEntry={isNewPasswordHidden}
RightAccessory={(props) => (
<PressableIcon
icon={isNewPasswordHidden ? "view" : "hidden"}
color={colors.palette.neutral800}
containerStyle={props.style}
size={s(20)}
onPress={() => setIsNewPasswordHidden(!isNewPasswordHidden)}
/>
)}
/>
<TextField
labelTx="changePasswordScreen:confirmPassword"
value={confirmPassword}
onChangeText={setConfirmPassword}
containerStyle={themed($inputContainer)}
secureTextEntry={isConfirmPasswordHidden}
RightAccessory={(props) => (
<PressableIcon
icon={isConfirmPasswordHidden ? "view" : "hidden"}
color={colors.palette.neutral800}
containerStyle={props.style}
size={s(20)}
onPress={() => setIsConfirmPasswordHidden(!isConfirmPasswordHidden)}
/>
)}
/>
<View style={themed($switchContainer)}>
<Text tx="changePasswordScreen:logoutOtherDevices" style={themed($switchLabel)} />
<Switch value={logoutOtherDevices} onValueChange={setLogoutOtherDevices} />
</View>
{displayError ? (
<Text size="sm" style={themed($errorText)}>
{displayError}
</Text>
) : null}
</KeyboardAwareScrollView>
{/* Fixed Bottom Button */}
<View style={themed($bottomContainer)}>
<Button
tx="changePasswordScreen:submit"
preset="reversed"
onPress={handleChangePassword}
disabled={isLoading}
loading={isLoading}
/>
</View>
{/* Success Dialog */}
<Dialog
visible={successDialogVisible}
onClose={() => {
setSuccessDialogVisible(false)
navigation.goBack()
}}
titleTx="changePasswordScreen:success"
messageTx="changePasswordScreen:successMessage"
onConfirm={() => {
setSuccessDialogVisible(false)
navigation.goBack()
}}
/>
</Screen>
)
}
const $container: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingTop: spacing.lg, // 覆盖 $styles.container 的 56px使用 useHeader 时只需 24px
})
const $description: ThemedStyle<TextStyle> = ({ colors, spacing }) => ({
color: colors.textDim,
marginBottom: spacing.lg,
})
const $inputContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginBottom: spacing.md,
})
const $switchContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.md,
paddingVertical: spacing.sm,
})
const $switchLabel: ThemedStyle<TextStyle> = () => ({
flex: 1,
})
const $errorText: ThemedStyle<TextStyle> = ({ colors, spacing }) => ({
color: colors.error,
marginBottom: spacing.md,
})
const $bottomContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingHorizontal: spacing.lg,
paddingBottom: spacing.md,
})