diff --git a/App.tsx b/App.tsx index a6c3237..49fd3bd 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,7 @@ import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { CountrySelect } from "./app/screens/CountrySelect"; import { MainApp } from "./app/screens/MainApp"; -import { LoginScreen } from "./app/screens/LoginScreen"; +import { LoginScreen } from "./app/screens/loginList/index"; import { EmailLoginScreen } from "./app/screens/EmailLoginScreen"; import { GoogleScreen } from "./app/screens/login/Google"; import "./app/i18n"; @@ -48,6 +48,7 @@ import { EditAddress } from "./app/screens/address/EditAddress"; import { PaymentMethod } from "./app/screens/previewOrder/PaymentMethod"; import { ShippingFee } from "./app/screens/previewOrder/ShippingFee"; import { PreviewOrder } from "./app/screens/previewOrder/perviewOrder"; +import { ForgotPassword } from "./app/screens/loginList/ForgotPassword"; export type RootStackParamList = { CountrySelect: undefined; @@ -86,6 +87,7 @@ export type RootStackParamList = { PaymentMethod: undefined; ShippingFee: undefined; PreviewOrder: undefined; + ForgotPassword: undefined; }; const Stack = createNativeStackNavigator(); @@ -438,6 +440,15 @@ function AppContent() { gestureDirection: "horizontal", }} /> + diff --git a/app/constants/countries.ts b/app/constants/countries.ts index aead609..0e750e8 100644 --- a/app/constants/countries.ts +++ b/app/constants/countries.ts @@ -6,6 +6,18 @@ export interface Country { phoneCode: string; } + +export interface CountryList { + country: number; + currency: string; + language: string; + name: string; + name_en: string; + timezone: string; + user_count: number; + valid_digits:number[] +} + export const countries: Country[] = [ { code: 'CD', diff --git a/app/screens/CountrySelect.tsx b/app/screens/CountrySelect.tsx index bd362c3..2e864d2 100644 --- a/app/screens/CountrySelect.tsx +++ b/app/screens/CountrySelect.tsx @@ -8,20 +8,26 @@ import { StyleSheet, SafeAreaView, StatusBar, + Image, + ActivityIndicator, } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { Country, countries } from '../constants/countries'; +import { Country, CountryList } from '../constants/countries'; import Constants from 'expo-constants'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { RootStackParamList } from '../types/navigation'; +import { settingApi } from '../services/api/setting'; +import flagMap from '../utils/flagMap'; const SELECTED_COUNTRY_KEY = '@selected_country'; export const CountrySelect = () => { const { t } = useTranslation(); - const [selectedCountry, setSelectedCountry] = useState(''); + const [selectedCountry, setSelectedCountry] = useState(); + const [countryList, setCountryList] = useState([]); + const [loading, setLoading] = useState(true); const navigation = useNavigation>(); useEffect(() => { @@ -29,7 +35,9 @@ export const CountrySelect = () => { }, []); const checkSelectedCountry = async () => { + setLoading(true); try { + const savedCountry = await AsyncStorage.getItem(SELECTED_COUNTRY_KEY); if (savedCountry) { // 如果已经选择过国家,直接导航到主页面 @@ -37,39 +45,51 @@ export const CountrySelect = () => { const isCleared = await AsyncStorage.getItem('languageCleared'); if (!isCleared) { navigation.replace('MainTabs'); + } + }else{ + const res = await settingApi.getCountryList(); + setCountryList(res); + } } catch (error) { console.error('Error checking selected country:', error); + } finally { + setLoading(false); } }; - const handleCountrySelect = async (country: Country) => { + const handleCountrySelect = async (country: CountryList) => { try { await AsyncStorage.setItem(SELECTED_COUNTRY_KEY, JSON.stringify(country)); // 清除清除标记 await AsyncStorage.removeItem('languageCleared'); // 选择国家后导航到主页面 + setSelectedCountry(country.country); navigation.replace('MainTabs'); } catch (error) { console.error('Error saving selected country:', error); } }; - const renderCountryItem = ({ item }: { item: Country }) => ( + const renderCountryItem = ({ item }: { item: CountryList }) => ( handleCountrySelect(item)} > - {item.flag} + +{item.country} + + + {item.name} - {selectedCountry === item.code && ( + selectedCountry === item.country && styles.selectedText + ]}>{item.name_en} + + {selectedCountry === item.country && ( )} @@ -86,13 +106,20 @@ export const CountrySelect = () => { {t('selectCountry')} {t('subtitle')} - item.code} - style={styles.list} - showsVerticalScrollIndicator={false} - /> + + {loading ? ( + + + + ) : ( + item.country.toString()} + style={styles.list} + showsVerticalScrollIndicator={false} + /> + )} ); @@ -137,7 +164,8 @@ const styles = StyleSheet.create({ backgroundColor: '#f8f8f8', }, flag: { - fontSize: 24, + width: 24, + height: 24, marginRight: 16, }, countryName: { @@ -145,6 +173,13 @@ const styles = StyleSheet.create({ fontSize: 16, color: '#333', }, + countryCode: { + fontSize: 16, + color: '#333', + marginRight: 16, + width: 40, + textAlign: 'center', + }, selectedText: { color: '#007AFF', fontWeight: '500', @@ -153,4 +188,9 @@ const styles = StyleSheet.create({ fontSize: 20, color: '#007AFF', }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, }); \ No newline at end of file diff --git a/app/screens/HomeScreen.tsx b/app/screens/HomeScreen.tsx index 94d3c0e..7e7d7c9 100644 --- a/app/screens/HomeScreen.tsx +++ b/app/screens/HomeScreen.tsx @@ -282,7 +282,6 @@ export const HomeScreen = () => { width={screenWidth} data={data} height={widthUtils(286, 286).height} - onSnapToItem={(index) => setActiveIndex(index)} modeConfig={{ parallaxScrollingScale: 0.9, parallaxScrollingOffset: 50, diff --git a/app/screens/LoginScreen.tsx b/app/screens/LoginScreen.tsx index bfeb207..357e640 100644 --- a/app/screens/LoginScreen.tsx +++ b/app/screens/LoginScreen.tsx @@ -1,1339 +1,30 @@ -import React, { useEffect, useState, useRef } from "react"; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - StatusBar, - Platform, - BackHandler, - TextInput, - Animated, - Easing, - FlatList, - Keyboard, - Modal, - InteractionManager, - Image, - Alert -} from "react-native"; -import { useTranslation } from "react-i18next"; -import { useNavigation } from "@react-navigation/native"; -import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; -import { Country, countries } from "../constants/countries"; -import { useAuth } from "../contexts/AuthContext"; -import { settingApi } from "../services/api/setting"; -import { userApi } from "../services/api/userApi"; -import AsyncStorage from "@react-native-async-storage/async-storage"; - -import useUserStore from "../store/user"; - -type RootStackParamList = { - Login: undefined; - EmailLogin: undefined; - MainTabs: { screen: string }; - Google: undefined; - Home: { screen: string }; -}; - -// 常见邮箱后缀列表 -const EMAIL_DOMAINS = [ - "gmail.com", - "yahoo.com", - "hotmail.com", - "outlook.com", - "icloud.com", - "mail.com", - "protonmail.com", - "qq.com", - "163.com", - "126.com", -]; - -type LoginScreenProps = { - onClose?: () => void; - isModal?: boolean; -}; - -export const LoginScreen = ({ onClose, isModal }: LoginScreenProps) => { - const { t } = useTranslation(); - const navigation = - useNavigation>(); - const { login } = useAuth(); - const { setSettings, settings, user, setUser } = useUserStore(); - - // 邮箱登录状态 - const [showEmailLogin, setShowEmailLogin] = useState(false); - const [email, setEmail] = useState(""); - const [emailPassword, setEmailPassword] = useState(""); - const [emailPasswordError, setEmailPasswordError] = useState(false); - const [showSuggestions, setShowSuggestions] = useState(false); - const [suggestions, setSuggestions] = useState([]); - - // 手机号登录状态 - const [showPhoneLogin, setShowPhoneLogin] = useState(false); - const [phoneNumber, setPhoneNumber] = useState(""); - const [password, setPassword] = useState(""); - const [passwordError, setPasswordError] = useState(false); - const [showCountryModal, setShowCountryModal] = useState(false); - const [selectedCountry, setSelectedCountry] = useState({ - name: "Republic of Congo", - code: "CG", - flag: "🇨🇬", - userCount: 300000, - phoneCode: "+242", - }); - - // 动画值 - const emailSlideAnim = useRef(new Animated.Value(400)).current; - const phoneSlideAnim = useRef(new Animated.Value(400)).current; - const [emailLoginMounted, setEmailLoginMounted] = useState(false); - const [phoneLoginMounted, setPhoneLoginMounted] = useState(false); - - // 处理邮箱输入变化,生成邮箱建议 - const handleEmailChange = (text: string) => { - setEmail(text); - - // 检查是否包含@符号 - if (text.includes("@")) { - const [username, domain] = text.split("@"); - - if (domain) { - // 如果已经输入了部分域名,过滤匹配的域名 - const filteredDomains = EMAIL_DOMAINS.filter((item) => - item.toLowerCase().startsWith(domain.toLowerCase()) - ); - - // 生成完整的邮箱建议列表 - const emailSuggestions = filteredDomains.map((d) => `${username}@${d}`); - setSuggestions(emailSuggestions); - setShowSuggestions(emailSuggestions.length > 0); - } else { - // 如果只输入了@,显示所有域名建议 - const emailSuggestions = EMAIL_DOMAINS.map((d) => `${username}@${d}`); - setSuggestions(emailSuggestions); - setShowSuggestions(true); - } - } else if (text.length > 0) { - // 没有@符号但有输入内容,显示常见邮箱后缀建议 - const emailSuggestions = EMAIL_DOMAINS.map((d) => `${text}@${d}`); - setSuggestions(emailSuggestions); - setShowSuggestions(true); - } else { - // 输入为空,不显示建议 - setShowSuggestions(false); - } - }; - - // 选择一个邮箱建议 - const handleSelectSuggestion = (suggestion: string) => { - setEmail(suggestion); - setShowSuggestions(false); - }; - - // 验证邮箱格式 - const isValidEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - }; - - // 渲染单个邮箱建议项 - const renderSuggestionItem = ({ item }: { item: string }) => ( - handleSelectSuggestion(item)} - > - {item} - - ); - - // 选择国家 - const handleCountrySelect = (country: Country) => { - setSelectedCountry(country); - setShowCountryModal(false); - }; - - // 渲染国家列表项 - 添加性能优化 - const renderCountryItem = React.useCallback( - ({ item }: { item: Country }) => ( - handleCountrySelect(item)} - activeOpacity={0.7} - > - {item.flag} - - {item.name} - {item.phoneCode} - - - ), - [] - ); - - // 添加Android返回按钮处理 - useEffect(() => { - const backAction = () => { - if (showEmailLogin) { - closeEmailLogin(); - return true; - } - if (showPhoneLogin) { - closePhoneLogin(); - return true; - } - if (showCountryModal) { - setShowCountryModal(false); - return true; - } - handleClose(); - return true; - }; - - const backHandler = BackHandler.addEventListener( - "hardwareBackPress", - backAction - ); - - return () => backHandler.remove(); - }, [showEmailLogin, showPhoneLogin, showCountryModal]); - - // 初始化动画配置 - useEffect(() => { - // React Native Animated API不支持直接设置useNativeDriver - // 这些配置会在动画函数中使用 - }, []); - - // 打开邮箱登录面板 - const openEmailLogin = () => { - emailSlideAnim.setValue(400); - setEmailLoginMounted(true); - - // 等待组件挂载后启动动画 - requestAnimationFrame(() => { - setShowEmailLogin(true); - Animated.spring(emailSlideAnim, { - toValue: 0, - useNativeDriver: true, - friction: 8, - tension: 40, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, - }).start(); - }); - }; - - // 关闭邮箱登录面板 - const closeEmailLogin = () => { - Keyboard.dismiss(); - - // 使用更简单的配置减少计算复杂度 - Animated.timing(emailSlideAnim, { - toValue: 400, - duration: 200, - useNativeDriver: true, - easing: Easing.cubic, - }).start(); - - // 先隐藏建议列表,减少布局计算 - setShowSuggestions(false); - - // 使用InteractionManager确保动画完成后再执行耗时操作 - InteractionManager.runAfterInteractions(() => { - setShowEmailLogin(false); - setEmail(""); - setEmailPassword(""); - setEmailPasswordError(false); - - // 延迟卸载组件 - setTimeout(() => { - setEmailLoginMounted(false); - }, 50); - }); - }; - - // 打开手机号登录面板 - const openPhoneLogin = () => { - phoneSlideAnim.setValue(400); - setPhoneLoginMounted(true); - - // 等待组件挂载后启动动画 - requestAnimationFrame(() => { - setShowPhoneLogin(true); - Animated.spring(phoneSlideAnim, { - toValue: 0, - useNativeDriver: true, - friction: 8, - tension: 40, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, - }).start(); - }); - }; - - // 关闭手机号登录面板 - const closePhoneLogin = () => { - Keyboard.dismiss(); - - // 使用更简单的配置减少计算复杂度 - Animated.timing(phoneSlideAnim, { - toValue: 400, - duration: 200, - useNativeDriver: true, - easing: Easing.cubic, - }).start(); - - // 使用InteractionManager确保动画完成后再执行耗时操作 - InteractionManager.runAfterInteractions(() => { - setShowPhoneLogin(false); - setPhoneNumber(""); - - // 延迟卸载组件 - setTimeout(() => { - setPhoneLoginMounted(false); - }, 50); - }); - }; - - // 修改关闭按钮处理函数 - const handleClose = () => { - if (isModal && onClose) { - onClose(); - } else { - navigation.goBack(); - } - }; - - // 处理谷歌登录 - const handleGoogleLogin = async () => { - navigation.navigate("Google"); - }; - - const handleFacebookLogin = () => { - // 处理Facebook登录 - }; - - const handleAppleLogin = () => { - // 处理Apple登录 - }; - - const handleInstagramLogin = () => { - // 处理Instagram登录 - }; - - const handleEmailLogin = () => { - // 处理邮箱登录 - 显示邮箱登录面板 - openEmailLogin(); - }; - - //邮箱登录 - const handleEmailContinue = async () => { - const params = { - grant_type: "password", - username: "lifei", - password: "123456", - client_id: "2", - client_secret: "", - scope: "", - }; - try { - const res = await userApi.login(params); - if (res.access_token) { - const token = res.token_type + " " + res.access_token; - await AsyncStorage.setItem("token", token); - const data = await settingApi.postFirstLogin(221); - setSettings(data); - const user = await userApi.getProfile() - setUser(user); - navigation.navigate("MainTabs", { screen: "Home" }); - } - } catch (error) { - Alert.alert(t("loginFailed")); - } - }; - - const handlePhoneLogin = () => { - // 处理手机号登录 - 显示手机号登录面板 - openPhoneLogin(); - }; - - const handlePhoneVerificationSuccess = async () => { - await login(); - closePhoneLogin(); - navigation.replace("MainTabs", { screen: "Home" }); - }; - - // 处理手机号登录 - const handlePhoneContinue = async () => { - // if (phoneNumber.trim() && password) { - // if (password === "123") { - // await login(); - // closePhoneLogin(); - // navigation.replace("MainTabs", { screen: "Home" }); - // } else { - // setPasswordError(true); - // } - // } - - const params = { - grant_type: "password", - username: phoneNumber, - password: password, - client_id: "2", - client_secret: "", - scope: "", - }; - try { - const res = await userApi.login(params); - if (res.access_token) { - const token = res.token_type + " " + res.access_token; - await AsyncStorage.setItem("token", token); - const data = await settingApi.postFirstLogin(221); - setSettings(data); - const user = await userApi.getProfile() - setUser(user); - navigation.navigate("MainTabs", { screen: "Home" }); - } - } catch (error) { - Alert.alert(t("loginFailed")); - } - console.log(phoneNumber,password); - - }; - - const handleForgotPassword = () => { - // 处理忘记密码 - }; +import React from "react"; +import { View, Platform } from "react-native"; +import LoginScreen from "./loginList"; +export default function LoginScreenWrapper() { return ( - - - - {/* 顶部蓝色背景区域 */} - - brainnel - - - - 💰 - - {t("wholesalePrice")} - - - - 🚚 - - {t("fastShipping")} - - - - - {/* 登录区域 */} - - - × - - - - {/* Login For brainnel */} - {t("loginSubtitle")} - - - {/* 登录按钮 */} - - - - - {t("continueWithGoogle")} - - - - - f - - - {t("continueWithFacebook")} - - - - {Platform.OS === "ios" && ( - - - 🍎 - - {t("continueWithApple")} - - )} - - - - 📷 - - - {t("continueWithInstagram")} - - - - - - ✉️ - - {t("continueWithEmail")} - - - - - 📱 - - {t("continueWithPhone")} - - - {/* 忘记密码 */} - - {t("forgotPassword")} - - - {/* 服务条款 */} - - - {t("termsText")} {t("termsOfUse")} - - - {t("and")} {t("privacyPolicy")} - - - - - {/* 邮箱登录面板 - 从底部滑出 */} - {emailLoginMounted && ( - - - - - - {t("logInOrSignUp")} - - - - - { - handleEmailChange(text); - setEmailPasswordError(false); - }} - keyboardType="email-address" - autoCapitalize="none" - autoCorrect={false} - autoFocus - maxLength={50} - /> - {email.length > 0 && ( - { - setEmail(""); - setShowSuggestions(false); - setEmailPasswordError(false); - }} - activeOpacity={0.7} - > - - - )} - - - {/* 邮箱后缀建议列表 */} - {showSuggestions && ( - - item} - style={styles.suggestionsList} - keyboardShouldPersistTaps="handled" - removeClippedSubviews={true} - initialNumToRender={5} - maxToRenderPerBatch={5} - windowSize={5} - getItemLayout={(data, index) => ({ - length: 44, - offset: 44 * index, - index, - })} - /> - - )} - - {/* 密码输入框 */} - - { - setEmailPassword(text); - setEmailPasswordError(false); - }} - secureTextEntry={true} - autoCapitalize="none" - /> - {emailPasswordError && ( - - ! - - )} - - - {/* 密码错误提示 */} - {emailPasswordError && ( - <> - - {t("passwordIncorrect")} - - - - - {t("forgotPassword")} - - - - )} - - - - {t("continue")} - - - - - )} - - {/* 手机号登录面板 - 从底部滑出 */} - {phoneLoginMounted && ( - - - - - - {t("logInOrSignUp")} - - - - - setShowCountryModal(true)} - activeOpacity={0.7} - > - {selectedCountry.flag} - - {selectedCountry.phoneCode} - - - - - { - setPhoneNumber(text); - setPasswordError(false); - }} - keyboardType="phone-pad" - autoFocus - maxLength={15} - /> - {phoneNumber.length > 0 && ( - { - setPhoneNumber(""); - setPasswordError(false); - }} - activeOpacity={0.7} - > - - - )} - - - {/* 密码输入框 */} - - { - setPassword(text); - setPasswordError(false); - }} - secureTextEntry={true} - autoCapitalize="none" - /> - {passwordError && ( - - ! - - )} - - - {/* 密码错误提示 */} - {passwordError && ( - <> - - {t("passwordIncorrect")} - - - - - {t("forgotPassword")} - - - - )} - - - - - - {t("continue")} - - - - - )} - - {/* 国家选择模态框 */} - setShowCountryModal(false)} - hardwareAccelerated={true} - statusBarTranslucent={true} - presentationStyle="overFullScreen" - > - - - - setShowCountryModal(false)} - activeOpacity={0.7} - > - - - {t("selectCountry")} - - item.code} - style={styles.countryList} - showsVerticalScrollIndicator={false} - removeClippedSubviews={true} - initialNumToRender={10} - maxToRenderPerBatch={10} - windowSize={10} - getItemLayout={(data, index) => ({ - length: 69, - offset: 69 * index, - index, - })} - /> - - - + + ); +} + +// 手机号登录样式 +const phoneLoginContainer = { + position: "absolute", + top: Platform.OS === "ios" ? 60 : 40, + height: "80%", + bottom: 0, + left: 0, + right: 0, + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + zIndex: 10, }; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#fff", - }, - closeButton: { - position: "absolute", - top: 15, - left: 10, - width: 24, - height: 24, - justifyContent: "center", - alignItems: "center", - zIndex: 1, - }, - closeButtonText: { - color: "#000", - fontSize: 24, - fontWeight: "300", - }, - blueHeader: { - backgroundColor: "#0066FF", - paddingHorizontal: 20, - paddingBottom: 20, - paddingTop: Platform.OS === "ios" ? 60 : 40, - borderBottomLeftRadius: 24, - borderBottomRightRadius: 24, - }, - logo: { - fontSize: 28, - fontWeight: "bold", - color: "#fff", - marginBottom: 15, - }, - features: { - flexDirection: "row", - gap: 16, - }, - featureItem: { - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - featureIconContainer: { - backgroundColor: "rgba(255, 255, 255, 0.2)", - borderRadius: 8, - width: 24, - height: 24, - justifyContent: "center", - alignItems: "center", - }, - featureIcon: { - fontSize: 12, - }, - featureText: { - fontSize: 14, - color: "#fff", - }, - loginContainer: { - flex: 1, - paddingHorizontal: 20, - paddingTop: Platform.OS === "ios" ? 40 : 20, - }, - titleContainer: { - alignItems: "center", - marginBottom: 30, - paddingTop: 20, - position: "relative", - }, - title: { - fontSize: 24, - fontWeight: "bold", - color: "#000", - marginBottom: 8, - textAlign: "center", - }, - subtitle: { - fontSize: 14, - color: "#666", - textAlign: "center", - }, - loginButton: { - flexDirection: "row", - height: 50, - borderRadius: 25, - borderWidth: 1, - borderColor: "#E1E1E1", - alignItems: "center", - marginBottom: 12, - paddingHorizontal: 16, - backgroundColor: "#fff", - }, - loginButtonIcon: { - width: 24, - height: 24, - borderRadius: 12, - justifyContent: "center", - alignItems: "center", - marginRight: 16, - }, - facebookIcon: { - backgroundColor: "#3b5998", - }, - appleIconBg: { - backgroundColor: "#000", - }, - instagramIcon: { - backgroundColor: "#E1306C", - }, - loginButtonText: { - flex: 1, - fontSize: 16, - color: "#000", - textAlign: "center", - marginRight: 16, - }, - forgotPassword: { - alignItems: "center", - marginVertical: 20, - }, - forgotPasswordText: { - color: "#0066FF", - fontSize: 14, - }, - termsContainer: { - alignItems: "center", - marginTop: 10, - }, - terms: { - fontSize: 12, - color: "#666", - textAlign: "center", - lineHeight: 18, - }, - link: { - color: "#0066FF", - }, - - // 邮箱登录样式 - emailLoginContainer: { - position: "absolute", - top: Platform.OS === "ios" ? 60 : 40, - height: Platform.OS === "ios" ? "auto" : "100%", - bottom: 0, - left: 0, - right: 0, - backgroundColor: "#fff", - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 5, - elevation: 5, - zIndex: 10, - }, - emailLoginHeader: { - flexDirection: "row", - alignItems: "center", - paddingTop: 20, - paddingHorizontal: 16, - paddingBottom: 15, - borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", - }, - emailLoginCloseButton: { - padding: 8, - width: 36, - height: 36, - justifyContent: "center", - alignItems: "center", - }, - emailLoginCloseButtonText: { - fontSize: 18, - color: "#000", - }, - emailLoginTitle: { - flex: 1, - fontSize: 18, - fontWeight: "600", - color: "#000", - textAlign: "center", - marginRight: 36, - }, - emailLoginContent: { - padding: 20, - paddingBottom: Platform.OS === "ios" ? 50 : 30, - flex: 1, - }, - emailInputContainer: { - flexDirection: "row", - alignItems: "center", - borderWidth: 1, - borderColor: "#E1E1E1", - borderRadius: 25, - height: 50, - marginBottom: 20, - }, - emailInput: { - flex: 1, - height: "100%", - paddingHorizontal: 16, - fontSize: 16, - paddingRight: 36, - }, - emailClearButton: { - position: "absolute", - right: 12, - top: "50%", - transform: [{ translateY: -12 }], - height: 24, - width: 24, - justifyContent: "center", - alignItems: "center", - }, - emailClearButtonText: { - fontSize: 16, - color: "#999", - fontWeight: "500", - textAlign: "center", - }, - suggestionsContainer: { - borderWidth: 1, - borderColor: "#E1E1E1", - borderRadius: 10, - marginTop: -10, - marginBottom: 20, - maxHeight: 200, - backgroundColor: "#fff", - }, - suggestionsList: { - padding: 8, - }, - suggestionItem: { - paddingVertical: 12, - paddingHorizontal: 16, - borderBottomWidth: 1, - borderBottomColor: "#F0F0F0", - }, - suggestionText: { - fontSize: 16, - color: "#333", - }, - emailContinueButton: { - height: 50, - backgroundColor: "#0039CB", - borderRadius: 25, - justifyContent: "center", - alignItems: "center", - marginTop: 20, - }, - emailDisabledButton: { - backgroundColor: "#CCCCCC", - }, - emailContinueButtonText: { - color: "#fff", - fontSize: 16, - fontWeight: "600", - }, - - // 手机号登录样式 - phoneLoginContainer: { - position: "absolute", - top: Platform.OS === "ios" ? 60 : 40, - height: Platform.OS === "ios" ? "auto" : "100%", - bottom: 0, - left: 0, - right: 0, - backgroundColor: "#fff", - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.1, - shadowRadius: 5, - elevation: 5, - zIndex: 10, - }, - phoneLoginHeader: { - flexDirection: "row", - alignItems: "center", - paddingTop: 20, - paddingHorizontal: 16, - paddingBottom: 15, - borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", - }, - phoneLoginCloseButton: { - padding: 8, - width: 36, - height: 36, - justifyContent: "center", - alignItems: "center", - }, - phoneLoginCloseButtonText: { - fontSize: 18, - color: "#000", - }, - phoneLoginTitle: { - flex: 1, - fontSize: 18, - fontWeight: "600", - color: "#000", - textAlign: "center", - marginRight: 36, - }, - phoneLoginContent: { - padding: 20, - paddingBottom: Platform.OS === "ios" ? 50 : 30, - flex: 1, - }, - phoneInputContainer: { - flexDirection: "row", - alignItems: "center", - borderWidth: 1, - borderColor: "#E1E1E1", - borderRadius: 25, - height: 50, - marginBottom: 20, - position: "relative", - }, - countrySelector: { - flexDirection: "row", - alignItems: "center", - paddingHorizontal: 12, - borderRightWidth: 1, - borderRightColor: "#E1E1E1", - backgroundColor: "#F7F7F7", - height: "100%", - minWidth: 90, - width: 90, - justifyContent: "center", - }, - countryFlag: { - fontSize: 18, - marginRight: 4, - }, - countryCode: { - fontSize: 12, - color: "#333", - marginRight: 4, - }, - downArrow: { - fontSize: 8, - color: "#666", - }, - phoneInput: { - flex: 1, - height: "100%", - paddingHorizontal: 16, - fontSize: 16, - paddingRight: 36, - }, - phoneClearButton: { - position: "absolute", - right: 12, - top: "50%", - transform: [{ translateY: -12 }], - height: 24, - width: 24, - justifyContent: "center", - alignItems: "center", - }, - phoneClearButtonText: { - fontSize: 16, - color: "#999", - fontWeight: "500", - textAlign: "center", - }, - phoneInfoText: { - fontSize: 14, - color: "#666", - marginBottom: 32, - lineHeight: 20, - }, - phoneContinueButton: { - height: 50, - backgroundColor: "#0039CB", - borderRadius: 25, - justifyContent: "center", - alignItems: "center", - }, - phoneDisabledButton: { - backgroundColor: "#CCCCCC", - }, - phoneContinueButtonText: { - color: "#fff", - fontSize: 16, - fontWeight: "600", - }, - - // 国家选择模态框样式 - countryModalContainer: { - flex: 1, - backgroundColor: "rgba(0,0,0,0.5)", - justifyContent: "flex-end", - zIndex: 999, - }, - countryModalContent: { - backgroundColor: "#fff", - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - maxHeight: "80%", - zIndex: 1000, - }, - countryModalHeader: { - flexDirection: "row", - alignItems: "center", - padding: 16, - borderBottomWidth: 1, - borderBottomColor: "#E5E5E5", - }, - countryModalCloseButton: { - padding: 4, - }, - countryModalCloseButtonText: { - fontSize: 18, - color: "#999", - }, - countryModalTitle: { - flex: 1, - fontSize: 18, - fontWeight: "600", - textAlign: "center", - marginRight: 24, - }, - countryList: { - padding: 8, - }, - countryItem: { - flexDirection: "row", - alignItems: "center", - padding: 16, - borderBottomWidth: 1, - borderBottomColor: "#F0F0F0", - }, - countryItemFlag: { - fontSize: 24, - marginRight: 16, - }, - countryItemContent: { - flex: 1, - }, - countryItemName: { - fontSize: 16, - color: "#333", - }, - countryItemCode: { - fontSize: 14, - color: "#666", - marginTop: 4, - }, - // 密码输入框样式 - passwordInput: { - flex: 1, - height: "100%", - paddingHorizontal: 16, - fontSize: 16, - }, - passwordErrorContainer: { - borderColor: "#FF3B30", - }, - passwordErrorIcon: { - position: "absolute", - right: 12, - top: "50%", - transform: [{ translateY: -12 }], - width: 24, - height: 24, - backgroundColor: "#FF3B30", - borderRadius: 12, - justifyContent: "center", - alignItems: "center", - }, - passwordErrorIconText: { - color: "white", - fontWeight: "bold", - fontSize: 16, - }, - passwordErrorText: { - color: "#FF3B30", - fontSize: 14, - marginTop: -12, - marginBottom: 16, - paddingHorizontal: 5, - }, - forgotPasswordLink: { - alignItems: "center", - marginTop: 5, - }, - forgotPasswordLinkText: { - color: "#0066FF", - fontSize: 14, - }, -}); diff --git a/app/screens/ProfileScreen.tsx b/app/screens/ProfileScreen.tsx index 3d1f2d9..1ae69f4 100644 --- a/app/screens/ProfileScreen.tsx +++ b/app/screens/ProfileScreen.tsx @@ -144,6 +144,18 @@ export const ProfileScreen = () => { style={styles.timecardWidget} resizeMode="stretch" > + + + navigation.navigate("SettingList")} + > + + + + + + + void; +}; + +const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => { + const { t } = useTranslation(); + const navigation = useNavigation>(); + const { setSettings, setUser } = useUserStore(); + + // 状态管理 + const [email, setEmail] = useState(""); + const [emailPassword, setEmailPassword] = useState(""); + const [emailPasswordError, setEmailPasswordError] = useState(false); + const [showSuggestions, setShowSuggestions] = useState(false); + const [suggestions, setSuggestions] = useState([]); + + // 防止重复关闭 + const isClosing = useRef(false); + + // 引用输入框 + const emailInputRef = useRef(null); + const passwordInputRef = useRef(null); + + // 主动弹出键盘 + const focusEmailInput = () => { + if (emailInputRef.current) { + emailInputRef.current.focus(); + } + }; + + const focusPasswordInput = () => { + if (passwordInputRef.current) { + passwordInputRef.current.focus(); + } + }; + + React.useEffect(() => { + if (visible) { + // 当模态框显示时,等待动画完成后主动弹出键盘 + const timer = setTimeout(() => { + focusEmailInput(); + }, 300); + + return () => clearTimeout(timer); + } + }, [visible]); + + // 处理邮箱输入变化 + const handleEmailChange = (text: string) => { + setEmail(text); + + // Check if it includes @ symbol + if (text.includes("@")) { + const [username, domain] = text.split("@"); + + if (domain) { + // If domain part is already entered, filter matching domains + const filteredDomains = EMAIL_DOMAINS.filter((item) => + item.toLowerCase().startsWith(domain.toLowerCase()) + ); + + // Generate complete email suggestion list + const emailSuggestions = filteredDomains.map((d) => `${username}@${d}`); + setSuggestions(emailSuggestions); + setShowSuggestions(emailSuggestions.length > 0); + } else { + // If only @ is entered, show all domain suggestions + const emailSuggestions = EMAIL_DOMAINS.map((d) => `${username}@${d}`); + setSuggestions(emailSuggestions); + setShowSuggestions(true); + } + } else if (text.length > 0) { + // No @ symbol but has input content, show common email suffix suggestions + const emailSuggestions = EMAIL_DOMAINS.map((d) => `${text}@${d}`); + setSuggestions(emailSuggestions); + setShowSuggestions(true); + } else { + // Empty input, don't show suggestions + setShowSuggestions(false); + } + }; + + // Select an email suggestion + const handleSelectSuggestion = (suggestion: string) => { + setEmail(suggestion); + setShowSuggestions(false); + }; + + // Validate email format + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Render a single email suggestion item + const renderSuggestionItem = ({ item }: { item: string }) => ( + handleSelectSuggestion(item)} + > + {item} + + ); + + // Handle forgot password + const handleForgotPassword = () => { + // Handle forgot password logic + }; + + // Handle email login + const handleEmailContinue = async () => { + const params = { + grant_type: "password", + username: "lifei", + password: "123456", + client_id: "2", + client_secret: "", + scope: "", + }; + try { + const res = await userApi.login(params); + if (res.access_token) { + const token = res.token_type + " " + res.access_token; + await AsyncStorage.setItem("token", token); + const data = await settingApi.postFirstLogin(221); + setSettings(data); + const user = await userApi.getProfile(); + setUser(user); + navigation.navigate("MainTabs", { screen: "Home" }); + onClose(); + } + } catch (error) { + Alert.alert(t("loginFailed")); + } + }; + + // 安全地关闭模态框 + const closeEmailLogin = () => { + console.log("Closing email login modal"); + // 确保键盘关闭 + Keyboard.dismiss(); + // 直接调用关闭回调 + onClose(); + }; + + return ( + onClose()} + statusBarTranslucent={true} + > + + + + onClose()} + activeOpacity={0.7} + > + + + {t("logInOrSignUp")} + + + + + { + handleEmailChange(text); + setEmailPasswordError(false); + }} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + autoFocus + maxLength={50} + /> + {email.length > 0 ? ( + { + setEmail(""); + setShowSuggestions(false); + setEmailPasswordError(false); + }} + activeOpacity={0.7} + > + + + ) : ( + + ⌨️ + + )} + + + {/* Email suffix suggestion list */} + {showSuggestions && ( + + item} + style={styles.suggestionsList} + keyboardShouldPersistTaps="handled" + removeClippedSubviews={true} + initialNumToRender={5} + maxToRenderPerBatch={5} + windowSize={5} + getItemLayout={(data, index) => ({ + length: 44, + offset: 44 * index, + index, + })} + /> + + )} + + {/* Password input */} + + { + setEmailPassword(text); + setEmailPasswordError(false); + }} + secureTextEntry={true} + autoCapitalize="none" + /> + {emailPasswordError ? ( + + ! + + ) : ( + + ⌨️ + + )} + + + {/* Password error message */} + {emailPasswordError && ( + <> + + {t("passwordIncorrect")} + + + + + {t("forgotPassword")} + + + + )} + + + + {t("continue")} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + modalContainer: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.5)", + justifyContent: "flex-end", + zIndex: 999, + }, + emailLoginContainer: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: "80%", + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + }, + emailLoginHeader: { + flexDirection: "row", + alignItems: "center", + paddingTop: 20, + paddingHorizontal: 16, + paddingBottom: 15, + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + emailLoginCloseButton: { + padding: 8, + width: 36, + height: 36, + justifyContent: "center", + alignItems: "center", + }, + emailLoginCloseButtonText: { + fontSize: 18, + color: "#000", + }, + emailLoginTitle: { + flex: 1, + fontSize: 18, + fontWeight: "600", + color: "#000", + textAlign: "center", + marginRight: 36, + }, + emailLoginContent: { + padding: 20, + paddingBottom: Platform.OS === "ios" ? 50 : 30, + flex: 1, + }, + emailInputContainer: { + flexDirection: "row", + alignItems: "center", + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + height: 50, + marginBottom: 20, + }, + emailInput: { + flex: 1, + height: "100%", + paddingHorizontal: 16, + fontSize: 16, + paddingRight: 36, + }, + emailClearButton: { + position: "absolute", + right: 12, + top: "50%", + transform: [{ translateY: -12 }], + height: 24, + width: 24, + justifyContent: "center", + alignItems: "center", + }, + emailClearButtonText: { + fontSize: 16, + color: "#999", + fontWeight: "500", + textAlign: "center", + }, + suggestionsContainer: { + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 10, + marginTop: -10, + marginBottom: 20, + maxHeight: 200, + backgroundColor: "#fff", + }, + suggestionsList: { + padding: 8, + }, + suggestionItem: { + paddingVertical: 12, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: "#F0F0F0", + }, + suggestionText: { + fontSize: 16, + color: "#333", + }, + passwordContainer: { + flexDirection: "row", + alignItems: "center", + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + height: 50, + marginBottom: 20, + position: "relative", + }, + passwordInput: { + flex: 1, + height: "100%", + paddingHorizontal: 16, + fontSize: 16, + }, + passwordErrorContainer: { + borderColor: "#FF3B30", + }, + passwordErrorIcon: { + position: "absolute", + right: 12, + top: "50%", + transform: [{ translateY: -12 }], + width: 24, + height: 24, + backgroundColor: "#FF3B30", + borderRadius: 12, + justifyContent: "center", + alignItems: "center", + }, + passwordErrorIconText: { + color: "white", + fontWeight: "bold", + fontSize: 16, + }, + passwordErrorText: { + color: "#FF3B30", + fontSize: 14, + marginTop: -12, + marginBottom: 16, + paddingHorizontal: 5, + }, + forgotPasswordLink: { + alignItems: "center", + marginTop: 5, + }, + forgotPasswordLinkText: { + color: "#0066FF", + fontSize: 14, + }, + emailContinueButton: { + height: 50, + backgroundColor: "#0039CB", + borderRadius: 25, + justifyContent: "center", + alignItems: "center", + marginTop: 20, + }, + emailDisabledButton: { + backgroundColor: "#CCCCCC", + }, + emailContinueButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, +}); + +export default EmailLoginModal; \ No newline at end of file diff --git a/app/screens/loginList/ForgotPassword.tsx b/app/screens/loginList/ForgotPassword.tsx new file mode 100644 index 0000000..8261f64 --- /dev/null +++ b/app/screens/loginList/ForgotPassword.tsx @@ -0,0 +1,7 @@ +import { View, Text } from "react-native"; + +export const ForgotPassword = () => { + return + 忘记密码 + +} \ No newline at end of file diff --git a/app/screens/loginList/PhoneLoginModal.tsx b/app/screens/loginList/PhoneLoginModal.tsx new file mode 100644 index 0000000..2e6a73a --- /dev/null +++ b/app/screens/loginList/PhoneLoginModal.tsx @@ -0,0 +1,798 @@ +import React, { useState, useRef, useCallback } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + FlatList, + Keyboard, + Modal, + Alert, + Platform, + Image, + ActivityIndicator, + StyleProp, + ViewStyle, + TextStyle, + ImageStyle +} from "react-native"; +import { useTranslation } from "react-i18next"; +import { useNavigation } from "@react-navigation/native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { userApi } from "../../services/api/userApi"; +import { settingApi } from "../../services/api/setting"; +import useUserStore from "../../store/user"; +import { CountryList } from "../../constants/countries"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; + +type RootStackParamList = { + Login: undefined; + EmailLogin: undefined; + MainTabs: { screen: string }; + Google: undefined; + Home: { screen: string }; + ForgotPassword: undefined; +}; + +type PhoneLoginModalProps = { + visible: boolean; + onClose: () => void; +}; + +// Define the styles type +type Styles = { + phoneLoginContainer: ViewStyle; + phoneLoginHeader: ViewStyle; + phoneLoginCloseButton: ViewStyle; + phoneLoginCloseButtonText: TextStyle; + phoneLoginTitle: TextStyle; + phoneLoginContent: ViewStyle; + phoneInputContainer: ViewStyle; + countrySelectRow: ViewStyle; + countrySelectContent: ViewStyle; + countryFlag: TextStyle; + flag: ImageStyle; + countryName: TextStyle; + countryCode: TextStyle; + downArrow: TextStyle; + phoneInput: TextStyle; + phoneClearButton: ViewStyle; + phoneClearButtonText: TextStyle; + phoneInfoText: TextStyle; + phoneContinueButton: ViewStyle; + phoneDisabledButton: ViewStyle; + phoneContinueButtonText: TextStyle; + passwordInput: TextStyle; + passwordErrorContainer: ViewStyle; + passwordErrorIcon: ViewStyle; + passwordErrorIconText: TextStyle; + passwordErrorText: TextStyle; + forgotPasswordLink: ViewStyle; + forgotPasswordLinkText: TextStyle; + countryModalContainer: ViewStyle; + countryModalOverlay: ViewStyle; + countryModalContent: ViewStyle; + modalHandleContainer: ViewStyle; + modalHandle: ViewStyle; + countryModalHeader: ViewStyle; + countryModalCloseButton: ViewStyle; + countryModalCloseButtonText: TextStyle; + countryModalTitle: TextStyle; + countryList: ViewStyle; + countryItem: ViewStyle; + countryItemFlag: TextStyle; + countryItemContent: ViewStyle; + countryItemName: TextStyle; + countryItemCode: TextStyle; + modalContainer: ViewStyle; + checkmark: TextStyle; + countryCodeButton: ViewStyle; + countryCodeFlag: ImageStyle; + countryCodeText: TextStyle; + countryCodeArrow: TextStyle; + phoneInputDivider: ViewStyle; + phoneNumberErrorText: TextStyle; +}; + +const PhoneLoginModal = ({ visible, onClose }: PhoneLoginModalProps) => { + const { t } = useTranslation(); + const navigation = + useNavigation>(); + const { setSettings, setUser } = useUserStore(); + + // Phone login state + const [phoneNumber, setPhoneNumber] = useState(""); + const [password, setPassword] = useState(""); + const [passwordError, setPasswordError] = useState(false); + const [phoneNumberError, setPhoneNumberError] = useState(false); + const [showCountryModal, setShowCountryModal] = useState(false); + // Countries + const [countryList, setCountryList] = useState([]); + const [selectedCountry, setSelectedCountry] = useState(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + + // Validate phone number against valid_digits + const validatePhoneNumber = (phoneNum: string) => { + if (!selectedCountry || !selectedCountry.valid_digits || selectedCountry.valid_digits.length === 0) { + return true; // No validation if no valid_digits available + } + + return selectedCountry.valid_digits.includes(phoneNum.length); + }; + + // Handle phone number input with validation + const handlePhoneNumberChange = (text: string) => { + setPhoneNumber(text); + if (text.length > 0) { + setPhoneNumberError(!validatePhoneNumber(text)); + // todo 防止重复关闭 + } else { + setPhoneNumberError(false); + } + setPasswordError(false); + }; + + // useEffect替换为普通函数 + React.useEffect(() => { + if (visible) { + loadData(); + } + }, [visible]); + + // 加载国家列表和选中的国家 + const loadData = async () => { + try { + + const res = await settingApi.getSendSmsCountryList(); + console.log(res); + + setCountryList(res); + + + const savedCountry = await AsyncStorage.getItem("@selected_country"); + if (savedCountry) { + try { + const parsedCountry = JSON.parse(savedCountry); + console.log(parsedCountry); + + const item = res.find(item => item.country === parsedCountry.country); + console.log(item); + + setSelectedCountry(item); + } catch (e) { + console.error("Error parsing stored country", e); + } + } + } catch (error) { + console.error("Failed to load country data", error); + } + }; + + // Select country + const handleCountrySelect = (country: CountryList) => { + setSelectedCountry(country); + setShowCountryModal(false); + + // Save selected country to AsyncStorage + AsyncStorage.setItem("@selected_country", JSON.stringify(country)); + }; + + // Render country list item - with performance optimization + const renderCountryItem = useCallback( + ({ item }: { item: CountryList }) => ( + handleCountrySelect(item)} + activeOpacity={0.7} + > + + +{item.country} + {/* */} + {item.name_en} + + {/* Add checkmark for selected country */} + {selectedCountry && selectedCountry.country === item.country && ( + + )} + + ), + [selectedCountry] + ); + + // 忘记密码 + const handleForgotPassword = () => { + onClose(); + navigation.navigate("ForgotPassword"); + }; + + // Handle phone login + const handlePhoneContinue = async () => { + // Validate phone number before proceeding + + // todo 防止重复关闭 + if (!validatePhoneNumber(phoneNumber)) { + setPhoneNumberError(true); + return; + } + + const params = { + grant_type: "password", + username: phoneNumber, + password: password, + client_id: "2", + client_secret: "", + scope: "", + }; + try { + setLoading(true); + const res = await userApi.login(params); + if (res.access_token) { + const token = res.token_type + " " + res.access_token; + await AsyncStorage.setItem("token", token); + const data = await settingApi.postFirstLogin(221); + setSettings(data); + const user = await userApi.getProfile(); + setUser(user); + setLoading(false); + navigation.navigate("MainTabs", { screen: "Home" }); + onClose(); + } + } catch (error) { + setError('用户名或密码错误') + setLoading(false); + setPasswordError(true); + } + }; + + + + // 引用输入框 + const phoneInputRef = useRef(null); + + // 主动弹出键盘 + const focusPhoneInput = () => { + if (phoneInputRef.current) { + phoneInputRef.current.focus(); + } + }; + + React.useEffect(() => { + if (visible) { + // 当模态框显示时,等待动画完成后主动弹出键盘 + const timer = setTimeout(() => { + focusPhoneInput(); + }, 300); + + return () => clearTimeout(timer); + } + }, [visible]); + + return ( + onClose()} + statusBarTranslucent={true} + > + + + + onClose()} + activeOpacity={0.7} + > + + + {t("logInOrSignUp")} + + + + {/* Country Selector Row - Now removed as we integrated it into the phone input */} + + + setShowCountryModal(true)} + > + {/* {selectedCountry?.name_en && ( + + )} */} + + +{selectedCountry?.country} + + + + + + {phoneNumber.length > 0 ? ( + { + setPhoneNumber(""); + setPhoneNumberError(false); + setPasswordError(false); + }} + activeOpacity={0.7} + > + + + ) : ( + + ⌨️ + + )} + + + {/* Phone number error message */} + {phoneNumberError && ( + + {t("invalidPhoneNumber")} + {selectedCountry?.valid_digits && + `(${t("requiresDigits")}: ${selectedCountry.valid_digits.join(', ')})`} + + )} + + {/* Password input */} + + { + setPassword(text); + setPasswordError(false); + }} + secureTextEntry={true} + autoCapitalize="none" + /> + {passwordError && ( + + ! + + )} + + + {/* Password error message */} + {passwordError && ( + <> + + {error} + + + + + {t("forgotPassword")} + + + + )} + + + + + {loading ? ( + + ) : ( + + {t("continue")} + + )} + + + + {/* Country selection modal */} + setShowCountryModal(false)} + hardwareAccelerated={true} + statusBarTranslucent={true} + presentationStyle="overFullScreen" + > + + setShowCountryModal(false)} + /> + + + + + + setShowCountryModal(false)} + activeOpacity={0.7} + > + + + + {t("selectCountry")} + + + item.country.toString()} + style={styles.countryList} + showsVerticalScrollIndicator={false} + removeClippedSubviews={true} + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={10} + getItemLayout={(data, index) => ({ + length: 69, + offset: 69 * index, + index, + })} + /> + + + + + + + ); +}; + +const styles = StyleSheet.create({ + phoneLoginContainer: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: "80%", + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + }, + phoneLoginHeader: { + flexDirection: "row", + alignItems: "center", + paddingTop: 20, + paddingHorizontal: 16, + paddingBottom: 15, + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + phoneLoginCloseButton: { + padding: 8, + width: 36, + height: 36, + justifyContent: "center", + alignItems: "center", + }, + phoneLoginCloseButtonText: { + fontSize: 18, + color: "#000", + }, + phoneLoginTitle: { + flex: 1, + fontSize: 18, + fontWeight: "600", + color: "#000", + textAlign: "center", + marginRight: 36, + }, + phoneLoginContent: { + padding: 20, + paddingBottom: Platform.OS === "ios" ? 50 : 30, + flex: 1, + }, + phoneInputContainer: { + flexDirection: "row", + alignItems: "center", + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + height: 50, + marginBottom: 20, + position: "relative", + }, + countrySelectRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + height: 50, + marginBottom: 15, + paddingHorizontal: 16, + backgroundColor: "#F7F7F7", + }, + countrySelectContent: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + countryFlag: { + fontSize: 22, + marginRight: 12, + }, + flag: { + width: 24, + height: 24, + marginRight: 16, + }, + countryName: { + fontSize: 16, + color: "#333", + marginRight: 10, + flex: 1, + }, + countryCode: { + fontSize: 15, + color: "#666", + marginRight: 10, + width: 40, + textAlign: "center", + }, + downArrow: { + fontSize: 12, + color: "#666", + }, + countryCodeButton: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 12, + height: "100%", + minWidth: 80, + justifyContent: "space-between", + }, + countryCodeFlag: { + width: 20, + height: 20, + marginRight: 4, + }, + countryCodeText: { + fontSize: 15, + color: "#333", + }, + countryCodeArrow: { + fontSize: 10, + color: "#666", + marginLeft: 2, + }, + phoneInputDivider: { + width: 1, + height: "60%", + backgroundColor: "#E1E1E1", + }, + phoneInput: { + flex: 1, + height: "100%", + paddingLeft: 10, + paddingRight: 36, + fontSize: 16, + }, + phoneClearButton: { + position: "absolute", + right: 12, + top: "50%", + transform: [{ translateY: -12 }], + height: 24, + width: 24, + justifyContent: "center", + alignItems: "center", + }, + phoneClearButtonText: { + fontSize: 16, + color: "#999", + fontWeight: "500", + textAlign: "center", + }, + phoneInfoText: { + fontSize: 14, + color: "#666", + marginBottom: 32, + lineHeight: 20, + }, + phoneContinueButton: { + height: 50, + backgroundColor: "#0039CB", + borderRadius: 25, + justifyContent: "center", + alignItems: "center", + }, + phoneDisabledButton: { + backgroundColor: "#CCCCCC", + }, + phoneContinueButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + // Phone number error + phoneNumberErrorText: { + color: "#FF3B30", + fontSize: 14, + marginTop: -12, + marginBottom: 16, + paddingHorizontal: 5, + }, + // Password styling + passwordInput: { + flex: 1, + height: "100%", + paddingHorizontal: 16, + fontSize: 16, + }, + passwordErrorContainer: { + borderColor: "#FF3B30", + }, + passwordErrorIcon: { + position: "absolute", + right: 12, + top: "50%", + transform: [{ translateY: -12 }], + width: 24, + height: 24, + backgroundColor: "#FF3B30", + borderRadius: 12, + justifyContent: "center", + alignItems: "center", + }, + passwordErrorIconText: { + color: "white", + fontWeight: "bold", + fontSize: 16, + }, + passwordErrorText: { + color: "#FF3B30", + fontSize: 14, + marginTop: -12, + marginBottom: 16, + paddingHorizontal: 5, + }, + forgotPasswordLink: { + alignItems: "center", + marginTop: 5, + }, + forgotPasswordLinkText: { + color: "#0066FF", + fontSize: 14, + }, + // Country modal styles + countryModalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "flex-end", + zIndex: 999, + }, + countryModalOverlay: { + flex: 1, + backgroundColor: "transparent", + }, + countryModalContent: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: "80%", + maxHeight: "80%", + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + }, + modalHandleContainer: { + width: "100%", + alignItems: "center", + paddingTop: 12, + paddingBottom: 8, + }, + modalHandle: { + width: 40, + height: 4, + backgroundColor: "#E0E0E0", + borderRadius: 2, + }, + countryModalHeader: { + flexDirection: "row", + alignItems: "center", + padding: 16, + borderBottomWidth: 1, + borderBottomColor: "#E5E5E5", + }, + countryModalCloseButton: { + padding: 4, + }, + countryModalCloseButtonText: { + fontSize: 18, + color: "#999", + }, + countryModalTitle: { + flex: 1, + fontSize: 18, + fontWeight: "600", + textAlign: "center", + marginRight: 24, + }, + countryList: { + padding: 8, + }, + countryItem: { + flexDirection: "row", + alignItems: "center", + padding: 16, + borderBottomWidth: 1, + borderBottomColor: "#F0F0F0", + }, + countryItemFlag: { + fontSize: 24, + marginRight: 16, + }, + countryItemContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + }, + countryItemName: { + fontSize: 16, + color: "#333", + }, + countryItemCode: { + fontSize: 14, + color: "#666", + marginTop: 4, + }, + modalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "flex-end", + zIndex: 999, + }, + checkmark: { + fontSize: 20, + color: "#0066FF", + fontWeight: "bold", + marginRight: 10, + }, +}); + +export default PhoneLoginModal; diff --git a/app/screens/loginList/index.tsx b/app/screens/loginList/index.tsx new file mode 100644 index 0000000..23826ae --- /dev/null +++ b/app/screens/loginList/index.tsx @@ -0,0 +1,408 @@ +import React, { useEffect, useState, useRef } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + StatusBar, + Platform, + BackHandler, + Image, + Modal +} from "react-native"; +import { useTranslation } from "react-i18next"; +import { useNavigation } from "@react-navigation/native"; +import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; + +import EmailLoginModal from "./EmailLoginModal"; +import PhoneLoginModal from "./PhoneLoginModal"; + +type RootStackParamList = { + Login: undefined; + EmailLogin: undefined; + MainTabs: { screen: string }; + Google: undefined; + Home: { screen: string }; +}; + +type LoginScreenProps = { + onClose?: () => void; + isModal?: boolean; +}; + +export const LoginScreen = ({ onClose, isModal }: LoginScreenProps) => { + const { t } = useTranslation(); + const navigation = + useNavigation>(); + + // 全新的状态管理方式 + const [emailModalVisible, setEmailModalVisible] = useState(false); + const [phoneModalVisible, setPhoneModalVisible] = useState(false); + + // 防止多次触发 + const isProcessingEmail = useRef(false); + const isProcessingPhone = useRef(false); + + // 处理Android返回按钮 + useEffect(() => { + const backAction = () => { + if (emailModalVisible) { + setEmailModalVisible(false); + return true; + } + if (phoneModalVisible) { + setPhoneModalVisible(false); + return true; + } + handleClose(); + return true; + }; + + const backHandler = BackHandler.addEventListener( + "hardwareBackPress", + backAction + ); + + return () => backHandler.remove(); + }, [emailModalVisible, phoneModalVisible]); + + // 关闭主屏幕 + const handleClose = () => { + if (isModal && onClose) { + onClose(); + } else { + navigation.goBack(); + } + }; + + // 处理谷歌登录 + const handleGoogleLogin = async () => { + navigation.navigate("Google"); + }; + + // 处理Facebook登录 + const handleFacebookLogin = () => { + // 处理Facebook登录 + }; + + // 处理Apple登录 + const handleAppleLogin = () => { + // 处理Apple登录 + }; + + // 处理Instagram登录 + const handleInstagramLogin = () => { + // 处理Instagram登录 + }; + + // 显示邮箱登录 + const showEmailModal = () => { + if (isProcessingEmail.current) return; + + isProcessingEmail.current = true; + // 确保手机模态框已关闭 + setPhoneModalVisible(false); + + // 延迟打开邮箱模态框,避免冲突 + setTimeout(() => { + setEmailModalVisible(true); + isProcessingEmail.current = false; + }, 100); + }; + + // 显示手机登录 + const showPhoneModal = () => { + if (isProcessingPhone.current) return; + + isProcessingPhone.current = true; + // 确保邮箱模态框已关闭 + setEmailModalVisible(false); + + // 延迟打开手机模态框,避免冲突 + setTimeout(() => { + setPhoneModalVisible(true); + isProcessingPhone.current = false; + }, 100); + }; + + // 关闭邮箱登录 + const hideEmailModal = () => { + console.log("Hiding email modal"); + setEmailModalVisible(false); + }; + + // 关闭手机登录 + const hidePhoneModal = () => { + console.log("Hiding phone modal"); + setPhoneModalVisible(false); + }; + + // 处理忘记密码 + const handleForgotPassword = () => { + // 处理忘记密码 + }; + + return ( + + + + {/* 顶部蓝色背景区域 */} + + brainnel + + + + 💰 + + {t("wholesalePrice")} + + + + 🚚 + + {t("fastShipping")} + + + + + {/* 登录区域 */} + + + × + + + + {t("loginSubtitle")} + + + {/* 登录按钮 */} + + + + + {t("continueWithGoogle")} + + + + + f + + + {t("continueWithFacebook")} + + + + {Platform.OS === "ios" && ( + + + 🍎 + + {t("continueWithApple")} + + )} + + + + 📷 + + + {t("continueWithInstagram")} + + + + + + ✉️ + + {t("continueWithEmail")} + + + + + 📱 + + {t("continueWithPhone")} + + + {/* 忘记密码 */} + + {t("forgotPassword")} + + + {/* 服务条款 */} + + + {t("termsText")} {t("termsOfUse")} + + + {t("and")} {t("privacyPolicy")} + + + + + {/* 邮箱登录模态框 - 直接渲染 */} + + + {/* 手机登录模态框 - 直接渲染 */} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#fff", + }, + closeButton: { + position: "absolute", + top: 15, + left: 10, + width: 24, + height: 24, + justifyContent: "center", + alignItems: "center", + zIndex: 1, + }, + closeButtonText: { + color: "#000", + fontSize: 24, + fontWeight: "300", + }, + blueHeader: { + backgroundColor: "#0066FF", + paddingHorizontal: 20, + paddingBottom: 20, + paddingTop: Platform.OS === "ios" ? 60 : 40, + borderBottomLeftRadius: 24, + borderBottomRightRadius: 24, + }, + logo: { + fontSize: 28, + fontWeight: "bold", + color: "#fff", + marginBottom: 15, + }, + features: { + flexDirection: "row", + gap: 16, + }, + featureItem: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + featureIconContainer: { + backgroundColor: "rgba(255, 255, 255, 0.2)", + borderRadius: 8, + width: 24, + height: 24, + justifyContent: "center", + alignItems: "center", + }, + featureIcon: { + fontSize: 12, + }, + featureText: { + fontSize: 14, + color: "#fff", + }, + loginContainer: { + flex: 1, + paddingHorizontal: 20, + paddingTop: Platform.OS === "ios" ? 40 : 20, + }, + titleContainer: { + alignItems: "center", + marginBottom: 30, + paddingTop: 20, + position: "relative", + }, + subtitle: { + fontSize: 14, + color: "#666", + textAlign: "center", + }, + loginButton: { + flexDirection: "row", + height: 50, + borderRadius: 25, + borderWidth: 1, + borderColor: "#E1E1E1", + alignItems: "center", + marginBottom: 12, + paddingHorizontal: 16, + backgroundColor: "#fff", + }, + loginButtonIcon: { + width: 24, + height: 24, + borderRadius: 12, + justifyContent: "center", + alignItems: "center", + marginRight: 16, + }, + facebookIcon: { + backgroundColor: "#3b5998", + }, + appleIconBg: { + backgroundColor: "#000", + }, + instagramIcon: { + backgroundColor: "#E1306C", + }, + loginButtonText: { + flex: 1, + fontSize: 16, + color: "#000", + textAlign: "center", + marginRight: 16, + }, + forgotPassword: { + alignItems: "center", + marginVertical: 20, + }, + forgotPasswordText: { + color: "#0066FF", + fontSize: 14, + }, + termsContainer: { + alignItems: "center", + marginTop: 10, + }, + terms: { + fontSize: 12, + color: "#666", + textAlign: "center", + lineHeight: 18, + }, + link: { + color: "#0066FF", + }, +}); + +export default LoginScreen; \ No newline at end of file diff --git a/app/screens/pay/Pay.tsx b/app/screens/pay/Pay.tsx index 24b5789..8c88280 100644 --- a/app/screens/pay/Pay.tsx +++ b/app/screens/pay/Pay.tsx @@ -5,37 +5,28 @@ import { payApi, PaymentInfoResponse } from '../../services/api/payApi'; import { WebView } from "react-native-webview"; type PayScreenRouteProp = RouteProp<{ - Pay: { order_id: string }; + Pay: { payUrl: string }; }, 'Pay'>; export const Pay = () => { const [loading, setLoading] = useState(true); const route = useRoute(); - const {order_id} = route.params; + const {payUrl} = route.params; const [payInfo, setPayInfo] = useState(); - const getPayInfo = async () => { - const data = { - order_id: order_id, - method: 'paypal', - amount: '100', - currency: 'USD' - } - const res = await payApi.getPayInfo(data); - console.log('res',res); - - setPayInfo(res); - } + useEffect(() => { + console.log(route.params); + + console.log(payUrl); - getPayInfo(); },[]) const handleNavigationStateChange = (navState: any) => { console.log(navState); } return - {payInfo?.payment_url ? ( + {payUrl ? ( setLoading(true)} diff --git a/app/screens/previewOrder/PaymentMethod.tsx b/app/screens/previewOrder/PaymentMethod.tsx index 09cd816..b8c62db 100644 --- a/app/screens/previewOrder/PaymentMethod.tsx +++ b/app/screens/previewOrder/PaymentMethod.tsx @@ -21,7 +21,7 @@ import useUserStore from "../../store/user"; import { createOrderDataType } from "../../types/createOrder"; import { ordersApi, - OrderData, CreateOrderRequest, + OrderData, CreateOrderRequest, Order, } from "../../services/api/orders"; @@ -32,7 +32,7 @@ type PaymentMethodRouteParams = { // Define the root navigation params type RootStackParamList = { - PreviewOrder: undefined; + PreviewOrder: {data:Order,payMethod:string,currency:string,amount:number}; Pay: { order_id: string }; ShippingFee: { freight_forwarder_address_id?: number }; PaymentMethod: { freight_forwarder_address_id?: number }; @@ -202,10 +202,11 @@ export const PaymentMethod = () => { const [loading, setLoading] = useState(false); const { user } = useUserStore(); const [createOrderData, setCreateOrderData] = useState(); - const { items, orderData,setOrderData } = useCreateOrderStore(); + const { items, orderData,setOrderData,resetOrder } = useCreateOrderStore(); const [selectedCurrency, setSelectedCurrency] = useState("USD"); const [convertedAmount, setConvertedAmount] = useState(0); const [isConverting, setIsConverting] = useState(false); + const [createLoading, setCreateLoading] = useState(false); const [exchangeRates] = useState({ usd: 580.00, eur: 655.96 @@ -374,9 +375,21 @@ export const PaymentMethod = () => { } setOrderData(createOrderData || {}); - const res = await ordersApi.createOrder(createOrderData as CreateOrderRequest) - console.log(res) - navigation.navigate("PreviewOrder"); + setCreateLoading(true) + try { + const res = await ordersApi.createOrder(createOrderData as CreateOrderRequest) + setCreateLoading(false) + navigation.navigate("PreviewOrder",{data:res,payMethod:selectedPayment,currency:selectedCurrency,amount:convertedAmount}); + resetOrder() + }catch(e) { + setCreateLoading(false) + Alert.alert('Error', 'Failed to get preview order'); + }finally { + setCreateLoading(false) + } + + + }; return ( @@ -646,13 +659,13 @@ export const PaymentMethod = () => { - 创建订单 + {createLoading ? '创建中...' : '创建订单'} diff --git a/app/screens/previewOrder/PreviewAddress.tsx b/app/screens/previewOrder/PreviewAddress.tsx index 8c44703..8b0fef5 100644 --- a/app/screens/previewOrder/PreviewAddress.tsx +++ b/app/screens/previewOrder/PreviewAddress.tsx @@ -8,15 +8,13 @@ import { StyleSheet, KeyboardAvoidingView, Platform, - Switch, ActivityIndicator, Modal, FlatList, Dimensions, } from "react-native"; import { useState } from "react"; -import DropDownPicker from "react-native-dropdown-picker"; -import { Country, countries } from "../../constants/countries"; + import AsyncStorage from "@react-native-async-storage/async-storage"; import BackIcon from "../../components/BackIcon"; import { useNavigation, useRoute, RouteProp } from "@react-navigation/native"; diff --git a/app/screens/setting/SettingList.tsx b/app/screens/setting/SettingList.tsx index baa89a4..873c042 100644 --- a/app/screens/setting/SettingList.tsx +++ b/app/screens/setting/SettingList.tsx @@ -8,6 +8,7 @@ import { useState, useEffect } from "react"; import { settingApi, MySetting } from "../../services/api/setting"; import { RootStackParamList } from "../../navigation/types"; import { eventBus } from "../../utils/eventBus"; +import AsyncStorage from '@react-native-async-storage/async-storage'; export const SettingList = () => { const [mySetting, setMySetting] = useState(); @@ -93,6 +94,17 @@ export const SettingList = () => { + + { + AsyncStorage.clear(); + navigation.navigate("CountrySelect"); + }}> + 清理缓存 + + + + + { diff --git a/app/services/api/orders.ts b/app/services/api/orders.ts index 6ada4c6..624613c 100644 --- a/app/services/api/orders.ts +++ b/app/services/api/orders.ts @@ -197,17 +197,21 @@ export interface Address { export interface Order { user_id: number; total_amount: number; + currency:string, actual_amount: number; discount_amount: number; shipping_fee: number; address_id: number; + domestic_shipping_fee:number; receiver_name: string; receiver_phone: string; receiver_address: string; + whatsapp_number: number; buyer_message: string; pay_status: number; order_status: number; shipping_status: number; + receiver_country:string order_id: number; payment_method: string; order_no: string; diff --git a/app/services/api/payApi.ts b/app/services/api/payApi.ts index eedcad9..5ad7be6 100644 --- a/app/services/api/payApi.ts +++ b/app/services/api/payApi.ts @@ -24,9 +24,9 @@ export interface PaymentInfoResponse { msg: string; } export interface PayInfoBody { - order_id: string; + order_id: number; method: string; - amount: string; + amount: number; currency: string; } diff --git a/app/services/api/setting.ts b/app/services/api/setting.ts index 61dfaeb..2f8583e 100644 --- a/app/services/api/setting.ts +++ b/app/services/api/setting.ts @@ -1,4 +1,5 @@ import apiService from './apiClient'; +import {CountryList} from '../../constants/countries' export interface Country { country: number; @@ -43,10 +44,19 @@ export interface FirstLogin { export const settingApi = { - getCountryList: () => apiService.get('/api/user_settings/countries/'), + // 获取国家 + getCountryList: () => apiService.get('/api/user_settings/countries/'), + // 获取货币 getCurrencyList: () => apiService.get('/api/user_settings/currencies/'), + // 获取语言 getLanguageList: () => apiService.get('/api/user_settings/languages/'), + // 我的设置 getMySetting: () => apiService.get('/api/user_settings/me/'), + // 首次登录 postFirstLogin: (country: number) => apiService.post(`/api/user_settings/first_login/?country=${country}`), + // 修改设置 putSetting: (setting: object) => apiService.put('/api/user_settings/me/', setting), + + // 获取发送短信的国家列表 + getSendSmsCountryList: () => apiService.get('/api/user_settings/phone_config/'), } \ No newline at end of file