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,
|
|||
|
|
})
|