@towsila/expo-ui-library 3.5.1 → 3.5.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towsila/expo-ui-library",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
4
4
  "description": "",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,14 +1,14 @@
1
1
  import React, { useState } from "react";
2
- import { Text, TouchableOpacity, View } from "react-native";
2
+ import { ScrollView, Text, TouchableOpacity, View } from "react-native";
3
3
  import { Field } from "../../types/formFields";
4
4
  import { useAppTheme } from "../../context/ThemeContext";
5
5
  import { useLanguage } from "../../context/LanguageContext";
6
- import { useFormSubmit } from "../../hooks/useSignup";
7
- import { ScrollWithIndicator } from "../ui/scrollIndicator";
6
+ import ScreenHeader from "../ui/PageTopBar";
8
7
  import { DynamicForm } from "../ui/dynamicForm";
8
+ import { Fonts, textSize } from "../../tokens/typography";
9
9
  import SharpButton from "../ui/sharpButton";
10
10
  import TermsPopup from "../ui/termsPopup";
11
- import { Fonts, textSize } from "../../tokens/typography";
11
+ import { useFormSubmit } from "../../hooks/useSignup";
12
12
 
13
13
  interface SignupProps {
14
14
  title: string;
@@ -28,11 +28,13 @@ interface SignupProps {
28
28
  dialogTitle: string;
29
29
  dialogDescription: string;
30
30
  dialogButton: string;
31
+ agreeText?: string; // "By continuing, you agree to our"
32
+ termsLinkText?: string; // "Terms & Privacy"
31
33
  t?: (key: string) => string;
32
- appLogo?: React.ReactNode;
33
34
  }
34
35
 
35
36
  export default function SignUpScreen({
37
+ title,
36
38
  formState,
37
39
  setFormState,
38
40
  onSubmit,
@@ -47,12 +49,14 @@ export default function SignUpScreen({
47
49
  dialogTitle,
48
50
  dialogDescription,
49
51
  dialogButton,
52
+ agreeText = "By continuing, you agree",
53
+ termsLinkText = "Terms & Privacy",
50
54
  t,
51
- appLogo,
52
55
  }: SignupProps) {
53
56
  const [showTerms, setShowTerms] = useState(true);
54
57
  const { colors } = useAppTheme();
55
- const {isRTL} = useLanguage();
58
+ const { isRTL } = useLanguage();
59
+
56
60
  const { handleSubmit, errors, clearError, loading, setErrors } = useFormSubmit({
57
61
  fields,
58
62
  formState,
@@ -60,13 +64,16 @@ const {isRTL} = useLanguage();
60
64
  });
61
65
 
62
66
  return (
63
- <View style={{ flex: 1, paddingHorizontal: 25, paddingVertical: 40 }}>
64
- {/* Fixed: logo */}
65
- <View style={{ alignItems: "center" }}>{appLogo}</View>
67
+ <View style={{ flex: 1, paddingHorizontal: 25, paddingTop: 50, paddingBottom: 20 }}>
68
+ {/* Fixed: header */}
69
+ <ScreenHeader title={title} description={""} />
66
70
 
67
- {/* Scrollable: form only, with vertical progress indicator */}
68
- <ScrollWithIndicator
71
+ {/* Scrollable: everything after the header */}
72
+ <ScrollView
73
+ style={{ flex: 1 }}
74
+ showsVerticalScrollIndicator={false}
69
75
  keyboardShouldPersistTaps="handled"
76
+ contentContainerStyle={{ paddingBottom: 20 }}
70
77
  >
71
78
  <DynamicForm
72
79
  fields={fields}
@@ -79,10 +86,27 @@ const {isRTL} = useLanguage();
79
86
  setErrors={setErrors}
80
87
  translator={t}
81
88
  />
82
- </ScrollWithIndicator>
83
89
 
84
- {/* Fixed: button + login link */}
85
- <View style={{ gap: 2, paddingTop: 20 }}>
90
+ {/* Terms notice */}
91
+ <Text
92
+ style={{
93
+ flexDirection: isRTL ? "row" : "row-reverse",
94
+ textAlign: isRTL? "left" : "right",
95
+ fontSize: textSize.xs,
96
+ color: colors.text.secondary,
97
+ fontFamily: Fonts.montserrat.regular,
98
+ marginBottom: 40,
99
+ }}
100
+ >
101
+ {agreeText}{" "}
102
+ <Text
103
+ onPress={() => setShowTerms(true)}
104
+ style={{ color: colors.primary, fontFamily: Fonts.montserrat.semiBold, textDecorationLine:"underline" }}
105
+ >
106
+ {termsLinkText}
107
+ </Text>
108
+ </Text>
109
+
86
110
  <SharpButton
87
111
  text={buttonLabel}
88
112
  bgColor={colors.primary}
@@ -90,21 +114,14 @@ const {isRTL} = useLanguage();
90
114
  loading={loading}
91
115
  height={50}
92
116
  />
93
- <TermsPopup
94
- title={dialogTitle}
95
- description={dialogDescription}
96
- buttonText={dialogButton}
97
- showTerms={showTerms}
98
- generalTerms={generalTerms}
99
- onAgree={() => setShowTerms(false)}
100
- />
117
+
101
118
  <View
102
119
  style={{
103
- flexDirection:isRTL ? "row":"row-reverse",
104
- justifyContent:isRTL ? "flex-end" :"flex-start",
120
+ flexDirection: isRTL ? "row" : "row-reverse",
121
+ justifyContent: "center",
105
122
  alignItems: "center",
106
123
  gap: 4,
107
- marginTop: 5,
124
+ marginTop: 15,
108
125
  }}
109
126
  >
110
127
  <Text
@@ -120,7 +137,7 @@ const {isRTL} = useLanguage();
120
137
  <Text
121
138
  style={{
122
139
  fontSize: textSize.xs,
123
- fontFamily: Fonts.montserrat.semiBold,
140
+ fontFamily: Fonts.montserrat.regular,
124
141
  color: colors.primary,
125
142
  }}
126
143
  >
@@ -128,7 +145,17 @@ const {isRTL} = useLanguage();
128
145
  </Text>
129
146
  </TouchableOpacity>
130
147
  </View>
131
- </View>
148
+ </ScrollView>
149
+
150
+ {/* Popup lives outside the scroll */}
151
+ <TermsPopup
152
+ title={dialogTitle}
153
+ description={dialogDescription}
154
+ buttonText={dialogButton}
155
+ showTerms={showTerms}
156
+ generalTerms={generalTerms}
157
+ onAgree={() => setShowTerms(false)}
158
+ />
132
159
  </View>
133
160
  );
134
161
  }
@@ -1,9 +1,12 @@
1
1
  import { useState } from "react";
2
- import {View } from "react-native";
2
+ import { View } from "react-native";
3
3
  import { Field } from "../../types/formFields";
4
4
  import Input from "./input";
5
+
6
+ type KeyedField = Field & { key?: string };
7
+
5
8
  interface Props {
6
- fields: Field[];
9
+ fields: KeyedField[];
7
10
  formState: Record<string, string>;
8
11
  setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
9
12
  errors: Record<string, string>;
@@ -14,7 +17,6 @@ interface Props {
14
17
  backLabel?: string;
15
18
  translator?: (key: string) => string;
16
19
  }
17
- // can i split it into 2 not only move the password if the number of fields is > 4
18
20
 
19
21
  export const DynamicForm: React.FC<Props> = ({
20
22
  fields,
@@ -26,33 +28,55 @@ export const DynamicForm: React.FC<Props> = ({
26
28
  }) => {
27
29
  const [showPasswordField, setShowPasswordField] = useState<Record<string, boolean>>({});
28
30
 
31
+ // Group consecutive fields that share the same `key`
32
+ const groups = fields.reduce<KeyedField[][]>((acc, field) => {
33
+ const last = acc[acc.length - 1];
34
+ if (field.key && last && last[0].key === field.key) {
35
+ last.push(field);
36
+ } else {
37
+ acc.push([field]);
38
+ }
39
+ return acc;
40
+ }, []);
41
+
42
+ const renderField = (field: KeyedField) => (
43
+ <Input
44
+ value={formState[field.name] || ""}
45
+ onChangeText={(text) => setFormState((prev) => ({ ...prev, [field.name]: text }))}
46
+ iconLeft={field.icon}
47
+ iconRight={
48
+ field.secureTextEntry
49
+ ? showPasswordField[field.name] ? "eye-off-outline" : "eye-outline"
50
+ : undefined
51
+ }
52
+ placeholder={field.placeholder}
53
+ onPressIconRight={
54
+ field.secureTextEntry
55
+ ? () => setShowPasswordField((prev) => ({ ...prev, [field.name]: !prev[field.name] }))
56
+ : undefined
57
+ }
58
+ secureTextEntry={field.secureTextEntry && !showPasswordField[field.name]}
59
+ returnKeyType="next"
60
+ error={errors[field.name] ? translator?.(errors[field.name]) : ""}
61
+ onClearError={() => clearError(field.name)}
62
+ label={field.label}
63
+ />
64
+ );
65
+
29
66
  return (
30
- <View style={{ gap: 4, paddingBottom: 20 }}>
31
- {fields.map((field) =>
32
- <View key={field.name} style={{ marginBottom: 10 }}>
33
- <Input
34
- value={formState[field.name] || ""}
35
- onChangeText={(text) => setFormState((prev) => ({ ...prev, [field.name]: text }))}
36
- iconLeft={field.icon}
37
- iconRight={
38
- field.secureTextEntry
39
- ? showPasswordField[field.name] ? "eye-off-outline" : "eye-outline"
40
- : undefined
41
- }
42
- placeholder={field.placeholder}
43
- onPressIconRight={
44
- field.secureTextEntry
45
- ? () => setShowPasswordField((prev) => ({ ...prev, [field.name]: !prev[field.name] }))
46
- : undefined
47
- }
48
- secureTextEntry={field.secureTextEntry && !showPasswordField[field.name]}
49
- returnKeyType="next"
50
- error={errors[field.name] ? translator?.(errors[field.name]) : ""}
51
- onClearError={() => clearError(field.name)}
52
- label={field.label}
53
- />
54
- </View>
55
- )}
67
+ <View style={{ gap: 4 }}>
68
+ {groups.map((group) => (
69
+ <View
70
+ key={group.map((f) => f.name).join("-")}
71
+ style={{ flexDirection: "row", gap: 10, marginBottom: 10 }}
72
+ >
73
+ {group.map((field) => (
74
+ <View key={field.name} style={{ flex: 1, minWidth: 0 }}>
75
+ {renderField(field)}
76
+ </View>
77
+ ))}
78
+ </View>
79
+ ))}
56
80
  </View>
57
81
  );
58
82
  };