template_0205
This commit is contained in:
216
RN_TEMPLATE/app/screens/ChangePasswordScreen.tsx
Normal file
216
RN_TEMPLATE/app/screens/ChangePasswordScreen.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
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,
|
||||
})
|
||||
Reference in New Issue
Block a user