Browse Source

手机号校验

main
Your Name 3 weeks ago
parent
commit
0fbe08ecb1
  1. 13
      App.tsx
  2. 12
      app/constants/countries.ts
  3. 74
      app/screens/CountrySelect.tsx
  4. 1
      app/screens/HomeScreen.tsx
  5. 1359
      app/screens/LoginScreen.tsx
  6. 12
      app/screens/ProfileScreen.tsx
  7. 521
      app/screens/loginList/EmailLoginModal.tsx
  8. 7
      app/screens/loginList/ForgotPassword.tsx
  9. 798
      app/screens/loginList/PhoneLoginModal.tsx
  10. 408
      app/screens/loginList/index.tsx
  11. 25
      app/screens/pay/Pay.tsx
  12. 33
      app/screens/previewOrder/PaymentMethod.tsx
  13. 4
      app/screens/previewOrder/PreviewAddress.tsx
  14. 12
      app/screens/setting/SettingList.tsx
  15. 4
      app/services/api/orders.ts
  16. 4
      app/services/api/payApi.ts
  17. 12
      app/services/api/setting.ts

13
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<RootStackParamList>();
@ -438,6 +440,15 @@ function AppContent() {
gestureDirection: "horizontal",
}}
/>
<Stack.Screen
name="ForgotPassword"
component={ForgotPassword}
options={{
animation: "slide_from_right",
gestureEnabled: true,
gestureDirection: "horizontal",
}}
/>
</Stack.Navigator>
<Toast />
</NavigationContainer>

12
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',

74
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<string>('');
const [selectedCountry, setSelectedCountry] = useState<number>();
const [countryList, setCountryList] = useState<CountryList[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
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 }) => (
<TouchableOpacity
style={[
styles.countryItem,
selectedCountry === item.code && styles.selectedItem
selectedCountry === item.country && styles.selectedItem
]}
onPress={() => handleCountrySelect(item)}
>
<Text style={styles.flag}>{item.flag}</Text>
<Text style={styles.countryCode}>+{item.country}</Text>
<Image source={flagMap.get(item.name_en)} style={styles.flag} />
<Text style={[
styles.countryName,
selectedCountry === item.code && styles.selectedText
]}>{item.name}</Text>
{selectedCountry === item.code && (
selectedCountry === item.country && styles.selectedText
]}>{item.name_en}</Text>
{selectedCountry === item.country && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
@ -86,13 +106,20 @@ export const CountrySelect = () => {
<Text style={styles.title}>{t('selectCountry')}</Text>
<Text style={styles.subtitle}>{t('subtitle')}</Text>
</View>
<FlatList
data={countries}
renderItem={renderCountryItem}
keyExtractor={(item) => item.code}
style={styles.list}
showsVerticalScrollIndicator={false}
/>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
) : (
<FlatList
data={countryList}
renderItem={renderCountryItem}
keyExtractor={(item) => item.country.toString()}
style={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</SafeAreaView>
</View>
);
@ -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',
},
});

1
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,

1359
app/screens/LoginScreen.tsx

File diff suppressed because it is too large Load Diff

12
app/screens/ProfileScreen.tsx

@ -144,6 +144,18 @@ export const ProfileScreen = () => {
style={styles.timecardWidget}
resizeMode="stretch"
>
<View style={styles.flexRowWithContent}>
<View style={styles.financialInfoContainer}>
<TouchableOpacity
onPress={() => navigation.navigate("SettingList")}
>
<View style={styles.svgContainer1}>
<SettingsIcon size={fontSize(24)} color="white" />
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.notLoggedInContainer}>
<View style={styles.profileImageCircle}>
<Image

521
app/screens/loginList/EmailLoginModal.tsx

@ -0,0 +1,521 @@
import React, { useState, useRef } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
FlatList,
Keyboard,
Alert,
Platform,
Modal
} 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 type { NativeStackNavigationProp } from "@react-navigation/native-stack";
// Common email domains list
const EMAIL_DOMAINS = [
"gmail.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"icloud.com",
"mail.com",
"protonmail.com",
"qq.com",
"163.com",
"126.com",
];
type RootStackParamList = {
Login: undefined;
EmailLogin: undefined;
MainTabs: { screen: string };
Google: undefined;
Home: { screen: string };
};
type EmailLoginModalProps = {
visible: boolean;
onClose: () => void;
};
const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => {
const { t } = useTranslation();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
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<string[]>([]);
// 防止重复关闭
const isClosing = useRef(false);
// 引用输入框
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(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 }) => (
<TouchableOpacity
style={styles.suggestionItem}
onPress={() => handleSelectSuggestion(item)}
>
<Text style={styles.suggestionText}>{item}</Text>
</TouchableOpacity>
);
// 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 (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={() => onClose()}
statusBarTranslucent={true}
>
<View style={styles.modalContainer}>
<View style={styles.emailLoginContainer}>
<View style={styles.emailLoginHeader}>
<TouchableOpacity
style={styles.emailLoginCloseButton}
onPress={() => onClose()}
activeOpacity={0.7}
>
<Text style={styles.emailLoginCloseButtonText}></Text>
</TouchableOpacity>
<Text style={styles.emailLoginTitle}>{t("logInOrSignUp")}</Text>
</View>
<View style={styles.emailLoginContent}>
<View style={styles.emailInputContainer}>
<TextInput
ref={emailInputRef}
style={styles.emailInput}
placeholder={t("pleaseEnterEmail")}
value={email}
onChangeText={(text) => {
handleEmailChange(text);
setEmailPasswordError(false);
}}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoFocus
maxLength={50}
/>
{email.length > 0 ? (
<TouchableOpacity
style={styles.emailClearButton}
onPress={() => {
setEmail("");
setShowSuggestions(false);
setEmailPasswordError(false);
}}
activeOpacity={0.7}
>
<Text style={styles.emailClearButtonText}></Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.emailClearButton}
onPress={focusEmailInput}
activeOpacity={0.7}
>
<Text style={{fontSize: 16, color: '#0066FF'}}></Text>
</TouchableOpacity>
)}
</View>
{/* Email suffix suggestion list */}
{showSuggestions && (
<View style={styles.suggestionsContainer}>
<FlatList
data={suggestions.slice(0, 5)} // Limit to showing 5 suggestions max
renderItem={renderSuggestionItem}
keyExtractor={(item) => item}
style={styles.suggestionsList}
keyboardShouldPersistTaps="handled"
removeClippedSubviews={true}
initialNumToRender={5}
maxToRenderPerBatch={5}
windowSize={5}
getItemLayout={(data, index) => ({
length: 44,
offset: 44 * index,
index,
})}
/>
</View>
)}
{/* Password input */}
<View
style={[
styles.passwordContainer,
emailPasswordError && styles.passwordErrorContainer,
]}
>
<TextInput
ref={passwordInputRef}
style={styles.passwordInput}
placeholder={t("enterPassword")}
value={emailPassword}
onChangeText={(text) => {
setEmailPassword(text);
setEmailPasswordError(false);
}}
secureTextEntry={true}
autoCapitalize="none"
/>
{emailPasswordError ? (
<View style={styles.passwordErrorIcon}>
<Text style={styles.passwordErrorIconText}>!</Text>
</View>
) : (
<TouchableOpacity
style={styles.emailClearButton}
onPress={focusPasswordInput}
activeOpacity={0.7}
>
<Text style={{fontSize: 16, color: '#0066FF'}}></Text>
</TouchableOpacity>
)}
</View>
{/* Password error message */}
{emailPasswordError && (
<>
<Text style={styles.passwordErrorText}>
{t("passwordIncorrect")}
</Text>
<TouchableOpacity
style={styles.forgotPasswordLink}
onPress={handleForgotPassword}
activeOpacity={0.7}
>
<Text style={styles.forgotPasswordLinkText}>
{t("forgotPassword")}
</Text>
</TouchableOpacity>
</>
)}
<TouchableOpacity
style={[
styles.emailContinueButton,
(!isValidEmail(email) || !emailPassword) &&
styles.emailDisabledButton,
]}
onPress={handleEmailContinue}
disabled={!isValidEmail(email) || !emailPassword}
activeOpacity={0.7}
>
<Text style={styles.emailContinueButtonText}>
{t("continue")}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
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;

7
app/screens/loginList/ForgotPassword.tsx

@ -0,0 +1,7 @@
import { View, Text } from "react-native";
export const ForgotPassword = () => {
return <View>
<Text></Text>
</View>
}

798
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<NativeStackNavigationProp<RootStackParamList>>();
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<CountryList[]>([]);
const [selectedCountry, setSelectedCountry] = useState<CountryList>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 }) => (
<TouchableOpacity
style={styles.countryItem}
onPress={() => handleCountrySelect(item)}
activeOpacity={0.7}
>
<View style={styles.countryItemContent}>
<Text style={styles.countryCode}>+{item.country}</Text>
{/* <Image source={flagMap.get(item.name_en)} style={styles.flag} /> */}
<Text style={[styles.countryName]}>{item.name_en}</Text>
</View>
{/* Add checkmark for selected country */}
{selectedCountry && selectedCountry.country === item.country && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
),
[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<TextInput>(null);
// 主动弹出键盘
const focusPhoneInput = () => {
if (phoneInputRef.current) {
phoneInputRef.current.focus();
}
};
React.useEffect(() => {
if (visible) {
// 当模态框显示时,等待动画完成后主动弹出键盘
const timer = setTimeout(() => {
focusPhoneInput();
}, 300);
return () => clearTimeout(timer);
}
}, [visible]);
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={() => onClose()}
statusBarTranslucent={true}
>
<View style={styles.modalContainer}>
<View style={styles.phoneLoginContainer}>
<View style={styles.phoneLoginHeader}>
<TouchableOpacity
style={styles.phoneLoginCloseButton}
onPress={() => onClose()}
activeOpacity={0.7}
>
<Text style={styles.phoneLoginCloseButtonText}></Text>
</TouchableOpacity>
<Text style={styles.phoneLoginTitle}>{t("logInOrSignUp")}</Text>
</View>
<View style={styles.phoneLoginContent}>
{/* Country Selector Row - Now removed as we integrated it into the phone input */}
<View style={styles.phoneInputContainer}>
<TouchableOpacity
style={styles.countryCodeButton}
onPress={() => setShowCountryModal(true)}
>
{/* {selectedCountry?.name_en && (
<Image
source={flagMap.get(selectedCountry.name_en)}
style={styles.countryCodeFlag}
/>
)} */}
<Text style={styles.countryCodeText}>
+{selectedCountry?.country}
</Text>
<Text style={styles.countryCodeArrow}></Text>
</TouchableOpacity>
<View style={styles.phoneInputDivider} />
<TextInput
ref={phoneInputRef}
style={styles.phoneInput}
placeholder={t("phoneNumber")}
value={phoneNumber}
onChangeText={handlePhoneNumberChange}
keyboardType="phone-pad"
autoFocus
maxLength={15}
/>
{phoneNumber.length > 0 ? (
<TouchableOpacity
style={styles.phoneClearButton}
onPress={() => {
setPhoneNumber("");
setPhoneNumberError(false);
setPasswordError(false);
}}
activeOpacity={0.7}
>
<Text style={styles.phoneClearButtonText}></Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.phoneClearButton}
onPress={focusPhoneInput}
activeOpacity={0.7}
>
<Text style={{fontSize: 16, color: '#0066FF'}}></Text>
</TouchableOpacity>
)}
</View>
{/* Phone number error message */}
{phoneNumberError && (
<Text style={styles.phoneNumberErrorText}>
{t("invalidPhoneNumber")}
{selectedCountry?.valid_digits &&
`(${t("requiresDigits")}: ${selectedCountry.valid_digits.join(', ')})`}
</Text>
)}
{/* Password input */}
<View
style={[
styles.phoneInputContainer,
passwordError && styles.passwordErrorContainer,
]}
>
<TextInput
style={styles.passwordInput}
placeholder={t("enterPassword")}
value={password}
onChangeText={(text) => {
setPassword(text);
setPasswordError(false);
}}
secureTextEntry={true}
autoCapitalize="none"
/>
{passwordError && (
<View style={styles.passwordErrorIcon}>
<Text style={styles.passwordErrorIconText}>!</Text>
</View>
)}
</View>
{/* Password error message */}
{passwordError && (
<>
<Text style={styles.passwordErrorText}>
{error}
</Text>
<TouchableOpacity
style={styles.forgotPasswordLink}
onPress={handleForgotPassword}
activeOpacity={0.7}
>
<Text style={styles.forgotPasswordLinkText}>
{t("forgotPassword")}
</Text>
</TouchableOpacity>
</>
)}
<Text
style={[styles.phoneInfoText, passwordError && { marginTop: 5 }]}
></Text>
<TouchableOpacity
style={[
styles.phoneContinueButton,
(phoneNumberError || !phoneNumber.trim() || !password) &&
styles.phoneDisabledButton,
]}
onPress={handlePhoneContinue}
disabled={phoneNumberError || !phoneNumber.trim() || !password}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.phoneContinueButtonText}>
{t("continue")}
</Text>
)}
</TouchableOpacity>
</View>
{/* Country selection modal */}
<Modal
visible={showCountryModal}
animationType="slide"
transparent={true}
onRequestClose={() => setShowCountryModal(false)}
hardwareAccelerated={true}
statusBarTranslucent={true}
presentationStyle="overFullScreen"
>
<View style={styles.countryModalContainer}>
<TouchableOpacity
style={styles.countryModalOverlay}
activeOpacity={1}
onPress={() => setShowCountryModal(false)}
/>
<View style={styles.countryModalContent}>
<View style={styles.modalHandleContainer}>
<View style={styles.modalHandle} />
</View>
<View style={styles.countryModalHeader}>
<TouchableOpacity
style={styles.countryModalCloseButton}
onPress={() => setShowCountryModal(false)}
activeOpacity={0.7}
>
<Text style={styles.countryModalCloseButtonText}></Text>
</TouchableOpacity>
<Text style={styles.countryModalTitle}>
{t("selectCountry")}
</Text>
</View>
<FlatList
data={countryList}
renderItem={renderCountryItem}
keyExtractor={(item) => 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,
})}
/>
</View>
</View>
</Modal>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create<Styles>({
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;

408
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<NativeStackNavigationProp<RootStackParamList>>();
// 全新的状态管理方式
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 (
<View style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="#0066FF" />
{/* 顶部蓝色背景区域 */}
<View style={styles.blueHeader}>
<Text style={styles.logo}>brainnel</Text>
<View style={styles.features}>
<View style={styles.featureItem}>
<View style={styles.featureIconContainer}>
<Text style={styles.featureIcon}>💰</Text>
</View>
<Text style={styles.featureText}>{t("wholesalePrice")}</Text>
</View>
<View style={styles.featureItem}>
<View style={styles.featureIconContainer}>
<Text style={styles.featureIcon}>🚚</Text>
</View>
<Text style={styles.featureText}>{t("fastShipping")}</Text>
</View>
</View>
</View>
{/* 登录区域 */}
<View style={styles.loginContainer}>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Text style={styles.closeButtonText}>×</Text>
</TouchableOpacity>
<View style={styles.titleContainer}>
<Text style={styles.subtitle}>{t("loginSubtitle")}</Text>
</View>
{/* 登录按钮 */}
<TouchableOpacity
style={styles.loginButton}
onPress={handleGoogleLogin}
>
<View style={styles.loginButtonIcon}>
<Image
source={require("../../../assets/img/google.png")}
style={{ width: 20, height: 20 }}
/>
</View>
<Text style={styles.loginButtonText}>{t("continueWithGoogle")}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.loginButton}
onPress={handleFacebookLogin}
>
<View style={[styles.loginButtonIcon, styles.facebookIcon]}>
<Text style={{ color: "#fff" }}>f</Text>
</View>
<Text style={styles.loginButtonText}>
{t("continueWithFacebook")}
</Text>
</TouchableOpacity>
{Platform.OS === "ios" && (
<TouchableOpacity
style={styles.loginButton}
onPress={handleAppleLogin}
>
<View style={[styles.loginButtonIcon, styles.appleIconBg]}>
<Text>🍎</Text>
</View>
<Text style={styles.loginButtonText}>{t("continueWithApple")}</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.loginButton}
onPress={handleInstagramLogin}
>
<View style={[styles.loginButtonIcon, styles.instagramIcon]}>
<Text>📷</Text>
</View>
<Text style={styles.loginButtonText}>
{t("continueWithInstagram")}
</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.loginButton} onPress={showEmailModal}>
<View style={styles.loginButtonIcon}>
<Text></Text>
</View>
<Text style={styles.loginButtonText}>{t("continueWithEmail")}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.loginButton} onPress={showPhoneModal}>
<View style={styles.loginButtonIcon}>
<Text>📱</Text>
</View>
<Text style={styles.loginButtonText}>{t("continueWithPhone")}</Text>
</TouchableOpacity>
{/* 忘记密码 */}
<TouchableOpacity
style={styles.forgotPassword}
onPress={handleForgotPassword}
>
<Text style={styles.forgotPasswordText}>{t("forgotPassword")}</Text>
</TouchableOpacity>
{/* 服务条款 */}
<View style={styles.termsContainer}>
<Text style={styles.terms}>
{t("termsText")} <Text style={styles.link}>{t("termsOfUse")}</Text>
</Text>
<Text style={styles.terms}>
{t("and")} <Text style={styles.link}>{t("privacyPolicy")}</Text>
</Text>
</View>
</View>
{/* 邮箱登录模态框 - 直接渲染 */}
<EmailLoginModal
visible={emailModalVisible}
onClose={hideEmailModal}
/>
{/* 手机登录模态框 - 直接渲染 */}
<PhoneLoginModal
visible={phoneModalVisible}
onClose={hidePhoneModal}
/>
</View>
);
};
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;

25
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<PayScreenRouteProp>();
const {order_id} = route.params;
const {payUrl} = route.params;
const [payInfo, setPayInfo] = useState<PaymentInfoResponse>();
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 <View style={{ flex: 1 }}>
{payInfo?.payment_url ? (
{payUrl ? (
<WebView
source={{ uri: payInfo.payment_url }}
source={{ uri: payUrl }}
style={styles.webview}
onNavigationStateChange={handleNavigationStateChange}
onLoadStart={() => setLoading(true)}

33
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<createOrderDataType>();
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 = () => {
<TouchableOpacity
style={[
styles.primaryButtonStyle,
// (isDomesticShippingLoading || domesticShippingFeeData?.total_shipping_fee == null)
// ? styles.disabledButtonStyle : {}
(createLoading)
? styles.disabledButtonStyle : {}
]}
onPress={handleSubmit}
// disabled={isDomesticShippingLoading || domesticShippingFeeData?.total_shipping_fee == null}
disabled={createLoading}
>
<Text style={styles.buttonText}></Text>
<Text style={styles.buttonText}>{createLoading ? '创建中...' : '创建订单'}</Text>
</TouchableOpacity>
</View>
</ScrollView>

4
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";

12
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<MySetting>();
@ -93,6 +94,17 @@ export const SettingList = () => {
</Text>
</View>
<TouchableOpacity style={styles.item} onPress={() => {
AsyncStorage.clear();
navigation.navigate("CountrySelect");
}}>
<Text></Text>
<Text>
<LeftArrowIcon size={fontSize(20)} color="#acacac" />
</Text>
</TouchableOpacity>
<View>
<TouchableOpacity
onPress={() => {

4
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;

4
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;
}

12
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<Country[]>('/api/user_settings/countries/'),
// 获取国家
getCountryList: () => apiService.get<CountryList[]>('/api/user_settings/countries/'),
// 获取货币
getCurrencyList: () => apiService.get<string[]>('/api/user_settings/currencies/'),
// 获取语言
getLanguageList: () => apiService.get<string[]>('/api/user_settings/languages/'),
// 我的设置
getMySetting: () => apiService.get<MySetting>('/api/user_settings/me/'),
// 首次登录
postFirstLogin: (country: number) => apiService.post<FirstLogin>(`/api/user_settings/first_login/?country=${country}`),
// 修改设置
putSetting: (setting: object) => apiService.put<MySetting>('/api/user_settings/me/', setting),
// 获取发送短信的国家列表
getSendSmsCountryList: () => apiService.get<CountryList[]>('/api/user_settings/phone_config/'),
}
Loading…
Cancel
Save