Quellcode durchsuchen

implemented KeyboardAwareScrollView and made QOL changes

Ian Fung vor 1 Jahr
Ursprung
Commit
6c904bdf06

+ 1 - 1
component/global/PaginationIndicator.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { View, StyleSheet } from "react-native";
-``;
+
 const Circle = ({ isActive }: { isActive: boolean }) => {
 	return <View style={isActive ? styles.activeCircle : styles.circle} />;
 };

+ 4 - 2
component/global/date_input.tsx

@@ -5,8 +5,9 @@ import DatePicker from "react-native-modern-datepicker";
 
 type DateModalProps = {
 	onDateChange: (date: string) => void;
+	placeholder: string;
 };
-const DateModal: React.FC<DateModalProps> = ({ onDateChange }) => {
+const DateModal: React.FC<DateModalProps> = ({ onDateChange, placeholder }) => {
 	const [open, setOpen] = useState(false);
 	const [date, setDate] = useState("");
 
@@ -27,7 +28,8 @@ const DateModal: React.FC<DateModalProps> = ({ onDateChange }) => {
 	return (
 		<View style={styles.container}>
 			<Pressable onPress={openOrCloseModal} style={[styles.inputContainer]}>
-				<Text style={styles.placeholder}>{date ? date : "DD/MM/YY"}</Text>
+				{/* <Text style={styles.placeholder}>{date ? date : "DD/MM/YY"}</Text> */}
+				<Text style={styles.placeholder}>{placeholder}</Text>
 			</Pressable>
 			<Modal animationType="slide" transparent={true} visible={open}>
 				<View style={styles.centeredView}>

+ 3 - 1
component/global/normal_button.tsx

@@ -5,12 +5,14 @@ interface NormalButtonProps {
 	title: ReactElement;
 	extendedStyle?: StyleProp<ViewStyle>;
 	onPress: () => void;
+	disabled?: boolean;
 }
 
-const NormalButton: React.FC<NormalButtonProps> = ({ title, extendedStyle, onPress }) => {
+const NormalButton: React.FC<NormalButtonProps> = ({ title, extendedStyle, onPress, disabled }) => {
 	return (
 		<Pressable
 			onPress={onPress}
+			disabled={disabled}
 			style={({ pressed }) => [styles.button, pressed ? styles.buttonPressed : null, extendedStyle]}
 		>
 			{title}

+ 10 - 3
component/global/number_input.tsx

@@ -5,19 +5,20 @@ interface NumberInputProps {
 	placeholder: string;
 	extendedStyle?: StyleProp<ViewStyle>;
 	onChangeText: (text: string) => void;
+	editable?: boolean;
 }
 
-const NumberInput: React.FC<NumberInputProps> = ({ placeholder, extendedStyle, onChangeText }) => {
+const NumberInput: React.FC<NumberInputProps> = ({ placeholder, extendedStyle, onChangeText, editable }) => {
 	const [inputValue, setInputValue] = useState<string>("");
 	const [error, setError] = useState("");
 
 	//if users input non-number, show error message
+
 	const validateInput = (input: string) => {
 		const regex = /^[0-9\b]+$/;
 		if (!input.trim()) {
 			setError("");
 			setInputValue(input);
-
 			onChangeText(input);
 			return;
 		}
@@ -39,6 +40,7 @@ const NumberInput: React.FC<NumberInputProps> = ({ placeholder, extendedStyle, o
 					style={[styles.input]}
 					onChangeText={(text) => validateInput(text)}
 					value={inputValue}
+					editable={editable}
 				/>
 			</View>
 			{error && <Text style={styles.errorMessage}>{error}</Text>}
@@ -47,7 +49,12 @@ const NumberInput: React.FC<NumberInputProps> = ({ placeholder, extendedStyle, o
 };
 
 const styles = StyleSheet.create({
-	mainContainer: { width: "100%", maxWidth: "100%", height: "100%", maxHeight: "100%" },
+	mainContainer: {
+		width: "100%",
+		maxWidth: "100%",
+		height: "100%",
+		maxHeight: "100%",
+	},
 	inputContainer: {
 		flexDirection: "row",
 		alignItems: "center",

+ 3 - 1
component/global/phone_input.tsx

@@ -5,9 +5,10 @@ interface PhoneInputProps {
 	placeholder: string;
 	extendedStyle?: StyleProp<ViewStyle>;
 	handleFormDataChange?: HandleFormDataChange;
+	editable?: boolean;
 }
 
-const PhoneInput: React.FC<PhoneInputProps> = ({ placeholder, extendedStyle, handleFormDataChange }) => {
+const PhoneInput: React.FC<PhoneInputProps> = ({ placeholder, extendedStyle, handleFormDataChange, editable }) => {
 	const [error, setError] = useState("");
 	const handleTextChange = (text: string) => {
 		if (text.length >= 8) {
@@ -32,6 +33,7 @@ const PhoneInput: React.FC<PhoneInputProps> = ({ placeholder, extendedStyle, han
 					placeholder={placeholder}
 					style={[styles.input]}
 					placeholderTextColor="#888888"
+					editable={editable}
 				/>
 			</View>
 			{error && <Text style={styles.errorMessage}>{error}</Text>}

+ 1 - 2
component/global/select_button.tsx

@@ -10,7 +10,7 @@ interface SingleSelectButtonGroupProps {
 	onSelectionChange: (value: string) => void;
 	extendedStyle?: StyleProp<ViewStyle>;
 	shouldShowRedOutline: boolean;
-	selectedOption: string | null;
+	selectedOption: string | null | undefined;
 }
 
 const SingleSelectButtonGroup: React.FC<SingleSelectButtonGroupProps> = ({
@@ -45,7 +45,6 @@ const styles = StyleSheet.create({
 	button: {
 		maxWidth: "100%",
 		padding: 20,
-
 		marginBottom: 10,
 		borderWidth: 1,
 		justifyContent: "space-between",

+ 37 - 21
component/multiStepForm/formComponent/form.tsx

@@ -1,5 +1,5 @@
 import { useState } from "react";
-import { View, Text, StyleSheet, Pressable } from "react-native";
+import { View, Text, StyleSheet, Pressable, TouchableWithoutFeedback, Keyboard } from "react-native";
 import Verification from "./formPages/verification";
 import BasicInformation from "./formPages/basicInformation";
 import UberDriver from "./formPages/uberDriver";
@@ -9,6 +9,7 @@ import { FormData, FormDataKey } from "../../type";
 import CreateWallet from "./formPages/createWallet";
 import FinishSignUp from "./formPages/finishSignUp";
 import LoginPage from "./formPages/loginPage";
+import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
 
 type FormProps = {
 	formData: FormData;
@@ -82,37 +83,52 @@ const Form: React.FC<FormProps> = ({ formData, setFormData }) => {
 				return <></>;
 		}
 	};
+
 	const goToPreviousPage = () => {
-		if (screen > 0) {
-			setScreen(screen - 1);
-		}
+		setScreen((prevState) => {
+			if (prevState > 0) {
+				return prevState - 1;
+			} else {
+				return prevState;
+			}
+		});
 	};
+
 	const goToNextPage = () => {
-		if (screen < 6) {
-			setScreen(screen + 1);
-		}
+		setScreen((prevState) => {
+			if (prevState < 6) {
+				return prevState + 1;
+			} else {
+				return prevState;
+			}
+		});
 	};
 
 	return (
-		//not showing and title pagination on the first and last page
 		<>
+			{/* not showing title and pagination on the first and last page */}
 			{screen == 0 ||
 				(screen < 6 && (
-					<View style={styles.topContainer}>
-						<Text style={styles.text}>{FormTitle[screen]}</Text>
-						<View style={styles.breakline} />
-						<View style={styles.previouspageAndPaginationWrapper}>
-							<Pressable onPress={goToPreviousPage}>
-								<Text style={{ color: "#888888" }}>{`<  上一步`}</Text>
-							</Pressable>
-							<PaginationIndicator totalPages={FormTitle.length} currentPage={screen} />
-							<Pressable disabled={true} onPress={goToNextPage}>
-								<Text style={{ color: "#888888", opacity: 0 }}>{`下一步  >`}</Text>
-							</Pressable>
+					//dismiss keyboard when user click outside of the input field to improve user experience
+					<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
+						<View style={styles.topContainer}>
+							<Text style={styles.text}>{FormTitle[screen]}</Text>
+							<View style={styles.breakline} />
+							<View style={styles.previouspageAndPaginationWrapper}>
+								<Pressable onPress={goToPreviousPage}>
+									<Text style={{ color: "#888888" }}>{`<  上一步`}</Text>
+								</Pressable>
+								<PaginationIndicator totalPages={FormTitle.length} currentPage={screen} />
+								<Pressable disabled={true} onPress={goToNextPage}>
+									<Text style={{ color: "#888888", opacity: 0 }}>{`下一步  >`}</Text>
+								</Pressable>
+							</View>
 						</View>
-					</View>
+					</TouchableWithoutFeedback>
 				))}
-			<View style={styles.bottomContainer}>{ScreenDisplay()}</View>
+			<KeyboardAwareScrollView enableOnAndroid={true} resetScrollToCoords={{ x: 0, y: 0 }}>
+				<View style={styles.bottomContainer}>{ScreenDisplay()}</View>
+			</KeyboardAwareScrollView>
 		</>
 	);
 };

+ 6 - 1
component/multiStepForm/formComponent/formPages/basicInformation.tsx

@@ -33,7 +33,10 @@ const BasicInformation: React.FC<basicInformationProps> = ({ handleFormDataChang
 						gap: 10,
 					}}
 				>
-					<NormalInput placeholder="姓名" onChangeText={(text) => handleFormDataChange("name", text)} />
+					<NormalInput
+						placeholder={formData.name ? formData.name : "姓名"}
+						onChangeText={(text) => handleFormDataChange("name", text)}
+					/>
 					<NormalInput
 						placeholder="帳戶密碼"
 						onChangeText={(text) => handleFormDataChange("password", text)}
@@ -48,6 +51,8 @@ const BasicInformation: React.FC<basicInformationProps> = ({ handleFormDataChang
 							extendedStyle={{ width: "50%" }}
 						/>
 						<DateModal
+							// placeholder="出生日期"
+							placeholder={formData.birthDate ? formData.birthDate : "DD/MM/YY"}
 							onDateChange={(date) => {
 								handleFormDataChange("birthDate", date);
 							}}

+ 7 - 7
component/multiStepForm/formComponent/formPages/carInformation.tsx

@@ -1,12 +1,12 @@
-import { View, Text, StyleSheet,  } from "react-native";
-import { FormData,HandleFormDataChange } from "../../../type";
+import { View, Text, StyleSheet } from "react-native";
+import { FormData, HandleFormDataChange } from "../../../type";
 import NormalInput from "../../../global/normal_input";
 import NormalButton from "../../../global/normal_button";
 import { useState } from "react";
 
 type CarInformationProps = {
 	goToNextPage: () => void;
-	handleFormDataChange:HandleFormDataChange;
+	handleFormDataChange: HandleFormDataChange;
 	formData: FormData;
 };
 
@@ -24,7 +24,7 @@ const CarInformation: React.FC<CarInformationProps> = ({ goToNextPage, handleFor
 	return (
 		<>
 			<View style={styles.container}>
-				<Text style={styles.text}>請填妥以下資料</Text>
+				<Text style={styles.text}>您的車輛</Text>
 				<View
 					style={{
 						display: "flex",
@@ -33,15 +33,15 @@ const CarInformation: React.FC<CarInformationProps> = ({ goToNextPage, handleFor
 					}}
 				>
 					<NormalInput
-						placeholder="車輛品牌"
+						placeholder={formData.vehicleType ? formData.vehicleType : "車輛品牌"}
 						onChangeText={(vehicleType) => handleFormDataChange("vehicleType", vehicleType)}
 					/>
 					<NormalInput
-						placeholder="車輛型號"
+						placeholder={formData.vehicleModel ? formData.vehicleModel : "車輛型號"}
 						onChangeText={(vehicleModel) => handleFormDataChange("vehicleModel", vehicleModel)}
 					/>
 					<NormalInput
-						placeholder="車輛號碼"
+						placeholder={formData.licensePlate ? formData.licensePlate : "車輛號碼"}
 						onChangeText={(licensePlate) => handleFormDataChange("licensePlate", licensePlate)}
 					/>
 					<NormalButton

+ 15 - 9
component/multiStepForm/formComponent/formPages/createWallet.tsx

@@ -1,5 +1,5 @@
 import { View, Text, StyleSheet } from "react-native";
-import { FormData,HandleFormDataChange } from "../../../type";
+import { FormData, HandleFormDataChange } from "../../../type";
 import NormalInput from "../../../global/normal_input";
 import NormalButton from "../../../global/normal_button";
 import { useState } from "react";
@@ -7,7 +7,7 @@ import SingleSelectButtonGroup from "../../../global/select_button";
 
 type CreateWalletProps = {
 	goToNextPage: () => void;
-	handleFormDataChange: HandleFormDataChange
+	handleFormDataChange: HandleFormDataChange;
 	formData: FormData;
 };
 const creditCard = "信用卡";
@@ -15,11 +15,8 @@ const weChatAliPay = "微信支付/支付寶";
 
 const CreateWallet: React.FC<CreateWalletProps> = ({ goToNextPage, handleFormDataChange, formData }) => {
 	const options = [{ label: creditCard }, { label: weChatAliPay }];
-	
-	const [selectedOption, setSelectedOption] = useState("");
 
 	const handleSelectedChange = (selectedLabel: string) => {
-		setSelectedOption(selectedLabel);
 		handleFormDataChange("paymentMethod", selectedLabel);
 		setError("");
 	};
@@ -33,6 +30,16 @@ const CreateWallet: React.FC<CreateWalletProps> = ({ goToNextPage, handleFormDat
 		}
 	};
 
+	const selectLabelShown = () => {
+		if (formData.paymentMethod == null) {
+			return null;
+		} else if (formData.paymentMethod == creditCard) {
+			return creditCard;
+		} else if (formData.paymentMethod == weChatAliPay) {
+			return weChatAliPay;
+		}
+	};
+
 	const [error, setError] = useState("");
 	return (
 		<>
@@ -46,19 +53,18 @@ const CreateWallet: React.FC<CreateWalletProps> = ({ goToNextPage, handleFormDat
 					}}
 				>
 					<NormalInput
-						placeholder="電郵地址"
+						placeholder={formData.email ? formData.email : "電子郵件"}
 						onChangeText={(email) => handleFormDataChange("email", email)}
 					/>
 					<NormalInput
-						placeholder="地址"
+						placeholder={formData.address ? formData.address : "地址"}
 						onChangeText={(address) => handleFormDataChange("address", address)}
 					/>
-
 					<SingleSelectButtonGroup
 						options={options}
 						onSelectionChange={handleSelectedChange}
 						shouldShowRedOutline={error ? true : false}
-						selectedOption={selectedOption}
+						selectedOption={selectLabelShown()}
 					/>
 
 					<NormalButton

+ 2 - 2
component/multiStepForm/formComponent/formPages/loginPage.tsx

@@ -32,10 +32,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ goToNextPage }) => {
 						<Text style={styles.text}>忘記密碼</Text>
 					</Pressable>
 					<NormalButton
+						onPress={() => console.log("登入")}
 						title={<Text style={{ fontWeight: "700", fontSize: 20, color: "#fff" }}>登入</Text>}
-						onPress={goToNextPage}
 					/>
-					<Pressable>
+					<Pressable onPress={goToNextPage}>
 						<Text style={styles.text}>註冊會員</Text>
 					</Pressable>
 				</View>

+ 11 - 1
component/multiStepForm/formComponent/formPages/uberDriver.tsx

@@ -27,6 +27,16 @@ const UberDriver: React.FC<UberDriverProps> = ({ formData, goToNextPage, handleF
 		setError("");
 	};
 
+	const selectLabelShown = () => {
+		if (formData.isUberDriver == undefined) {
+			return null;
+		} else if (formData.isUberDriver == true) {
+			return "是(可享有獨家優惠)";
+		} else {
+			return "否";
+		}
+	};
+
 	const options = [{ label: "是(可享有獨家優惠)" }, { label: "否" }];
 	return (
 		<>
@@ -36,7 +46,7 @@ const UberDriver: React.FC<UberDriverProps> = ({ formData, goToNextPage, handleF
 					options={options}
 					onSelectionChange={handleSelectedChange}
 					shouldShowRedOutline={error ? true : false}
-					selectedOption={selectedOption}
+					selectedOption={selectLabelShown()}
 				/>
 				<NormalButton
 					title={<Text style={{ color: "#fff" }}>下一步</Text>}

+ 47 - 9
component/multiStepForm/formComponent/formPages/verification.tsx

@@ -1,5 +1,5 @@
-import { View, Text, StyleSheet, TextInput } from "react-native";
-import { useState } from "react";
+import { View, Text, StyleSheet, TextInput, Pressable } from "react-native";
+import { useEffect, useState } from "react";
 import { FormData, HandleFormDataChange } from "../../../type";
 import PhoneInput from "../../../global/phone_input";
 import NumberInput from "../../../global/number_input";
@@ -14,20 +14,58 @@ type VerificationProps = {
 const Verification: React.FC<VerificationProps> = ({ setScreen, formData, handleFormDataChange }) => {
 	const [error, setError] = useState("");
 	const [otp, setOtp] = useState("");
+	const [canSendOtp, setCanSendOtp] = useState(true);
+	const [lockPhoneInput, setLockPhoneInput] = useState(false);
 
 	const handleVerification = () => {
 		if (formData.phone === "" || otp === "") {
-			setError("請輸入電話號碼和OTP驗證碼");
+			setError("請確保所有資料都已填寫。");
 		} else {
 			setError("");
 			setScreen((currentScreenNumber) => currentScreenNumber + 1);
 		}
 	};
+
+	const handleSubmitOtp = () => {
+		if (formData.phoneVerificationStatus) {
+			if (canSendOtp) {
+				setCanSendOtp(false);
+				setLockPhoneInput(true);
+				console.log(lockPhoneInput);
+				//can only request otp every 60 seconds
+				setTimeout(() => {
+					setCanSendOtp(true);
+					setLockPhoneInput(false);
+				}, 60000);
+				setError("");
+			} else {
+				setError("請等待一分鐘後再重新發送。");
+			}
+		} else {
+			setError("請確保所有資料都已填寫。");
+		}
+	};
+
+	const handleChangePhoneNumber = () => {
+		setLockPhoneInput(false);
+	};
+
+	const otpButtonText = lockPhoneInput ? (
+		<Text style={{ color: "#fff" }}>重新發送</Text>
+	) : (
+		<Text style={{ color: "#fff" }}>發送</Text>
+	);
+
 	return (
 		<>
 			<View style={styles.container}>
 				<Text style={styles.text}>請驗證您的電話號碼</Text>
-				<PhoneInput placeholder="電話號碼" handleFormDataChange={handleFormDataChange} />
+				<PhoneInput
+					placeholder={formData.phone ? formData.phone : "輸入電話號碼"}
+					handleFormDataChange={handleFormDataChange}
+					editable={!lockPhoneInput}
+					extendedStyle={{ opacity: !lockPhoneInput ? 1 : 0.5 }}
+				/>
 				<View
 					style={{
 						display: "flex",
@@ -37,11 +75,7 @@ const Verification: React.FC<VerificationProps> = ({ setScreen, formData, handle
 					}}
 				>
 					<NumberInput placeholder="OTP驗證碼" onChangeText={setOtp} extendedStyle={{ flex: 1 }} />
-					<NormalButton
-						title={<Text style={{ color: "#fff" }}>發送</Text>}
-						onPress={() => console.log("you pressed 發送")}
-						extendedStyle={{ flex: 1 / 2 }}
-					/>
+					<NormalButton title={otpButtonText} onPress={handleSubmitOtp} extendedStyle={{ flex: 1 / 2 }} />
 				</View>
 				<NormalButton
 					title={<Text style={{ color: "#fff" }}>驗證</Text>}
@@ -51,6 +85,9 @@ const Verification: React.FC<VerificationProps> = ({ setScreen, formData, handle
 					extendedStyle={{}}
 				/>
 				{error && <Text style={styles.errorMessage}>{error}</Text>}
+				<Pressable onPress={handleChangePhoneNumber}>
+					<Text style={[styles.footer, { opacity: lockPhoneInput ? 1 : 0 }]}>修改電話號碼</Text>
+				</Pressable>
 			</View>
 		</>
 	);
@@ -72,5 +109,6 @@ const styles = StyleSheet.create({
 		marginLeft: 10,
 		marginTop: 10,
 	},
+	footer: { color: "#02677D", fontSize: 16, paddingVertical: 10 },
 });
 export default Verification;

+ 21 - 0
package-lock.json

@@ -21,6 +21,7 @@
         "expo-status-bar": "~1.11.1",
         "react": "18.2.0",
         "react-native": "0.73.4",
+        "react-native-keyboard-aware-scroll-view": "^0.9.5",
         "react-native-maps": "1.10.0",
         "react-native-modern-datepicker": "^1.0.0-beta.91",
         "react-native-safe-area-context": "4.8.2",
@@ -12341,6 +12342,26 @@
         "react": "18.2.0"
       }
     },
+    "node_modules/react-native-iphone-x-helper": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
+      "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==",
+      "peerDependencies": {
+        "react-native": ">=0.42.0"
+      }
+    },
+    "node_modules/react-native-keyboard-aware-scroll-view": {
+      "version": "0.9.5",
+      "resolved": "https://registry.npmjs.org/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz",
+      "integrity": "sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==",
+      "dependencies": {
+        "prop-types": "^15.6.2",
+        "react-native-iphone-x-helper": "^1.0.3"
+      },
+      "peerDependencies": {
+        "react-native": ">=0.48.4"
+      }
+    },
     "node_modules/react-native-maps": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.10.0.tgz",

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "expo-status-bar": "~1.11.1",
     "react": "18.2.0",
     "react-native": "0.73.4",
+    "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-maps": "1.10.0",
     "react-native-modern-datepicker": "^1.0.0-beta.91",
     "react-native-safe-area-context": "4.8.2",