emailVerified区分邮箱和手机号登录
This commit is contained in:
parent
747cc61393
commit
0aa2448c92
|
|
@ -20,6 +20,8 @@ interface ConfirmModalProps {
|
|||
onConfirm?: () => void
|
||||
/** 取消回调,如果不传则默认关闭Modal */
|
||||
onCancel?: () => void
|
||||
/** 确认按钮是否处于加载状态 */
|
||||
confirmLoading?: boolean
|
||||
}
|
||||
|
||||
const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
|
|
@ -30,8 +32,10 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|||
cancelText = '取消',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmLoading = false,
|
||||
}) => {
|
||||
const handleCancel = () => {
|
||||
if (confirmLoading) return
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
} else {
|
||||
|
|
@ -40,6 +44,7 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (confirmLoading) return
|
||||
onConfirm && onConfirm()
|
||||
}
|
||||
|
||||
|
|
@ -69,10 +74,20 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
|||
<Block
|
||||
className="flex flex-1 flex-row items-center justify-center border-[3px] border-black bg-accent py-[12px] shadow-[4px_4px_0px_#000]"
|
||||
onClick={handleConfirm}
|
||||
style={{ opacity: confirmLoading ? 0.6 : 1 }}
|
||||
>
|
||||
<Text className="text-black">{confirmText}</Text>
|
||||
{title==='确认支付?' && (
|
||||
<Ionicons color="#000" name="flash" size={16} style={{ marginLeft: 4 }} />
|
||||
{confirmLoading ? (
|
||||
<>
|
||||
<Ionicons color="#000" name="sync" size={16} style={{ marginRight: 4 }} />
|
||||
<Text className="text-black">{confirmText}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text className="text-black">{confirmText}</Text>
|
||||
{title==='确认支付?' && (
|
||||
<Ionicons color="#000" name="flash" size={16} style={{ marginLeft: 4 }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Block>
|
||||
</Block>
|
||||
|
|
|
|||
329
app/profile.tsx
329
app/profile.tsx
|
|
@ -55,6 +55,267 @@ function EditNicknameModal({ initialName, onConfirm, onCancel }: EditNicknameMod
|
|||
)
|
||||
}
|
||||
|
||||
type ChangePasswordModalProps = {
|
||||
onConfirm: (currentPassword: string, newPassword: string) => void | Promise<void>
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
// 验证密码强度
|
||||
const validatePassword = (password: string): string | null => {
|
||||
if (!password) {
|
||||
return '请输入新密码'
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return '密码长度至少6位'
|
||||
}
|
||||
// 检查是否是无效密码:全是相同字符
|
||||
if (/^(.)\1+$/.test(password)) {
|
||||
return '密码不能全是相同字符'
|
||||
}
|
||||
// 检查是否是连续数字(如 123456, 654321)
|
||||
const isSequentialNumbers = (str: string): boolean => {
|
||||
for (let i = 0; i < str.length - 1; i++) {
|
||||
const current = parseInt(str[i], 10)
|
||||
const next = parseInt(str[i + 1], 10)
|
||||
if (isNaN(current) || isNaN(next)) return false
|
||||
if (Math.abs(current - next) !== 1) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 检查是否是连续字母(如 abcdef, fedcba)
|
||||
const isSequentialLetters = (str: string): boolean => {
|
||||
const lowerStr = str.toLowerCase()
|
||||
for (let i = 0; i < lowerStr.length - 1; i++) {
|
||||
const current = lowerStr.charCodeAt(i)
|
||||
const next = lowerStr.charCodeAt(i + 1)
|
||||
if (Math.abs(current - next) !== 1) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (isSequentialNumbers(password) || isSequentialLetters(password)) {
|
||||
return '密码不能是连续字符'
|
||||
}
|
||||
// 检查是否是简单密码(如 111111, 000000, aaaaaa)
|
||||
const commonWeakPasswords = ['111111', '222222', '333333', '444444', '555555', '666666', '777777', '888888', '999999', '000000', 'aaaaaa', 'bbbbbb', 'cccccc', 'dddddd', 'eeeeee', 'ffffff', '123456', '654321', 'abcdef', 'fedcba']
|
||||
if (commonWeakPasswords.includes(password.toLowerCase())) {
|
||||
return '密码过于简单,请使用更复杂的密码'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function ChangePasswordModal({ onConfirm, onCancel }: ChangePasswordModalProps) {
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
|
||||
const [showNewPassword, setShowNewPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [errors, setErrors] = useState<{
|
||||
currentPassword?: string
|
||||
newPassword?: string
|
||||
confirmPassword?: string
|
||||
}>({})
|
||||
|
||||
const validateCurrentPassword = (value: string) => {
|
||||
if (!value) {
|
||||
setErrors((prev) => ({ ...prev, currentPassword: '请输入当前密码' }))
|
||||
} else {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors.currentPassword
|
||||
return newErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validateNewPassword = (value: string) => {
|
||||
const error = validatePassword(value)
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, newPassword: error }))
|
||||
} else {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors.newPassword
|
||||
return newErrors
|
||||
})
|
||||
// 如果确认密码已输入,重新验证确认密码
|
||||
if (confirmPassword) {
|
||||
validateConfirmPassword(confirmPassword, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validateConfirmPassword = (value: string, newPwd = newPassword) => {
|
||||
if (!value) {
|
||||
setErrors((prev) => ({ ...prev, confirmPassword: '请再次输入新密码' }))
|
||||
} else if (value !== newPwd) {
|
||||
setErrors((prev) => ({ ...prev, confirmPassword: '两次密码输入不一致' }))
|
||||
} else {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors.confirmPassword
|
||||
return newErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (loading) return
|
||||
|
||||
// 验证所有字段并收集错误
|
||||
const newErrors: typeof errors = {}
|
||||
|
||||
if (!currentPassword) {
|
||||
newErrors.currentPassword = '请输入当前密码'
|
||||
}
|
||||
|
||||
const newPasswordError = validatePassword(newPassword)
|
||||
if (newPasswordError) {
|
||||
newErrors.newPassword = newPasswordError
|
||||
}
|
||||
|
||||
if (!confirmPassword) {
|
||||
newErrors.confirmPassword = '请再次输入新密码'
|
||||
} else if (confirmPassword !== newPassword) {
|
||||
newErrors.confirmPassword = '两次密码输入不一致'
|
||||
}
|
||||
|
||||
// 设置错误状态
|
||||
setErrors(newErrors)
|
||||
|
||||
// 如果有错误,不继续提交
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await onConfirm(currentPassword, newPassword)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
badge="password"
|
||||
cancelText="取消"
|
||||
confirmText={loading ? '修改中...' : '确定'}
|
||||
confirmLoading={loading}
|
||||
content={
|
||||
<Block className="w-full gap-[16px]">
|
||||
<Block className="w-full">
|
||||
<Text className="mb-[8px] text-[14px] text-black">当前密码</Text>
|
||||
<Block className="relative w-full">
|
||||
<Input
|
||||
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
|
||||
errors.currentPassword ? 'border-red-500' : 'border-black'
|
||||
}`}
|
||||
placeholder="请输入当前密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={currentPassword}
|
||||
onChangeText={(text) => {
|
||||
setCurrentPassword(text)
|
||||
if (errors.currentPassword) {
|
||||
validateCurrentPassword(text)
|
||||
}
|
||||
}}
|
||||
onBlur={() => validateCurrentPassword(currentPassword)}
|
||||
secureTextEntry={!showCurrentPassword}
|
||||
/>
|
||||
<Block
|
||||
className="absolute right-[12px] top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
<Ionicons
|
||||
color="#9CA3AF"
|
||||
name={showCurrentPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
{errors.currentPassword && (
|
||||
<Text className="mt-[4px] text-[12px] text-red-500">{errors.currentPassword}</Text>
|
||||
)}
|
||||
</Block>
|
||||
<Block className="w-full">
|
||||
<Text className="mb-[8px] text-[14px] text-black">新密码</Text>
|
||||
<Block className="relative w-full">
|
||||
<Input
|
||||
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
|
||||
errors.newPassword ? 'border-red-500' : 'border-black'
|
||||
}`}
|
||||
placeholder="请输入新密码(至少6位)"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={newPassword}
|
||||
onChangeText={(text) => {
|
||||
setNewPassword(text)
|
||||
if (errors.newPassword) {
|
||||
validateNewPassword(text)
|
||||
}
|
||||
}}
|
||||
onBlur={() => validateNewPassword(newPassword)}
|
||||
secureTextEntry={!showNewPassword}
|
||||
/>
|
||||
<Block
|
||||
className="absolute right-[12px] top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
>
|
||||
<Ionicons
|
||||
color="#9CA3AF"
|
||||
name={showNewPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
{errors.newPassword && (
|
||||
<Text className="mt-[4px] text-[12px] text-red-500">{errors.newPassword}</Text>
|
||||
)}
|
||||
</Block>
|
||||
<Block className="w-full">
|
||||
<Text className="mb-[8px] text-[14px] text-black">确认新密码</Text>
|
||||
<Block className="relative w-full">
|
||||
<Input
|
||||
className={`w-full rounded-lg border-2 px-[12px] py-[10px] text-[14px] text-black ${
|
||||
errors.confirmPassword ? 'border-red-500' : 'border-black'
|
||||
}`}
|
||||
placeholder="请再次输入新密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={confirmPassword}
|
||||
onChangeText={(text) => {
|
||||
setConfirmPassword(text)
|
||||
if (errors.confirmPassword) {
|
||||
validateConfirmPassword(text)
|
||||
}
|
||||
}}
|
||||
onBlur={() => validateConfirmPassword(confirmPassword)}
|
||||
secureTextEntry={!showConfirmPassword}
|
||||
/>
|
||||
<Block
|
||||
className="absolute right-[12px] top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
<Ionicons
|
||||
color="#9CA3AF"
|
||||
name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
{errors.confirmPassword && (
|
||||
<Text className="mt-[4px] text-[12px] text-red-500">{errors.confirmPassword}</Text>
|
||||
)}
|
||||
</Block>
|
||||
</Block>
|
||||
}
|
||||
title="修改密码"
|
||||
onCancel={onCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(function ProfilePage() {
|
||||
const { user } = userStore
|
||||
|
||||
|
|
@ -126,6 +387,32 @@ export default observer(function ProfilePage() {
|
|||
)
|
||||
}
|
||||
|
||||
const handleChangePassword = () => {
|
||||
Toast.showModal(
|
||||
<ChangePasswordModal
|
||||
onConfirm={async (currentPassword, newPassword) => {
|
||||
try {
|
||||
// better-auth 使用 changePassword 方法修改密码
|
||||
const res = await (authClient as any).changePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
})
|
||||
const err = (res as { error?: { message?: string } }).error
|
||||
if (err) {
|
||||
Toast.show({ title: err.message || '修改失败,请重试' })
|
||||
return
|
||||
}
|
||||
Toast.hideModal()
|
||||
Toast.show({ title: '密码已修改' })
|
||||
} catch (e) {
|
||||
Toast.show({ title: '修改失败,请重试' })
|
||||
}
|
||||
}}
|
||||
onCancel={() => Toast.hideModal()}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
const infoItems: InfoItem[] = [
|
||||
{
|
||||
id: 'nickname',
|
||||
|
|
@ -133,14 +420,37 @@ export default observer(function ProfilePage() {
|
|||
value: user?.name || '未设置',
|
||||
onPress: handleEditNickname,
|
||||
},
|
||||
{
|
||||
id: 'phone',
|
||||
label: '手机号',
|
||||
value: user?.phoneNumber || '',
|
||||
onPress: () => {
|
||||
Toast.show({ title: '手机号不可修改' })
|
||||
},
|
||||
}
|
||||
...(user?.emailVerified
|
||||
? [
|
||||
{
|
||||
id: 'email',
|
||||
label: '邮箱',
|
||||
value: user.email,
|
||||
onPress: () => {
|
||||
Toast.show({ title: '邮箱不可修改' })
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
label: '密码',
|
||||
value: '修改密码',
|
||||
valueGray: true,
|
||||
onPress: handleChangePassword,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.phoneNumber
|
||||
? [
|
||||
{
|
||||
id: 'phone',
|
||||
label: '手机号',
|
||||
value: user.phoneNumber,
|
||||
onPress: () => {
|
||||
Toast.show({ title: '手机号不可修改' })
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const renderHeader = () => (
|
||||
|
|
@ -199,8 +509,7 @@ export default observer(function ProfilePage() {
|
|||
>
|
||||
{item.value}
|
||||
</Text>
|
||||
{/*手机号 显示隐藏按钮 */}
|
||||
{item.id !== 'phone' && (
|
||||
{item.id !== 'phone' && item.id !== 'email' && (
|
||||
<Ionicons color="#9D9D9D" name="chevron-forward" size={18} />
|
||||
)}
|
||||
</Block>
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ export default observer(function SettingsPage() {
|
|||
</Block>
|
||||
<Block className="flex-1">
|
||||
<Text className="text-[16px] font-[700] text-black">{user?.name || user?.phoneNumber || '未登录'}</Text>
|
||||
{user?.phoneNumber && user?.name && (
|
||||
<Text className="mt-[4px] text-[12px] text-gray-500">{user.phoneNumber}</Text>
|
||||
{(user?.phoneNumber || (user?.email && user?.emailVerified)) && user?.name && (
|
||||
<Text className="mt-[4px] text-[12px] text-gray-500">{user.phoneNumber || user.email}</Text>
|
||||
)}
|
||||
</Block>
|
||||
<Ionicons color="#323232" name="chevron-forward" size={20} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue