@towsila/expo-ui-library 3.5.0 → 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.0",
3
+ "version": "3.5.2",
4
4
  "description": "",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,120 +1,161 @@
1
- // import { useRouter } from "expo-router";
2
1
  import React, { useState } from "react";
3
- import TemplatePage from "../reusable/templatePage";
4
- import { Text, TouchableOpacity, View } from "react-native";
5
- import TermsPopup from "../ui/termsPopup";
6
- import { Fonts, textSize } from "../../tokens/typography";
7
- import { useAppTheme } from "../../context/ThemeContext";
2
+ import { ScrollView, Text, TouchableOpacity, View } from "react-native";
8
3
  import { Field } from "../../types/formFields";
9
- import { useFormSubmit } from "../../hooks/useSignup";
10
- import ErrorMessage from "../ui/error";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import { useLanguage } from "../../context/LanguageContext";
6
+ import ScreenHeader from "../ui/PageTopBar";
7
+ import { DynamicForm } from "../ui/dynamicForm";
8
+ import { Fonts, textSize } from "../../tokens/typography";
11
9
  import SharpButton from "../ui/sharpButton";
12
- import { SharpDynamicForm } from "../ui/sharpDynamicForm";
13
- interface SignupProps{
14
- title: string;
15
- subtitle: string;
16
- formState: Record<string, string>;
17
- setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
18
- onSubmit: (formState: Record<string, string>) => Promise<void>;
19
- onLogin: () => void;
20
- errorMessage?: string;
21
- haveAccountLabel: string;
22
- loginLabel: string;
23
- buttonLabel: string;
24
- fields: Field[];
25
- nextLabel: string;
26
- backLabel: string;
27
- generalTerms: string[];
28
- dialogTitle:string;
29
- dialogDescription: string;
30
- dialogButton: string;
31
- t?: (key: string) => string;
10
+ import TermsPopup from "../ui/termsPopup";
11
+ import { useFormSubmit } from "../../hooks/useSignup";
12
+
13
+ interface SignupProps {
14
+ title: string;
15
+ subtitle: string;
16
+ formState: Record<string, string>;
17
+ setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
18
+ onSubmit: (formState: Record<string, string>) => Promise<void>;
19
+ onLogin: () => void;
20
+ errorMessage?: string;
21
+ haveAccountLabel: string;
22
+ loginLabel: string;
23
+ buttonLabel: string;
24
+ fields: Field[];
25
+ nextLabel: string;
26
+ backLabel: string;
27
+ generalTerms: string[];
28
+ dialogTitle: string;
29
+ dialogDescription: string;
30
+ dialogButton: string;
31
+ agreeText?: string; // "By continuing, you agree to our"
32
+ termsLinkText?: string; // "Terms & Privacy"
33
+ t?: (key: string) => string;
32
34
  }
33
- export default function SignupPage({
34
- title,
35
- subtitle,
36
- formState,
37
- setFormState,
38
- onSubmit,
39
- onLogin,
40
- errorMessage,
41
- haveAccountLabel,
42
- loginLabel,
43
- buttonLabel,
44
- fields,
45
- nextLabel,
46
- backLabel,
35
+
36
+ export default function SignUpScreen({
37
+ title,
38
+ formState,
39
+ setFormState,
40
+ onSubmit,
41
+ onLogin,
42
+ haveAccountLabel,
43
+ loginLabel,
44
+ buttonLabel,
45
+ fields,
46
+ nextLabel,
47
+ backLabel,
47
48
  generalTerms,
48
49
  dialogTitle,
49
50
  dialogDescription,
50
51
  dialogButton,
52
+ agreeText = "By continuing, you agree",
53
+ termsLinkText = "Terms & Privacy",
51
54
  t,
52
55
  }: SignupProps) {
53
- const [showTerms, setShowTerms] = useState(true);
54
- const {colors} = useAppTheme();
55
- const [isLastStep, setIsLastStep] = useState(false);
56
+ const [showTerms, setShowTerms] = useState(true);
57
+ const { colors } = useAppTheme();
58
+ const { isRTL } = useLanguage();
56
59
 
57
- const {handleSubmit, errors, clearError, loading, setErrors} = useFormSubmit({
60
+ const { handleSubmit, errors, clearError, loading, setErrors } = useFormSubmit({
58
61
  fields,
59
62
  formState,
60
63
  onSubmit,
61
- });
64
+ });
65
+
62
66
  return (
63
- <TemplatePage
64
- title={title}
65
- subtitle={subtitle}
66
- body = {
67
- <View style={{gap:10}}>
68
- {errorMessage && <ErrorMessage message={errorMessage} position="center" fontSize={textSize.md} />}
69
- <SharpDynamicForm
70
- onStepChange={(step, total) => setIsLastStep(step === total - 1)}
71
- fields={fields}
72
- formState={formState}
73
- setFormState={setFormState}
74
- errors={errors}
75
- clearError={clearError}
76
- nextLabel={nextLabel}
77
- backLabel={backLabel}
78
- setErrors={setErrors}
79
- translator={t}
80
- />
81
- {isLastStep && (
82
- <SharpButton
83
- text={buttonLabel}
84
- bgColor={colors.primary}
85
- onPress={handleSubmit}
86
- loading={loading}
87
- height={50}
88
- />
89
- )}
90
- <TermsPopup
91
- title={dialogTitle}
92
- description={dialogDescription}
93
- buttonText={dialogButton}
94
- showTerms={showTerms}
95
- generalTerms={generalTerms}
96
- onAgree={()=>setShowTerms(false)}/>
97
- </View>
98
- }
99
- footer={
100
- <View style={{flexDirection: "row",
101
- justifyContent: "center",
102
- alignItems: "center",
103
- gap: 4,
104
- marginTop: 15,}}>
105
- <Text style={{fontSize: textSize.sm,
106
- color: colors.text.secondary,
107
- fontFamily: Fonts.montserrat.regular,}}>{haveAccountLabel}</Text>
108
- <TouchableOpacity onPress={onLogin}>
109
- <Text
110
- style={{ fontSize: textSize.sm, fontFamily: Fonts.montserrat.semiBold, color: colors.primary }}
111
- >
112
- {loginLabel}
113
- </Text>
114
- </TouchableOpacity>
115
- </View>
116
- }
117
- marginTop={-150}
118
- />
67
+ <View style={{ flex: 1, paddingHorizontal: 25, paddingTop: 50, paddingBottom: 20 }}>
68
+ {/* Fixed: header */}
69
+ <ScreenHeader title={title} description={""} />
70
+
71
+ {/* Scrollable: everything after the header */}
72
+ <ScrollView
73
+ style={{ flex: 1 }}
74
+ showsVerticalScrollIndicator={false}
75
+ keyboardShouldPersistTaps="handled"
76
+ contentContainerStyle={{ paddingBottom: 20 }}
77
+ >
78
+ <DynamicForm
79
+ fields={fields}
80
+ formState={formState}
81
+ setFormState={setFormState}
82
+ errors={errors}
83
+ clearError={clearError}
84
+ nextLabel={nextLabel}
85
+ backLabel={backLabel}
86
+ setErrors={setErrors}
87
+ translator={t}
88
+ />
89
+
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
+
110
+ <SharpButton
111
+ text={buttonLabel}
112
+ bgColor={colors.primary}
113
+ onPress={handleSubmit}
114
+ loading={loading}
115
+ height={50}
116
+ />
117
+
118
+ <View
119
+ style={{
120
+ flexDirection: isRTL ? "row" : "row-reverse",
121
+ justifyContent: "center",
122
+ alignItems: "center",
123
+ gap: 4,
124
+ marginTop: 15,
125
+ }}
126
+ >
127
+ <Text
128
+ style={{
129
+ fontSize: textSize.xs,
130
+ color: colors.text.secondary,
131
+ fontFamily: Fonts.montserrat.regular,
132
+ }}
133
+ >
134
+ {haveAccountLabel}
135
+ </Text>
136
+ <TouchableOpacity onPress={onLogin}>
137
+ <Text
138
+ style={{
139
+ fontSize: textSize.xs,
140
+ fontFamily: Fonts.montserrat.regular,
141
+ color: colors.primary,
142
+ }}
143
+ >
144
+ {loginLabel}
145
+ </Text>
146
+ </TouchableOpacity>
147
+ </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
+ />
159
+ </View>
119
160
  );
120
161
  }
@@ -1,7 +1,8 @@
1
1
  import { View, Text, StyleSheet, Modal, Animated, Dimensions, ViewStyle } from "react-native";
2
- import { useAppTheme } from "../../context/ThemeContext";
3
- import { textSize, Fonts } from "../../tokens/typography";
4
2
  import { useEffect, useRef } from "react";
3
+ import { Fonts, textSize } from "../../tokens/typography";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import { useLanguage } from "../../context/LanguageContext";
5
6
  import SharpButton from "./sharpButton";
6
7
 
7
8
  const { height } = Dimensions.get("window");
@@ -91,7 +92,7 @@ const handleClose = () => {
91
92
  };
92
93
 
93
94
 
94
-
95
+ const {isRTL} = useLanguage();
95
96
 
96
97
  return (
97
98
  <Modal
@@ -119,6 +120,7 @@ const handleClose = () => {
119
120
  fontSize: textSize.xxl,
120
121
  color: colors.text.primary,
121
122
  marginBottom: 10,
123
+ textAlign:isRTL ? "left" :"right"
122
124
  }}>
123
125
  {title}
124
126
  </Text>
@@ -1,13 +1,12 @@
1
1
  import { useState } from "react";
2
- import { I18nManager, ScrollView, Text, TouchableOpacity, View } from "react-native";
3
- import Input from "./input";
2
+ import { View } from "react-native";
4
3
  import { Field } from "../../types/formFields";
5
- import { useAppTheme } from "../../context/ThemeContext";
6
- import { validateForm } from "../../utils/inputValidator/registerValidator";
7
- import { Fonts, textSize } from "../../tokens/typography";
8
- import {Arrow, ArrowBack} from "@towsila/icons";
4
+ import Input from "./input";
5
+
6
+ type KeyedField = Field & { key?: string };
7
+
9
8
  interface Props {
10
- fields: Field[];
9
+ fields: KeyedField[];
11
10
  formState: Record<string, string>;
12
11
  setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
13
12
  errors: Record<string, string>;
@@ -16,27 +15,7 @@ interface Props {
16
15
  onStepChange?: (step: number, total: number) => void;
17
16
  nextLabel?: string;
18
17
  backLabel?: string;
19
- }
20
- // can i split it into 2 not only move the password if the number of fields is > 4
21
- function splitIntoSteps(fields: Field[]): Field[][] {
22
- const info = fields.filter((f) => f.type !== "password");
23
- const security = fields.filter((f) => f.type === "password");
24
- const steps: Field[][] = [];
25
-
26
- if (info.length > 4) {
27
- const mid = Math.ceil(info.length / 2);
28
- steps.push(info.slice(0, mid));
29
- steps.push(info.slice(mid));
30
- } else if (info.length > 0) {
31
- steps.push(info);
32
- }
33
-
34
- // Add security fields as separate step
35
- if (security.length > 0) {
36
- steps.push(security);
37
- }
38
-
39
- return steps;
18
+ translator?: (key: string) => string;
40
19
  }
41
20
 
42
21
  export const DynamicForm: React.FC<Props> = ({
@@ -44,113 +23,60 @@ export const DynamicForm: React.FC<Props> = ({
44
23
  formState,
45
24
  setFormState,
46
25
  errors,
47
- setErrors,
48
26
  clearError,
49
- onStepChange,
50
- nextLabel,
51
- backLabel,
27
+ translator,
52
28
  }) => {
53
29
  const [showPasswordField, setShowPasswordField] = useState<Record<string, boolean>>({});
54
- const [currentStep, setCurrentStep] = useState(0);
55
- const { colors } = useAppTheme();
56
-
57
- const steps = splitIntoSteps(fields);
58
- const totalSteps = steps.length;
59
- const stepFields = steps[currentStep] ?? [];
60
-
61
- const goNext = () => {
62
- // validate current step fields before proceeding
63
- const newErrors = validateForm(stepFields, formState);
64
- setErrors((prev) => ({ ...prev, ...newErrors }));
65
- if (Object.keys(newErrors).length > 0) return;
66
30
 
67
- const next = Math.min(currentStep + 1, totalSteps - 1);
68
- setCurrentStep(next);
69
- onStepChange?.(next, totalSteps);
70
- };
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
+ }, []);
71
41
 
72
- const goPrev = () => {
73
- const prev = Math.max(currentStep - 1, 0);
74
- setCurrentStep(prev);
75
- onStepChange?.(prev, totalSteps);
76
- };
77
-
78
- const renderInput = (field: Field) => (
79
- <View key={field.name} style={{ marginBottom: 10 }}>
80
- <Input
81
- value={formState[field.name] || ""}
82
- onChangeText={(text) => setFormState((prev) => ({ ...prev, [field.name]: text }))}
83
- iconLeft={field.icon}
84
- iconRight={
85
- field.secureTextEntry
86
- ? showPasswordField[field.name] ? "eye-off-outline" : "eye-outline"
87
- : undefined
88
- }
89
- placeholder={field.placeholder}
90
- onPressIconRight={
91
- field.secureTextEntry
92
- ? () => setShowPasswordField((prev) => ({ ...prev, [field.name]: !prev[field.name] }))
93
- : undefined
94
- }
95
- secureTextEntry={field.secureTextEntry && !showPasswordField[field.name]}
96
- returnKeyType="next"
97
- error={errors[field.name]}
98
- onClearError={() => clearError(field.name)}
99
- label={field.label}
100
- />
101
- </View>
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
+ />
102
64
  );
103
65
 
104
66
  return (
105
- <View style={{ gap: 4, paddingBottom: 20 }}>
106
-
107
- {/* Stepper — only show if more than 1 step */}
108
- {totalSteps > 1 && (
109
- <View style={{ flexDirection: "row", alignItems: "center", marginBottom: 12 }}>
110
- {/* Back arrow */}
111
- <View style={{ flex: 1 }}>
112
- {currentStep > 0 && backLabel && (
113
- <TouchableOpacity onPress={goPrev} style={{ flexDirection: "row", alignItems: "center", justifyContent: "flex-start", gap: 6 }}>
114
- <ArrowBack width={16} height={16} color={colors.text.secondary} style={[I18nManager.isRTL && { transform: [{ scaleX: -1 }] },]}/>
115
- <Text style={{ color: colors.text.secondary, fontFamily: Fonts.montserrat.semiBold, fontSize: textSize.sm }}>
116
- {backLabel}
117
- </Text>
118
- </TouchableOpacity>
119
- )}
120
- </View>
121
-
122
- {/* Pills */}
123
- <View style={{ flexDirection: "row", gap: 8 }}>
124
- {steps.map((_, i) => (
125
- <View key={i} style={{
126
- width: i === currentStep ? 28 : 10,
127
- height: 6,
128
- borderRadius: 3,
129
- backgroundColor: i <= currentStep ? colors.primary : colors.text.placeholder + "40",
130
- }} />
131
- ))}
132
- </View>
133
- <View style={{ flex: 1 }} />
134
- </View>
135
- )}
136
-
137
- {/* Fields */}
138
- {stepFields.map((f) => renderInput(f))}
139
-
140
- {/* Navigation */}
141
- {totalSteps > 1 && (
142
- <View style={{ gap: 10, marginTop: 4 }}>
143
- {currentStep < totalSteps - 1 && (
144
- <TouchableOpacity onPress={goNext} style={{ flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 6 }}>
145
- <Text style={{ color: colors.text.secondary, fontFamily: Fonts.montserrat.semiBold, fontSize: textSize.sm }}>
146
- {nextLabel}
147
- </Text>
148
- <Arrow width={16} height={16} color={colors.text.secondary} style={[I18nManager.isRTL && { transform: [{ scaleX: -1 }] },]}/>
149
- </TouchableOpacity>
150
- )}
151
-
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
+ ))}
152
78
  </View>
153
- )}
79
+ ))}
154
80
  </View>
155
81
  );
156
82
  };
@@ -1,7 +1,8 @@
1
1
  import { ScrollView, Text, View } from "react-native";
2
2
  import CustomPopUp from "./customPopup";
3
- import { Fonts, textSize } from "../../tokens/typography";
4
3
  import { useAppTheme } from "../../context/ThemeContext";
4
+ import { useLanguage } from "../../context/LanguageContext";
5
+ import { Fonts, textSize } from "../../tokens/typography";
5
6
 
6
7
  interface TermsPopupProps{
7
8
  showTerms: boolean;
@@ -13,6 +14,7 @@ interface TermsPopupProps{
13
14
  }
14
15
  export default function TermsPopup({showTerms, generalTerms, onAgree, title, description, buttonText}: TermsPopupProps){
15
16
  const {colors} = useAppTheme();
17
+ const {isRTL} = useLanguage();
16
18
  return (
17
19
  <CustomPopUp
18
20
  visible={showTerms}
@@ -23,6 +25,7 @@ export default function TermsPopup({showTerms, generalTerms, onAgree, title, des
23
25
  fontSize: textSize.sm,
24
26
  color: colors.text.secondary,
25
27
  marginBottom: 4,
28
+ textAlign:isRTL ? "left" :"right"
26
29
  }}>
27
30
  {description}
28
31
  </Text>
@@ -34,7 +37,7 @@ export default function TermsPopup({showTerms, generalTerms, onAgree, title, des
34
37
  showsVerticalScrollIndicator={false}
35
38
  >
36
39
  {generalTerms.map((term: string, i: number) => (
37
- <View key={i} style={{ flexDirection: "row", gap: 8, marginBottom: 8 }}>
40
+ <View key={i} style={{ flexDirection: isRTL ? "row" : "row-reverse", gap: 8, marginBottom: 8 }}>
38
41
  <Text style={{ color: colors.primary }}>•</Text>
39
42
  <Text style={{
40
43
  fontFamily: Fonts.montserrat.regular,
@@ -42,6 +45,7 @@ export default function TermsPopup({showTerms, generalTerms, onAgree, title, des
42
45
  color: colors.text.secondary,
43
46
  flex: 1,
44
47
  lineHeight: 20,
48
+ textAlign:isRTL ? "left" :"right"
45
49
  }}>
46
50
  {term}
47
51
  </Text>