@towsila/expo-ui-library 3.5.1 → 3.5.3
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,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
|
|
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 {
|
|
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,
|
|
64
|
-
{/* Fixed:
|
|
65
|
-
<
|
|
67
|
+
<View style={{ flex: 1, paddingHorizontal: 25, paddingTop: 50, paddingBottom: 20 }}>
|
|
68
|
+
{/* Fixed: header */}
|
|
69
|
+
<ScreenHeader title={title} description={""} />
|
|
66
70
|
|
|
67
|
-
{/* Scrollable:
|
|
68
|
-
<
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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:
|
|
120
|
+
flexDirection: isRTL ? "row" : "row-reverse",
|
|
121
|
+
justifyContent: "center",
|
|
105
122
|
alignItems: "center",
|
|
106
123
|
gap: 4,
|
|
107
|
-
marginTop:
|
|
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.
|
|
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
|
-
</
|
|
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,10 +1,10 @@
|
|
|
1
1
|
import { Text, View } from "react-native";
|
|
2
|
-
import AnimatedSuccessIcon from "../ui/AnimatedSuccessIcon";
|
|
3
2
|
import { useAppTheme } from "../../context/ThemeContext";
|
|
4
3
|
import { useLanguage } from "../../context/LanguageContext";
|
|
5
|
-
import ScreenHeader from "../ui/PageTopBar";
|
|
6
|
-
import SharpButton from "../ui/sharpButton";
|
|
7
4
|
import { Fonts, textSize } from "../../tokens/typography";
|
|
5
|
+
import AnimatedSuccessIcon from "../ui/AnimatedSuccessIcon";
|
|
6
|
+
import SharpButton from "../ui/sharpButton";
|
|
7
|
+
|
|
8
8
|
|
|
9
9
|
interface SuccessScreenProps {
|
|
10
10
|
title?: string;
|
|
@@ -29,10 +29,17 @@ export default function SuccessScreen({
|
|
|
29
29
|
const {isRTL} = useLanguage();
|
|
30
30
|
return (
|
|
31
31
|
<View style={{ flex: 1, paddingHorizontal: 32, paddingVertical: 60 }}>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
<Text style={{color: colors.text.primary, textAlign: isRTL ? "left" : "right", fontFamily: Fonts.montserrat.semiBold, fontSize: textSize.xxl}}>{title}</Text>
|
|
33
|
+
{subtitle && (
|
|
34
|
+
<Text
|
|
35
|
+
style={{fontSize: textSize.md, fontFamily: Fonts.montserrat.regular, color: colors.text.secondary, textAlign: isRTL ? "left" : "right"}}
|
|
36
|
+
>
|
|
37
|
+
{subtitle}
|
|
38
|
+
</Text>
|
|
39
|
+
)}
|
|
40
|
+
<View style={{ flex: 1, marginTop: 70, alignItems: isRTL ? "flex-start" : "flex-end", gap: 20 }}>
|
|
41
|
+
<View style={{ backgroundColor: colors.primary + "30", padding: 25, borderRadius: 30 }}>
|
|
42
|
+
<AnimatedSuccessIcon color={colors.primary} size={37} strokeWidth={2} />
|
|
36
43
|
</View>
|
|
37
44
|
<Text style={{ color: colors.text.secondary, textAlign: isRTL ? "left" : "right", fontFamily: Fonts.montserrat.regular, fontSize: textSize.md }}>
|
|
38
45
|
{description}
|
|
@@ -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:
|
|
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
|
|
31
|
-
|
|
32
|
-
<View
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
};
|