@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,178 @@
1
+ import { Text, TouchableOpacity, View, StyleSheet, ImageSourcePropType, Image } from "react-native";
2
+ import PageIndicator from "../ui/pageIndicator";
3
+ import Button from "../ui/button";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import { Fonts, textSize } from "../../tokens/typography";
6
+ import Arrow from "../../assets/customIcons/arrow.svg";
7
+ import ArrowDark from "../../assets/customIcons/arrow_dark.svg";
8
+ import SharpButton from "../ui/sharpButton";
9
+
10
+ interface OnBoardingProps {
11
+ onSkip: () => void;
12
+ onNext: () => void;
13
+ image: React.ReactNode;
14
+ title: string;
15
+ description: string;
16
+ skipLabel: string;
17
+ nextLabel: string;
18
+ currentIndex: number;
19
+ total: number;
20
+ }
21
+
22
+ export default function OnBoarding({
23
+ onSkip,
24
+ onNext,
25
+ image,
26
+ title,
27
+ description,
28
+ skipLabel,
29
+ nextLabel,
30
+ currentIndex,
31
+ total,
32
+ }: OnBoardingProps) {
33
+ const { colors, isDark } = useAppTheme();
34
+
35
+ return (
36
+ <View
37
+ style={[
38
+ styles.container,
39
+ { backgroundColor: colors.background.main },
40
+ ]}
41
+ >
42
+ {/* ---------- Header ---------- */}
43
+ <View style={styles.header}>
44
+ <TouchableOpacity onPress={onSkip} style={styles.skipButton}>
45
+ <Text
46
+ style={[
47
+ styles.skipLabel,
48
+ { color: colors.text.primary },
49
+ ]}
50
+ >
51
+ {skipLabel}
52
+ </Text>
53
+ <Arrow width={20} height={20} color={colors.text.primary} style={styles.skipIcon}/>
54
+ </TouchableOpacity>
55
+ </View>
56
+
57
+ {/* ---------- Content ---------- */}
58
+ <View style={styles.content}>
59
+ {/* Image */}
60
+ <View style={styles.imageContainer}>
61
+ {image}
62
+ </View>
63
+
64
+ {/* Text */}
65
+ <View style={styles.textContainer}>
66
+ <Text
67
+ style={[
68
+ styles.title,
69
+ { color: colors.text.primary },
70
+ ]}
71
+ >
72
+ {title}
73
+ </Text>
74
+
75
+ <Text
76
+ style={[
77
+ styles.description,
78
+ { color: colors.text.secondary },
79
+ ]}
80
+ >
81
+ {description}
82
+ </Text>
83
+
84
+ <View style={styles.indicatorContainer}>
85
+ <PageIndicator
86
+ total={total}
87
+ current={currentIndex}
88
+ color={colors.primary}
89
+ borderColor={colors.text.primary}
90
+ defaultColor={colors.background.main}
91
+ />
92
+ </View>
93
+ </View>
94
+ </View>
95
+
96
+ {/* ---------- Footer ---------- */}
97
+ <View style={styles.footer}>
98
+ <SharpButton
99
+ text={nextLabel}
100
+ bgColor={colors.primary}
101
+ onPress={onNext}
102
+ />
103
+ </View>
104
+ </View>
105
+ );
106
+ }
107
+
108
+ const styles = StyleSheet.create({
109
+ container: {
110
+ flex: 1,
111
+ paddingHorizontal: 32,
112
+ paddingTop: 50,
113
+ paddingBottom: 40,
114
+ },
115
+
116
+ header: {
117
+ alignItems: "flex-end",
118
+ minHeight: 40,
119
+ },
120
+
121
+ skipButton: {
122
+ flexDirection: "row",
123
+ alignItems: "center",
124
+ paddingVertical: 8,
125
+ },
126
+
127
+ skipLabel: {
128
+ fontSize: textSize.md,
129
+ fontFamily: Fonts.montserrat.regular,
130
+ },
131
+
132
+ skipIcon: {
133
+ marginLeft: 6,
134
+ },
135
+
136
+ content: {
137
+ flex: 1,
138
+ justifyContent: "center",
139
+ },
140
+
141
+ imageContainer: {
142
+ flex: 0.58,
143
+ justifyContent: "center",
144
+ alignItems: "center",
145
+ overflow: "hidden",
146
+ },
147
+
148
+ textContainer: {
149
+ flex: 0.42,
150
+ justifyContent: "center",
151
+ alignItems: "center",
152
+ paddingHorizontal: 12,
153
+ },
154
+
155
+ title: {
156
+ fontSize: textSize.xxl,
157
+ fontFamily: Fonts.montserrat.bold,
158
+ textAlign: "center",
159
+ marginBottom: 14,
160
+ },
161
+
162
+ description: {
163
+ fontSize: textSize.sm,
164
+ fontFamily: Fonts.montserrat.regular,
165
+ textAlign: "center",
166
+ lineHeight: 22,
167
+ marginBottom: 28,
168
+ paddingHorizontal: 10,
169
+ },
170
+
171
+ indicatorContainer: {
172
+ marginTop: 4,
173
+ },
174
+
175
+ footer: {
176
+ paddingTop: 24,
177
+ },
178
+ });
@@ -0,0 +1,115 @@
1
+ import TemplateSubPage from "../reusable/templateSubPage";
2
+ import Input from "../ui/input";
3
+ import { useResetPasswordForm } from "../../hooks/useResetPasswordForm";
4
+ import PasswordRequirementsCard from "../ui/passwordRequirmentCard";
5
+ import SharpInput from "../ui/sharpInput";
6
+ import LockIcon from "../../assets/customIcons/lock_closed.svg";
7
+ import EyeOffIcon from "../../assets/customIcons/eye_off.svg";
8
+ import EyeIcon from "../../assets/customIcons/eye.svg";
9
+
10
+
11
+ interface ResetPasswordScreenProps {
12
+ title?: string;
13
+ subtitle?: string;
14
+ buttonText?: string;
15
+ passwordLabel?: string;
16
+ confirmPasswordLabel?: string;
17
+ passwordPlaceholder?: string;
18
+ confirmPasswordPlaceholder?: string;
19
+ cardTitle?:string,
20
+ cardItems?: string[],
21
+ onBack: () => void;
22
+ oldPasswordLabel?: string;
23
+ oldPasswordPlaceholder?: string;
24
+ includeOldPassword?: boolean;
25
+ onReset: (password: string, oldPassword?: string) => Promise<void>;
26
+ errorMessage?: string;
27
+ }
28
+
29
+ export default function ResetPasswordScreen({
30
+ title = "Reset Password",
31
+ subtitle = "Enter your new password below.",
32
+ buttonText = "Save",
33
+ passwordLabel = "Password",
34
+ confirmPasswordLabel = "Confirm password",
35
+ passwordPlaceholder = "Enter your new password",
36
+ confirmPasswordPlaceholder = "Re-enter your new password",
37
+ cardTitle,
38
+ cardItems,
39
+ onBack,
40
+ oldPasswordLabel = "Current Password",
41
+ oldPasswordPlaceholder = "Enter your current password",
42
+ includeOldPassword = false,
43
+ onReset,
44
+ errorMessage,
45
+ }: ResetPasswordScreenProps) {
46
+ const form = useResetPasswordForm({ onReset , includeOldPassword});
47
+
48
+ return (
49
+ <TemplateSubPage
50
+ title={title}
51
+ text={subtitle}
52
+ buttonText={buttonText}
53
+ handleFunction={form.handleSubmit}
54
+ error={errorMessage}
55
+ onBack={onBack}
56
+ loading={form.loading}
57
+ >
58
+ {includeOldPassword && (
59
+ <SharpInput
60
+ label={oldPasswordLabel}
61
+ value={form.oldPassword}
62
+ onChangeText={form.setOldPassword}
63
+ placeholder={oldPasswordPlaceholder}
64
+ iconLeft={(color) => <LockIcon width={18} height={18} color={color} />}
65
+ iconRight={form.showOldPassword
66
+ ? (color) => <EyeOffIcon width={18} height={18} color={color} />
67
+ : (color) => <EyeIcon width={18} height={18} color={color} />
68
+ }
69
+ onPressIconRight={() => form.setShowOldPassword(!form.showOldPassword)}
70
+ secureTextEntry={!form.showOldPassword}
71
+ textContentType="password"
72
+ returnKeyType="next"
73
+ error={form.errors.oldPassword}
74
+ onClearError={() => form.clearError("oldPassword")}
75
+ />
76
+ )}
77
+ <SharpInput
78
+ label={passwordLabel}
79
+ value={form.password}
80
+ onChangeText={form.setPassword}
81
+ placeholder={passwordPlaceholder}
82
+ iconLeft={(color) => <LockIcon width={18} height={18} color={color}/>}
83
+ iconRight={form.showPassword ? (color)=><EyeOffIcon width={18} height={18} color={color}/> : (color)=><EyeIcon width={18} height={18} color={color}/>}
84
+ onPressIconRight={() => form.setShowPassword(!form.showPassword)}
85
+ secureTextEntry={!form.showPassword}
86
+ textContentType="password"
87
+ returnKeyType="next"
88
+ error={form.errors.password}
89
+ onClearError={() => form.clearError("password")}
90
+ />
91
+ {cardItems && (
92
+ <PasswordRequirementsCard
93
+ checks={form.passwordChecks}
94
+ cardTitle={cardTitle}
95
+ cardItems = {cardItems}
96
+ />
97
+ )}
98
+
99
+ <SharpInput
100
+ label={confirmPasswordLabel}
101
+ value={form.confirmPassword}
102
+ onChangeText={form.setConfirmPassword}
103
+ placeholder={confirmPasswordPlaceholder}
104
+ iconLeft={(color) => <LockIcon width={18} height={18} color={color}/>}
105
+ iconRight={form.showConfirmPassword ? (color)=><EyeOffIcon width={18} height={18} color={color}/> : (color)=><EyeIcon width={18} height={18} color={color}/>}
106
+ onPressIconRight={() => form.setShowConfirmPassword(!form.showConfirmPassword)}
107
+ secureTextEntry={!form.showConfirmPassword}
108
+ textContentType="password"
109
+ returnKeyType="done"
110
+ error={form.errors.confirmPassword}
111
+ onClearError={() => form.clearError("confirmPassword")}
112
+ />
113
+ </TemplateSubPage>
114
+ );
115
+ }
@@ -0,0 +1,85 @@
1
+ import {Settings } from "../../types/settingsFields"
2
+ import { useAppTheme } from "../../context/ThemeContext";
3
+ import { Text, View, TouchableOpacity, ScrollView } from "react-native"
4
+ import { textSize, Fonts } from "../../tokens/typography"
5
+ import SharpButton from "../ui/sharpButton";
6
+ import TemplatePage from "../reusable/templatePage";
7
+ import { SharpContainer } from "../..";
8
+ import Arrow from "../../assets/customIcons/arrow.svg";
9
+
10
+ interface Props{
11
+ title: string;
12
+ subtitle: string;
13
+ onNavigate: (link: string) => void;
14
+ deleteLabel?:string;
15
+ fields: Settings[];
16
+ onDelete:()=>void;
17
+ }
18
+
19
+ export default function SettingsScreen({title, subtitle, onNavigate, deleteLabel="", fields, onDelete}: Props) {
20
+ const { colors } = useAppTheme();
21
+
22
+ return (
23
+ <TemplatePage title={title} subtitle={subtitle} marginTop={-150} body={
24
+ <View style={{
25
+ gap:12
26
+ }}>
27
+ {fields.map((item) => (
28
+ <SharpContainer key={item.id} height={56} bgColor={colors.background.main} borderColor={colors.semantic.border}>
29
+ <TouchableOpacity
30
+ onPress={() => item.route && onNavigate(item.route)}
31
+ activeOpacity={0.65}
32
+ style={{
33
+ flexDirection: "row",
34
+ justifyContent: "space-between",
35
+ alignItems: "center",
36
+ flex: 1,
37
+ paddingHorizontal: 15
38
+ }}
39
+ >
40
+ <View
41
+ style={{
42
+ flexDirection: "row",
43
+ alignItems: "center",
44
+ gap: 4,
45
+ }}
46
+ >
47
+ <View
48
+ style={{
49
+ width: 36,
50
+ height: 36,
51
+ borderRadius: 10,
52
+ alignItems: "center",
53
+ justifyContent: "center",
54
+ }}
55
+ >
56
+ {item.iconLeft}
57
+ </View>
58
+
59
+ <Text
60
+ style={{
61
+ fontFamily: Fonts.montserrat.semiBold,
62
+ color: colors.text.primary,
63
+ fontSize: textSize.sm,
64
+ }}
65
+ >
66
+ {item.title}
67
+ </Text>
68
+ </View>
69
+
70
+ <Arrow width={20} height={20} color={colors.primary} />
71
+ </TouchableOpacity>
72
+ </SharpContainer>
73
+ ))}
74
+ </View>}
75
+ footer={
76
+ <View style={{marginTop: 30, paddingHorizontal:30 }}>
77
+ <SharpButton
78
+ text={deleteLabel}
79
+ bgColor={colors.semantic.error}
80
+ onPress={onDelete}
81
+ />
82
+ </View>
83
+ }/>
84
+ );
85
+ }
@@ -0,0 +1,119 @@
1
+ // import { useRouter } from "expo-router";
2
+ import React, { useState, useEffect } 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";
8
+ import Button from "../ui/button";
9
+ import { DynamicForm } from "../ui/dynamicForm";
10
+ import { Field } from "../../types/formFields";
11
+ import { useFormSubmit } from "../../hooks/useSignup";
12
+ import ErrorMessage from "../ui/error";
13
+ import SharpButton from "../ui/sharpButton";
14
+ import { SharpDynamicForm } from "../ui/sharpDynamicForm";
15
+ interface SignupProps{
16
+ title: string;
17
+ subtitle: string;
18
+ formState: Record<string, string>;
19
+ setFormState: React.Dispatch<React.SetStateAction<Record<string, string>>>;
20
+ onSubmit: (formState: Record<string, string>) => Promise<void>;
21
+ onLogin: () => void;
22
+ errorMessage?: string;
23
+ haveAccountLabel: string;
24
+ loginLabel: string;
25
+ buttonLabel: string;
26
+ fields: Field[];
27
+ nextLabel: string;
28
+ backLabel: string;
29
+ generalTerms: string[];
30
+ dialogTitle:string;
31
+ dialogDescription: string;
32
+ dialogButton: string;
33
+ }
34
+ export default function SignupPage({
35
+ title,
36
+ subtitle,
37
+ formState,
38
+ setFormState,
39
+ onSubmit,
40
+ onLogin,
41
+ errorMessage,
42
+ haveAccountLabel,
43
+ loginLabel,
44
+ buttonLabel,
45
+ fields,
46
+ nextLabel,
47
+ backLabel,
48
+ generalTerms,
49
+ dialogTitle,
50
+ dialogDescription,
51
+ dialogButton
52
+ }: SignupProps) {
53
+ const [showTerms, setShowTerms] = useState(true);
54
+ const {colors} = useAppTheme();
55
+ const [isLastStep, setIsLastStep] = useState(false);
56
+
57
+ const {handleSubmit, errors, clearError, loading, setErrors} = useFormSubmit({
58
+ fields,
59
+ formState,
60
+ onSubmit,
61
+ });
62
+ 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
+ />
80
+ {isLastStep && (
81
+ <SharpButton
82
+ text={buttonLabel}
83
+ bgColor={colors.primary}
84
+ onPress={handleSubmit}
85
+ loading={loading}
86
+ height={50}
87
+ />
88
+ )}
89
+ <TermsPopup
90
+ title={dialogTitle}
91
+ description={dialogDescription}
92
+ buttonText={dialogButton}
93
+ showTerms={showTerms}
94
+ generalTerms={generalTerms}
95
+ onAgree={()=>setShowTerms(false)}/>
96
+ </View>
97
+ }
98
+ footer={
99
+ <View style={{flexDirection: "row",
100
+ justifyContent: "center",
101
+ alignItems: "center",
102
+ gap: 4,
103
+ marginTop: 15,}}>
104
+ <Text style={{fontSize: textSize.sm,
105
+ color: colors.text.secondary,
106
+ fontFamily: Fonts.montserrat.regular,}}>{haveAccountLabel}</Text>
107
+ <TouchableOpacity onPress={onLogin}>
108
+ <Text
109
+ style={{ fontSize: textSize.sm, fontFamily: Fonts.montserrat.semiBold, color: colors.primary }}
110
+ >
111
+ {loginLabel}
112
+ </Text>
113
+ </TouchableOpacity>
114
+ </View>
115
+ }
116
+ marginTop={-150}
117
+ />
118
+ );
119
+ }
@@ -0,0 +1,170 @@
1
+ import { useEffect } from "react";
2
+ import { View, Text, StyleSheet, Dimensions } from "react-native";
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withTiming,
7
+ withSequence,
8
+ withDelay,
9
+ withSpring,
10
+ Easing,
11
+ } from "react-native-reanimated";
12
+ import * as SplashScreen from "expo-splash-screen";
13
+ import { Fonts, textSize } from "../../tokens/typography";
14
+ import { useAppTheme } from "../../context/ThemeContext";
15
+
16
+ const { width } = Dimensions.get("window");
17
+
18
+ interface SplashScreenProps {
19
+ speedLines: React.ReactNode; // static background element
20
+ hook: React.ReactNode; // animated element
21
+ logoText: React.ReactNode; // title SVG
22
+ subtitle: string; // subtitle text
23
+ onFinish: () => void; // called when animation ends, app decides where to go
24
+ duration?: number; // total animation duration before onFinish, default 2800
25
+ hookStartX?: number; // hook entry X offset, default -80
26
+ hookStartY?: number; // hook entry Y offset, default -180
27
+ }
28
+
29
+ export default function AnimatedSplashScreen({
30
+ speedLines,
31
+ hook,
32
+ logoText,
33
+ subtitle,
34
+ onFinish,
35
+ duration = 2800,
36
+ hookStartX = -80,
37
+ hookStartY = -180,
38
+ }: SplashScreenProps) {
39
+ const { colors } = useAppTheme();
40
+
41
+ const hookY = useSharedValue(hookStartY);
42
+ const hookX = useSharedValue(hookStartX);
43
+ const hookRotate = useSharedValue(25);
44
+ const hookOpacity = useSharedValue(0);
45
+
46
+ const titleOpacity = useSharedValue(0);
47
+ const titleY = useSharedValue(24);
48
+ const subtitleOpacity = useSharedValue(0);
49
+ const subtitleY = useSharedValue(16);
50
+
51
+ useEffect(() => {
52
+ SplashScreen.hideAsync();
53
+
54
+ hookOpacity.value = withTiming(1, { duration: 100 });
55
+
56
+ hookX.value = withSequence(
57
+ withTiming(20, { duration: 600, easing: Easing.out(Easing.cubic) }),
58
+ withSpring(0, { damping: 5, stiffness: 80 })
59
+ );
60
+
61
+ hookY.value = withTiming(0, {
62
+ duration: 600,
63
+ easing: Easing.out(Easing.cubic),
64
+ });
65
+
66
+ hookRotate.value = withSequence(
67
+ withTiming(-5, { duration: 600, easing: Easing.out(Easing.cubic) }),
68
+ withSpring(8, { damping: 3, stiffness: 50 }),
69
+ withSpring(-5, { damping: 4, stiffness: 60 }),
70
+ withSpring(3, { damping: 5, stiffness: 80 }),
71
+ withSpring(0, { damping: 8, stiffness: 100 })
72
+ );
73
+
74
+ titleOpacity.value = withDelay(800, withTiming(1, { duration: 400 }));
75
+ titleY.value = withDelay(800, withTiming(0, {
76
+ duration: 400,
77
+ easing: Easing.out(Easing.quad),
78
+ }));
79
+
80
+ subtitleOpacity.value = withDelay(1100, withTiming(1, { duration: 400 }));
81
+ subtitleY.value = withDelay(1100, withTiming(0, {
82
+ duration: 400,
83
+ easing: Easing.out(Easing.quad),
84
+ }));
85
+
86
+ const timeout = setTimeout(() => {
87
+ onFinish();
88
+ }, duration);
89
+
90
+ return () => clearTimeout(timeout);
91
+ }, []);
92
+
93
+ const hookStyle = useAnimatedStyle(() => ({
94
+ opacity: hookOpacity.value,
95
+ transform: [
96
+ { translateX: hookX.value },
97
+ { translateY: hookY.value },
98
+ { rotate: `${hookRotate.value}deg` },
99
+ ],
100
+ }));
101
+
102
+ const titleStyle = useAnimatedStyle(() => ({
103
+ opacity: titleOpacity.value,
104
+ transform: [{ translateY: titleY.value }],
105
+ }));
106
+
107
+ const subtitleStyle = useAnimatedStyle(() => ({
108
+ opacity: subtitleOpacity.value,
109
+ transform: [{ translateY: subtitleY.value }],
110
+ }));
111
+
112
+ return (
113
+ <View style={[styles.container, { backgroundColor: colors.background.main }]}>
114
+ <View style={styles.imageContainer}>
115
+ {/* Static background element */}
116
+ {speedLines}
117
+
118
+ {/* Animated hook */}
119
+ <Animated.View style={[styles.hookWrapper, hookStyle]}>
120
+ {hook}
121
+ </Animated.View>
122
+ </View>
123
+
124
+ <View style={styles.textBlock}>
125
+ <Animated.View style={titleStyle}>
126
+ <View style={{ height: 55, overflow: "hidden", justifyContent: "center" }}>
127
+ {logoText}
128
+ </View>
129
+ </Animated.View>
130
+
131
+ <Animated.View style={subtitleStyle}>
132
+ <Text style={[styles.subtitle, { color: colors.text.primary }]}>
133
+ {subtitle}
134
+ </Text>
135
+ </Animated.View>
136
+ </View>
137
+ </View>
138
+ );
139
+ }
140
+
141
+ const styles = StyleSheet.create({
142
+ container: {
143
+ flex: 1,
144
+ alignItems: "center",
145
+ justifyContent: "center",
146
+ paddingHorizontal: 32,
147
+ },
148
+ imageContainer: {
149
+ width: "100%",
150
+ alignItems: "center",
151
+ justifyContent: "center",
152
+ position: "relative",
153
+ marginBottom: 16,
154
+ },
155
+ hookWrapper: {
156
+ position: "absolute",
157
+ left: "15%",
158
+ top: "20%",
159
+ },
160
+ textBlock: {
161
+ alignItems: "center",
162
+ gap: 4,
163
+ marginTop: 8,
164
+ },
165
+ subtitle: {
166
+ fontSize: textSize.sm,
167
+ fontFamily: Fonts.montserrat.semiBold,
168
+ textAlign: "center",
169
+ },
170
+ });