@towsila/expo-ui-library 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +142 -0
  2. package/package.json +55 -0
  3. package/src/assets/customIcons/arrow.svg +3 -0
  4. package/src/assets/customIcons/arrow_back.svg +3 -0
  5. package/src/assets/customIcons/ban.svg +4 -0
  6. package/src/assets/customIcons/checkmark.svg +4 -0
  7. package/src/assets/customIcons/close.svg +5 -0
  8. package/src/assets/customIcons/dark_mode.svg +3 -0
  9. package/src/assets/customIcons/dark_suspended.svg +67 -0
  10. package/src/assets/customIcons/document.svg +4 -0
  11. package/src/assets/customIcons/email.svg +3 -0
  12. package/src/assets/customIcons/eye.svg +5 -0
  13. package/src/assets/customIcons/eye_off.svg +7 -0
  14. package/src/assets/customIcons/humberger.svg +5 -0
  15. package/src/assets/customIcons/information.svg +6 -0
  16. package/src/assets/customIcons/light_mode.svg +18 -0
  17. package/src/assets/customIcons/light_suspended.svg +67 -0
  18. package/src/assets/customIcons/lock_closed.svg +4 -0
  19. package/src/assets/customIcons/logout.svg +4 -0
  20. package/src/assets/customIcons/person.svg +4 -0
  21. package/src/assets/customIcons/phone.svg +3 -0
  22. package/src/assets/customIcons/phone_dark.svg +36 -0
  23. package/src/assets/customIcons/phone_light.svg +36 -0
  24. package/src/assets/customIcons/shield.svg +4 -0
  25. package/src/assets/customIcons/trash.svg +9 -0
  26. package/src/assets/customIcons/warning.svg +5 -0
  27. package/src/assets/icons/google.svg +9 -0
  28. package/src/assets/images/bg_dark.svg +5 -0
  29. package/src/assets/images/bg_dark_v1.svg +5 -0
  30. package/src/assets/images/bg_light.svg +5 -0
  31. package/src/assets/images/bg_light_v1.svg +5 -0
  32. package/src/assets/images/en.jpg +0 -0
  33. package/src/assets/images/fr.jpg +0 -0
  34. package/src/components/icons/ban.tsx +77 -0
  35. package/src/components/icons/chatIcon.tsx +19 -0
  36. package/src/components/icons/homeIcon.tsx +25 -0
  37. package/src/components/icons/profileIcon.tsx +29 -0
  38. package/src/components/icons/settingsIcon.tsx +22 -0
  39. package/src/components/reusable/templatePage.tsx +94 -0
  40. package/src/components/reusable/templateSubPage.tsx +66 -0
  41. package/src/components/screens/AccountStatusScreen.tsx +102 -0
  42. package/src/components/screens/AddReportScreen.tsx +135 -0
  43. package/src/components/screens/LegalScreen.tsx +41 -0
  44. package/src/components/screens/VerificationCodeScreen.tsx +66 -0
  45. package/src/components/screens/aboutScreen.tsx +139 -0
  46. package/src/components/screens/forgotPasswordScreen.tsx +60 -0
  47. package/src/components/screens/loginScreen.tsx +165 -0
  48. package/src/components/screens/onBoarding.tsx +178 -0
  49. package/src/components/screens/resetPassword.tsx +115 -0
  50. package/src/components/screens/settingScreen.tsx +85 -0
  51. package/src/components/screens/signUpScreen.tsx +119 -0
  52. package/src/components/screens/splashScreen.tsx +170 -0
  53. package/src/components/ui/OTPInput.tsx +73 -0
  54. package/src/components/ui/PageTopBar.tsx +41 -0
  55. package/src/components/ui/asidebar.tsx +151 -0
  56. package/src/components/ui/badge.tsx +21 -0
  57. package/src/components/ui/button.tsx +58 -0
  58. package/src/components/ui/customPopup.tsx +176 -0
  59. package/src/components/ui/dynamicForm.tsx +144 -0
  60. package/src/components/ui/error.tsx +23 -0
  61. package/src/components/ui/input.tsx +121 -0
  62. package/src/components/ui/languageSelect.tsx +99 -0
  63. package/src/components/ui/pageIndicator.tsx +58 -0
  64. package/src/components/ui/passwordRequirmentCard.tsx +55 -0
  65. package/src/components/ui/sharpButton.tsx +67 -0
  66. package/src/components/ui/sharpContainer.tsx +180 -0
  67. package/src/components/ui/sharpDynamicForm.tsx +148 -0
  68. package/src/components/ui/sharpInput.tsx +148 -0
  69. package/src/components/ui/sharpOtpInput.tsx +228 -0
  70. package/src/components/ui/sharpSelect.tsx +155 -0
  71. package/src/components/ui/successDialog.tsx +97 -0
  72. package/src/components/ui/termsPopup.tsx +56 -0
  73. package/src/components/ui/themeToggle.tsx +98 -0
  74. package/src/components/ui/toast.tsx +163 -0
  75. package/src/components/ui/warningModel.tsx +132 -0
  76. package/src/constants/language.ts +15 -0
  77. package/src/context/LanguageContext.tsx +49 -0
  78. package/src/context/LocationPermissionProvider.tsx +106 -0
  79. package/src/context/ThemeContext.tsx +70 -0
  80. package/src/hooks/useCountdown.ts +21 -0
  81. package/src/hooks/useForgotPasswordForm.ts +46 -0
  82. package/src/hooks/useForm.ts +52 -0
  83. package/src/hooks/useLoginForm.ts +25 -0
  84. package/src/hooks/useResetPasswordForm.ts +75 -0
  85. package/src/hooks/useSignup.tsx +34 -0
  86. package/src/hooks/useSubmitReport.ts +47 -0
  87. package/src/hooks/useUpdateUser.tsx +81 -0
  88. package/src/icons/index.ts +36 -0
  89. package/src/index.ts +62 -0
  90. package/src/service/createApi.ts +57 -0
  91. package/src/service/sendEmail.ts +11 -0
  92. package/src/tokens/themes.ts +19 -0
  93. package/src/tokens/typography.ts +16 -0
  94. package/src/types/formFields.ts +13 -0
  95. package/src/types/icon.ts +6 -0
  96. package/src/types/language.ts +7 -0
  97. package/src/types/menu.ts +9 -0
  98. package/src/types/settingsFields.tsx +9 -0
  99. package/src/types/svg.d.ts +6 -0
  100. package/src/types/validateForm.ts +8 -0
  101. package/src/utils/helpers/groupSettings.ts +21 -0
  102. package/src/utils/inputValidator/customValidator.ts +36 -0
  103. package/src/utils/inputValidator/generalTerms.ts +19 -0
  104. package/src/utils/inputValidator/registerValidator.ts +31 -0
@@ -0,0 +1,73 @@
1
+ import { useRef, useState } from "react";
2
+ import { View, TextInput, StyleSheet, StyleProp, ViewProps } from "react-native";
3
+ import { useAppTheme } from "../../context/ThemeContext";
4
+ import { textSize, Fonts } from "../../tokens/typography";
5
+
6
+ interface OtpInputProps {
7
+ length?: number;
8
+ onComplete?: (code: string) => void;
9
+ onChange?: (code: string) => void;
10
+ style?: StyleProp<ViewProps>;
11
+ size?:number;
12
+ borderRadius?: number;
13
+ fontFamily?: string;
14
+ }
15
+
16
+ export default function OtpInput({ length = 6, onComplete, onChange, style, size = 45, borderRadius = 10, fontFamily=Fonts.montserrat.regular }: OtpInputProps) {
17
+ const { colors } = useAppTheme();
18
+ const styles = createStyles(colors);
19
+ const [digits, setDigits] = useState<string[]>(Array(length).fill(""));
20
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
21
+ const refs = useRef<TextInput[]>([]);
22
+
23
+ const handleChange = (text: string, index: number) => {
24
+ const newDigits = [...digits];
25
+ newDigits[index] = text;
26
+ setDigits(newDigits);
27
+
28
+ const code = newDigits.join("");
29
+ onChange?.(code);
30
+
31
+ if (text && index < length - 1) {
32
+ refs.current[index + 1]?.focus();
33
+ }
34
+
35
+ if (code.length === length && newDigits.every((d) => d !== "")) {
36
+ onComplete?.(code);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <View style={{ flexDirection: "row", justifyContent: "center", gap: 12, marginHorizontal: 20 }}>
42
+ {digits.map((digit, index) => (
43
+ <TextInput
44
+ key={index}
45
+ ref={(r) => { if (r) refs.current[index] = r; }}
46
+ style={[
47
+ styles.otpInput,
48
+ { borderColor: focusedIndex === index ? colors.primary : "transparent", width: size, height: size, borderRadius: borderRadius, fontFamily:fontFamily },
49
+ style
50
+ ]}
51
+ keyboardType="number-pad"
52
+ maxLength={1}
53
+ textAlign="center"
54
+ value={digit}
55
+ onChangeText={(text) => handleChange(text, index)}
56
+ onFocus={() => setFocusedIndex(index)}
57
+ onBlur={() => setFocusedIndex(null)}
58
+ placeholder="."
59
+ />
60
+ ))}
61
+ </View>
62
+ );
63
+ }
64
+
65
+ const createStyles = (colors: any) =>
66
+ StyleSheet.create({
67
+ otpInput: {
68
+ borderWidth: 2,
69
+ fontSize: textSize.md,
70
+ color: colors.text.primary,
71
+ backgroundColor: colors.background.card,
72
+ },
73
+ });
@@ -0,0 +1,41 @@
1
+ import { Fonts, textSize } from "../../tokens/typography";
2
+ import { useAppTheme } from "../../context/ThemeContext";
3
+ import { Ionicons } from "@expo/vector-icons";
4
+ import { Text, TouchableOpacity, View } from "react-native";
5
+ import ArrowBack from "../../assets/customIcons/arrow_back.svg";
6
+ import ArrowBackDark from "../../assets/customIcons/arrow_back_dark.svg";
7
+
8
+ interface PageProps{
9
+ title: string,
10
+ description: string;
11
+ onBack: ()=>void;
12
+ }
13
+
14
+ export default function PageTopBar({title, description, onBack}:PageProps){
15
+ const {colors, isDark} = useAppTheme();
16
+
17
+ return (
18
+ <View>
19
+ <View style={{
20
+ flexDirection: "row",
21
+ alignItems: "center", marginBottom: 10 }}>
22
+ <TouchableOpacity onPress={onBack}>
23
+ <ArrowBack width={24} height={24} color={colors.text.primary}/>
24
+ </TouchableOpacity>
25
+ <Text style={{ flex: 1, textAlign: "center", color: colors.text.primary, fontSize: textSize.xxl, fontFamily: Fonts.montserrat.bold }}>
26
+ {title}
27
+ </Text>
28
+ <View style={{ width: 24 }} />
29
+ </View>
30
+ <Text style={{
31
+ fontSize: textSize.sm,
32
+ fontFamily: Fonts.montserrat.regular,
33
+ textAlign: 'center',
34
+ marginBottom: 15,
35
+ color: colors.text.primary,
36
+ }}>
37
+ {description}
38
+ </Text>
39
+ </View>
40
+ )
41
+ }
@@ -0,0 +1,151 @@
1
+ import { TouchableOpacity, View, Text, Modal, Pressable, Animated } from "react-native";
2
+ import { Menu } from "../../types/menu";
3
+ import { useLanguage } from "../../context/LanguageContext";
4
+ import LanguageSelect from "./languageSelect";
5
+ import React, { useState, useEffect, useRef } from "react";
6
+ import { Fonts, textSize } from "../../tokens/typography";
7
+ import { useAppTheme } from "../../context/ThemeContext";
8
+ import { Dimensions } from "react-native";
9
+ import LogoutIcon from "../../assets/customIcons/logout.svg";
10
+ import ThemeToggle from "./themeToggle";
11
+ const SIDEBAR_WIDTH = Dimensions.get("window").width * 0.7;
12
+
13
+ interface AsideProps{
14
+ visible: boolean;
15
+ onClose: () => void;
16
+ logo: React.ReactNode;
17
+ name?: string;
18
+ menu: Menu[];
19
+ onLogout: ()=>void;
20
+ logoutLabel: string;
21
+ onNavigate: (link: string) => void;
22
+ themeLabel?:string;
23
+ }
24
+
25
+ export default function AsideBar({logo, menu, onLogout, logoutLabel, onNavigate, visible, onClose, themeLabel}:AsideProps){
26
+ const { language, setLanguage } = useLanguage();
27
+ const {colors} = useAppTheme();
28
+ const [activeIndex, setActiveIndex] = useState<number | null>(0);
29
+ const slideAnim = useRef(new Animated.Value(-SIDEBAR_WIDTH)).current;
30
+ const fadeAnim = useRef(new Animated.Value(0)).current;
31
+
32
+ useEffect(() => {
33
+ if (visible) {
34
+ Animated.parallel([
35
+ Animated.timing(slideAnim, {
36
+ toValue: 0,
37
+ duration: 250,
38
+ useNativeDriver: true,
39
+ }),
40
+ Animated.timing(fadeAnim, {
41
+ toValue: 1,
42
+ duration: 250,
43
+ useNativeDriver: true,
44
+ }),
45
+ ]).start();
46
+ } else {
47
+ Animated.parallel([
48
+ Animated.timing(slideAnim, {
49
+ toValue: -SIDEBAR_WIDTH,
50
+ duration: 200,
51
+ useNativeDriver: true,
52
+ }),
53
+ Animated.timing(fadeAnim, {
54
+ toValue: 0,
55
+ duration: 200,
56
+ useNativeDriver: true,
57
+ }),
58
+ ]).start();
59
+ }
60
+ }, [visible]);
61
+
62
+ return(
63
+ <Modal visible={visible} transparent animationType="none" onRequestClose={onClose}>
64
+ <View style={{ flex: 1, flexDirection: "row" }}>
65
+ {/* Animated sidebar panel sliding from left */}
66
+ <Animated.View style={{
67
+ justifyContent: "space-between",
68
+ height: "100%",
69
+ width: SIDEBAR_WIDTH,
70
+ backgroundColor: colors.background.main,
71
+ padding: 20,
72
+ transform: [{ translateX: slideAnim }],
73
+ }}>
74
+ {/* top container */}
75
+ <View style={{flex: 1, gap: 30}}>
76
+ {/* logo, name and language row */}
77
+ <View style={{flexDirection: "row", alignItems:"center", justifyContent:"space-between"}}>
78
+ <View style={{flexDirection: "row", alignItems: "center", gap: 6}}>
79
+ {logo}
80
+ </View>
81
+ <View>
82
+ <LanguageSelect value={language} onChange={setLanguage} />
83
+ </View>
84
+ </View>
85
+
86
+ {/* menu links */}
87
+ <View style={{flex: 1, gap: 11}}>
88
+ {menu.map((item, index) => {
89
+ const isActive = activeIndex === index;
90
+ return (
91
+ <TouchableOpacity
92
+ key={index}
93
+ style={{
94
+ flexDirection: "row",
95
+ alignItems: "center",
96
+ gap: 12,
97
+ paddingVertical: 14,
98
+ paddingHorizontal: 16,
99
+ borderRadius: 10,
100
+ backgroundColor: isActive ? colors.primary + "20" : "transparent"
101
+ }}
102
+ onPress={() => {
103
+ setActiveIndex(index);
104
+ onNavigate(item.link);
105
+ onClose();
106
+ }}
107
+ >
108
+ {item.iconSvg && (
109
+ item.iconSvg(isActive ? colors.primary : "transparent")
110
+ )}
111
+ <Text style={{
112
+ fontFamily: isActive ? Fonts.montserrat.semiBold : Fonts.montserrat.regular,
113
+ fontSize: textSize.md,
114
+ color: isActive ? colors.primary : colors.text.primary,
115
+ flex:1
116
+ }}>
117
+ {item.name}
118
+ </Text>
119
+ </TouchableOpacity>
120
+ );
121
+ })}
122
+ </View>
123
+ </View>
124
+
125
+ {/* bottom container */}
126
+ <View style={{ gap: 16, paddingHorizontal: 16, }}>
127
+ <TouchableOpacity onPress={onLogout} style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
128
+ <LogoutIcon width={22} height={22} color={colors.semantic.error}/>
129
+ <Text style={{
130
+ fontFamily: Fonts.montserrat.regular,
131
+ fontSize: textSize.md,
132
+ color: colors.text.primary,
133
+ }}>
134
+ {logoutLabel}
135
+ </Text>
136
+ </TouchableOpacity>
137
+ <ThemeToggle label={themeLabel}/>
138
+ </View>
139
+ </Animated.View>
140
+
141
+ {/* Dimmed backdrop — full remaining width, tap to close */}
142
+ <Animated.View style={{ flex: 1, opacity: fadeAnim }}>
143
+ <Pressable
144
+ style={{ flex: 1, backgroundColor: "rgba(0,0,0,0.5)" }}
145
+ onPress={onClose}
146
+ />
147
+ </Animated.View>
148
+ </View>
149
+ </Modal>
150
+ )
151
+ }
@@ -0,0 +1,21 @@
1
+ import { Text } from "react-native";
2
+ import { Fonts, textSize } from "../../tokens/typography";
3
+
4
+ interface BadgeProps{
5
+ text: string;
6
+ bgColor: string;
7
+ textColor: string;
8
+ }
9
+ export default function Badge({text, bgColor, textColor}: BadgeProps){
10
+ return(
11
+ <Text style={{padding: 8,
12
+ backgroundColor: bgColor,
13
+ borderRadius: 10,
14
+ fontSize: textSize.sm,
15
+ fontFamily: Fonts.montserrat.semiBold,
16
+ color: textColor}}
17
+ >
18
+ {text}
19
+ </Text>
20
+ )
21
+ }
@@ -0,0 +1,58 @@
1
+ import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, TouchableOpacityProps, StyleProp, ViewStyle } from "react-native";
2
+ import { Fonts, textSize } from "../../tokens/typography";
3
+ type ButtonProps = {
4
+ text: string;
5
+ bgColor?: string;
6
+ textColor?: string;
7
+ onPress?: () => void;
8
+ fontSize?: number;
9
+ fontFamily?: string;
10
+ height?: number;
11
+ loading?: boolean;
12
+ borderColor?:string;
13
+ borderRadius?:number;
14
+ style?: StyleProp<ViewStyle>;
15
+ };
16
+
17
+ export default function Button({
18
+ text,
19
+ bgColor,
20
+ textColor,
21
+ onPress,
22
+ fontSize = textSize.md,
23
+ fontFamily = Fonts.montserrat.semiBold,
24
+ height = 60,
25
+ loading,
26
+ borderColor,
27
+ borderRadius = 10,
28
+ style,
29
+ }: ButtonProps) {
30
+ return (
31
+ <TouchableOpacity
32
+ key={text}
33
+ style={[styles.button, {
34
+ backgroundColor: bgColor,
35
+ height: height,
36
+ borderColor: borderColor ?? "transparent",
37
+ borderWidth: borderColor ? 1 : 0,
38
+ borderRadius: borderRadius,
39
+ }, style]}
40
+ onPress={onPress}
41
+ >
42
+ {loading ? (
43
+ <ActivityIndicator color={textColor} size="small" />
44
+ ) : (
45
+ <Text style={{ fontSize, fontFamily, color: textColor }}>
46
+ {text}
47
+ </Text>
48
+ )}
49
+
50
+ </TouchableOpacity>
51
+ );
52
+ }
53
+ const styles= StyleSheet.create({
54
+ button: {
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ },
58
+ });
@@ -0,0 +1,176 @@
1
+ import { View, Text, StyleSheet, Modal, Animated, Dimensions } from "react-native";
2
+ import { useAppTheme } from "../../context/ThemeContext";
3
+ import { textSize, Fonts } from "../../tokens/typography";
4
+ import Button from "../ui/button";
5
+ import { useEffect, useRef } from "react";
6
+ import SharpButton from "./sharpButton";
7
+
8
+ const { height } = Dimensions.get("window");
9
+
10
+ interface CustomPopUpProps {
11
+ visible: boolean;
12
+ title?: string;
13
+ header?: React.ReactNode;
14
+ body: React.ReactNode;
15
+ onClose?: () => void;
16
+ onAgree?: () => void;
17
+ closeText?: string;
18
+ agreeText?: string;
19
+ agreeDisabled?: boolean;
20
+ fontFamily?: string;
21
+
22
+ // autoDismiss?: boolean;
23
+ // dismissDelay?: number;
24
+ }
25
+
26
+ export default function CustomPopUp({
27
+ visible,
28
+ title,
29
+ header,
30
+ body,
31
+ onClose,
32
+ onAgree,
33
+ closeText = "",
34
+ agreeText = "",
35
+ agreeDisabled = false,
36
+ fontFamily= Fonts.montserrat.bold,
37
+ }: CustomPopUpProps) {
38
+ const { colors } = useAppTheme();
39
+ const slideAnim = useRef(new Animated.Value(height)).current;
40
+ const overlayAnim = useRef(new Animated.Value(0)).current;
41
+
42
+ useEffect(() => {
43
+ if (visible) {
44
+ // animate IN
45
+ Animated.parallel([
46
+ Animated.timing(slideAnim, {
47
+ toValue: 0,
48
+ duration: 400,
49
+ useNativeDriver: true,
50
+ }),
51
+ Animated.timing(overlayAnim, {
52
+ toValue: 1,
53
+ duration: 400,
54
+ useNativeDriver: true,
55
+ }),
56
+ ]).start();
57
+ } else {
58
+ // animate OUT
59
+ Animated.parallel([
60
+ Animated.timing(slideAnim, {
61
+ toValue: height,
62
+ duration: 300,
63
+ useNativeDriver: true,
64
+ }),
65
+ Animated.timing(overlayAnim, {
66
+ toValue: 0,
67
+ duration: 300,
68
+ useNativeDriver: true,
69
+ }),
70
+ ]).start();
71
+ }
72
+ }, [visible]);
73
+
74
+
75
+ const handleAgree = () => {
76
+ Animated.parallel([
77
+ Animated.timing(slideAnim, { toValue: height, duration: 300, useNativeDriver: true }),
78
+ Animated.timing(overlayAnim, { toValue: 0, duration: 300, useNativeDriver: true }),
79
+ ]).start(() => {
80
+ onAgree ? onAgree() : undefined
81
+ }); // call onAgree AFTER animation finishes
82
+ };
83
+ const handleClose = () => {
84
+ Animated.parallel([
85
+ Animated.timing(slideAnim, {toValue: height, duration: 300,useNativeDriver: true,
86
+ }),
87
+ Animated.timing(overlayAnim, {toValue: 0,duration: 300,useNativeDriver: true,
88
+ }),
89
+ ]).start(() => {
90
+ onClose ? onClose(): undefined
91
+ });
92
+ };
93
+
94
+
95
+
96
+
97
+ return (
98
+ <Modal
99
+ visible={visible}
100
+ transparent
101
+ animationType="none" // we handle animation manually
102
+ statusBarTranslucent
103
+ >
104
+ {/* overlay */}
105
+ <Animated.View style={[styles.overlay, { opacity: overlayAnim }]} />
106
+
107
+ {/* sheet */}
108
+ <Animated.View
109
+ style={[
110
+ styles.sheetWrapper,
111
+ { transform: [{ translateY: slideAnim }], paddingHorizontal: 10 },
112
+ ]}
113
+ >
114
+ <View style={[styles.container, { backgroundColor: colors.background.main }]}>
115
+ {/* handle bar */}
116
+ <View style={[styles.handle, { backgroundColor: colors.text.placeholder }]} />
117
+
118
+ <Text style={{
119
+ fontFamily,
120
+ fontSize: textSize.xxl,
121
+ color: colors.text.primary,
122
+ marginBottom: 10,
123
+ }}>
124
+ {title}
125
+ </Text>
126
+
127
+ {header && <View style={{ marginBottom: 10 }}>{header}</View>}
128
+ <View style={{ marginBottom: 20 }}>{body}</View>
129
+
130
+ <View style={{ gap: 8, width: "100%", marginTop: 20 }}>
131
+ {agreeText && <SharpButton
132
+ text={agreeText}
133
+ onPress={handleAgree}
134
+ bgColor={colors.primary}
135
+ fontSize={textSize.sm}
136
+ height={50}
137
+ />}
138
+ {closeText ? (
139
+ <SharpButton
140
+ text={closeText}
141
+ onPress={handleClose}
142
+ bgColor={colors.primary}
143
+ fontSize={textSize.sm}
144
+ height={50}
145
+ />
146
+ ) : null}
147
+ </View>
148
+ </View>
149
+ </Animated.View>
150
+ </Modal>
151
+ );
152
+ }
153
+
154
+ const styles = StyleSheet.create({
155
+ overlay: {
156
+ ...StyleSheet.absoluteFill,
157
+ backgroundColor: "rgba(0,0,0,0.5)",
158
+ },
159
+ sheetWrapper: {
160
+ flex: 1,
161
+ justifyContent: "flex-end",
162
+ },
163
+ container: {
164
+ borderTopLeftRadius: 24,
165
+ borderTopRightRadius: 24,
166
+ padding: 24,
167
+ paddingBottom: 40,
168
+ },
169
+ handle: {
170
+ width: 40,
171
+ height: 4,
172
+ borderRadius: 2,
173
+ alignSelf: "center",
174
+ marginBottom: 0,
175
+ },
176
+ });
@@ -0,0 +1,144 @@
1
+ import { useState } from "react";
2
+ import { ScrollView, Text, TouchableOpacity, View } from "react-native";
3
+ import Input from "./input";
4
+ import { Field } from "../../types/formFields";
5
+ import { useAppTheme } from "../../context/ThemeContext";
6
+ import { validateForm } from "../../utils/inputValidator/registerValidator";
7
+ import { Ionicons } from "@expo/vector-icons";
8
+ import { Fonts, textSize } from "../../tokens/typography";
9
+ import SharpInput from "./sharpInput";
10
+ import Arrow from "../../assets/customIcons/arrow.svg";
11
+ import ArrowBack from "../../assets/customIcons/arrow_back.svg"
12
+ interface Props {
13
+ fields: Field[];
14
+ formState: Record<string, string>;
15
+ setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
16
+ errors: Record<string, string>;
17
+ setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>;
18
+ clearError: (name: string) => void;
19
+ onStepChange?: (step: number, total: number) => void;
20
+ nextLabel?: string;
21
+ backLabel?: string;
22
+ }
23
+ // can i split it into 2 not only move the password if the number of fields is > 4
24
+ function splitIntoSteps(fields: Field[]): Field[][] {
25
+ const info = fields.filter((f) => f.type !== "password");
26
+ const security = fields.filter((f) => f.type === "password");
27
+ return [info, security].filter((s) => s.length > 0);
28
+ }
29
+
30
+ export const DynamicForm: React.FC<Props> = ({
31
+ fields,
32
+ formState,
33
+ setFormState,
34
+ errors,
35
+ setErrors,
36
+ clearError,
37
+ onStepChange,
38
+ nextLabel,
39
+ backLabel,
40
+ }) => {
41
+ const [showPasswordField, setShowPasswordField] = useState<Record<string, boolean>>({});
42
+ const [currentStep, setCurrentStep] = useState(0);
43
+ const { colors } = useAppTheme();
44
+
45
+ const steps = splitIntoSteps(fields);
46
+ const totalSteps = steps.length;
47
+ const stepFields = steps[currentStep] ?? [];
48
+
49
+ const goNext = () => {
50
+ // validate current step fields before proceeding
51
+ const newErrors = validateForm(stepFields, formState);
52
+ setErrors((prev) => ({ ...prev, ...newErrors }));
53
+ if (Object.keys(newErrors).length > 0) return;
54
+
55
+ const next = Math.min(currentStep + 1, totalSteps - 1);
56
+ setCurrentStep(next);
57
+ onStepChange?.(next, totalSteps);
58
+ };
59
+
60
+ const goPrev = () => {
61
+ const prev = Math.max(currentStep - 1, 0);
62
+ setCurrentStep(prev);
63
+ onStepChange?.(prev, totalSteps);
64
+ };
65
+
66
+ const renderInput = (field: Field) => (
67
+ <View key={field.name} style={{ marginBottom: 10 }}>
68
+ <Input
69
+ value={formState[field.name] || ""}
70
+ onChangeText={(text) => setFormState((prev) => ({ ...prev, [field.name]: text }))}
71
+ iconLeft={field.icon}
72
+ iconRight={
73
+ field.secureTextEntry
74
+ ? showPasswordField[field.name] ? "eye-off-outline" : "eye-outline"
75
+ : undefined
76
+ }
77
+ placeholder={field.placeholder}
78
+ onPressIconRight={
79
+ field.secureTextEntry
80
+ ? () => setShowPasswordField((prev) => ({ ...prev, [field.name]: !prev[field.name] }))
81
+ : undefined
82
+ }
83
+ secureTextEntry={field.secureTextEntry && !showPasswordField[field.name]}
84
+ returnKeyType="next"
85
+ error={errors[field.name]}
86
+ onClearError={() => clearError(field.name)}
87
+ label={field.label}
88
+ />
89
+ </View>
90
+ );
91
+
92
+ return (
93
+ <View style={{ gap: 4 }}>
94
+
95
+ {/* Stepper — only show if more than 1 step */}
96
+ {totalSteps > 1 && (
97
+ <View style={{ flexDirection: "row", alignItems: "center", marginBottom: 12 }}>
98
+ {/* Back arrow */}
99
+ <View style={{ flex: 1 }}>
100
+ {currentStep > 0 && backLabel && (
101
+ <TouchableOpacity onPress={goPrev} style={{ flexDirection: "row", alignItems: "center", justifyContent: "flex-start", gap: 6 }}>
102
+ <ArrowBack width={16} height={16} color={colors.text.secondary}/>
103
+ <Text style={{ color: colors.text.secondary, fontFamily: Fonts.montserrat.semiBold, fontSize: textSize.sm }}>
104
+ {backLabel}
105
+ </Text>
106
+ </TouchableOpacity>
107
+ )}
108
+ </View>
109
+
110
+ {/* Pills */}
111
+ <View style={{ flexDirection: "row", gap: 8 }}>
112
+ {steps.map((_, i) => (
113
+ <View key={i} style={{
114
+ width: i === currentStep ? 28 : 10,
115
+ height: 6,
116
+ borderRadius: 3,
117
+ backgroundColor: i <= currentStep ? colors.primary : colors.text.placeholder + "40",
118
+ }} />
119
+ ))}
120
+ </View>
121
+ <View style={{ flex: 1 }} />
122
+ </View>
123
+ )}
124
+
125
+ {/* Fields */}
126
+ {stepFields.map((f) => renderInput(f))}
127
+
128
+ {/* Navigation */}
129
+ {totalSteps > 1 && (
130
+ <View style={{ gap: 10, marginTop: 4 }}>
131
+ {currentStep < totalSteps - 1 && (
132
+ <TouchableOpacity onPress={goNext} style={{ flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 6 }}>
133
+ <Text style={{ color: colors.text.secondary, fontFamily: Fonts.montserrat.semiBold, fontSize: textSize.sm }}>
134
+ {nextLabel}
135
+ </Text>
136
+ <Arrow width={16} height={16} color={colors.text.secondary}/>
137
+ </TouchableOpacity>
138
+ )}
139
+
140
+ </View>
141
+ )}
142
+ </View>
143
+ );
144
+ };
@@ -0,0 +1,23 @@
1
+ import { textSize, Fonts } from '../../tokens/typography'
2
+ import { useAppTheme } from "../../context/ThemeContext";
3
+ import {Text} from 'react-native';
4
+ interface ErrorProps{
5
+ message: string,
6
+ fontSize?: number,
7
+ position?: "left" | "center",
8
+ fontFamily?: string;
9
+ }
10
+ export default function ErrorMessage({message, fontSize = textSize.xs, position = "left", fontFamily = Fonts.montserrat.regular}:ErrorProps){
11
+ const {colors} = useAppTheme();
12
+ return(
13
+ <Text style={{
14
+ fontSize: fontSize,
15
+ color: colors.semantic.error,
16
+ fontFamily,
17
+ marginBottom: 10,
18
+ textAlign: position,
19
+ }}>
20
+ {message}
21
+ </Text>
22
+ )
23
+ }