217 lines
7.4 KiB
TypeScript
217 lines
7.4 KiB
TypeScript
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,
|
||
})
|