@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,97 @@
1
+ import {
2
+ View,
3
+ Text,
4
+ StyleSheet,
5
+ } from "react-native";
6
+ import { useAppTheme } from "../../context/ThemeContext";
7
+ import { textSize, Fonts } from "../../tokens/typography";
8
+ import CustomPopUp from "./customPopup";
9
+
10
+ interface SuccessDialogProps {
11
+ visible: boolean;
12
+ title: string;
13
+ description: string;
14
+ buttonText?: string;
15
+ onButtonPress?: () => void;
16
+ image?: React.ReactNode;
17
+ }
18
+
19
+ export default function SuccessDialog({
20
+ visible,
21
+ title,
22
+ description,
23
+ buttonText,
24
+ onButtonPress,
25
+ image,
26
+ }: SuccessDialogProps) {
27
+ const { colors } = useAppTheme();
28
+ const styles = createStyles(colors);
29
+
30
+ return (
31
+ <CustomPopUp
32
+ visible={visible}
33
+ body={
34
+ <>
35
+ {image && <View style={styles.imageWrapper}>{image}</View>}
36
+ <Text style={styles.title}>{title}</Text>
37
+ <Text style={styles.description}>{description}</Text>
38
+ </>
39
+ }
40
+ agreeText={buttonText}
41
+ onAgree={onButtonPress}
42
+ />
43
+ );
44
+
45
+ }
46
+
47
+ const createStyles = (colors: any) =>
48
+ StyleSheet.create({
49
+ overlay: {
50
+ flex: 1,
51
+ justifyContent: "flex-end",
52
+ },
53
+ backdrop: {
54
+ ...StyleSheet.absoluteFill,
55
+ backgroundColor: "rgba(0,0,0,0.45)",
56
+ },
57
+ sheet: {
58
+ width: "100%",
59
+ minHeight: "50%",
60
+ backgroundColor: colors.background.card,
61
+ borderTopLeftRadius: 10,
62
+ borderTopRightRadius: 10,
63
+ borderBottomLeftRadius: 0,
64
+ borderBottomRightRadius: 0,
65
+ paddingHorizontal: 24,
66
+ paddingTop: 15,
67
+ paddingBottom: 36,
68
+ // alignItems: "center",
69
+ justifyContent: "center",
70
+ },
71
+ handle: {
72
+ width: 40,
73
+ height: 4,
74
+ borderRadius: 2,
75
+ backgroundColor: colors.text.placeholder,
76
+ opacity: 0.35,
77
+ alignSelf: "center",
78
+ },
79
+ imageWrapper: {
80
+ alignSelf: "center",
81
+ marginBottom: 20,
82
+ },
83
+ title: {
84
+ fontSize: textSize.xxl,
85
+ fontFamily: Fonts.montserrat.semiBold,
86
+ color: colors.text.primary,
87
+ textAlign: "center",
88
+ // marginBottom: 8,
89
+ },
90
+ description: {
91
+ fontSize: textSize.md,
92
+ fontFamily: Fonts.montserrat.regular,
93
+ color: colors.text.secondary,
94
+ textAlign: "center",
95
+ // marginBottom: 24,
96
+ },
97
+ });
@@ -0,0 +1,56 @@
1
+ import { ScrollView, Text, View } from "react-native";
2
+ import CustomPopUp from "./customPopup";
3
+ import { Fonts, textSize } from "../../tokens/typography";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+
6
+ interface TermsPopupProps{
7
+ showTerms: boolean;
8
+ generalTerms: string[];
9
+ onAgree: ()=>void;
10
+ title:string;
11
+ description: string;
12
+ buttonText: string;
13
+ }
14
+ export default function TermsPopup({showTerms, generalTerms, onAgree, title, description, buttonText}: TermsPopupProps){
15
+ const {colors} = useAppTheme();
16
+ return (
17
+ <CustomPopUp
18
+ visible={showTerms}
19
+ title={title}
20
+ header={
21
+ <Text style={{
22
+ fontFamily: Fonts.montserrat.regular,
23
+ fontSize: textSize.sm,
24
+ color: colors.text.secondary,
25
+ marginBottom: 4,
26
+ }}>
27
+ {description}
28
+ </Text>
29
+ }
30
+ body={
31
+ <ScrollView
32
+ style={{ maxHeight: 280 }}
33
+ scrollEventThrottle={16}
34
+ showsVerticalScrollIndicator={false}
35
+ >
36
+ {generalTerms.map((term: string, i: number) => (
37
+ <View key={i} style={{ flexDirection: "row", gap: 8, marginBottom: 8 }}>
38
+ <Text style={{ color: colors.primary }}>•</Text>
39
+ <Text style={{
40
+ fontFamily: Fonts.montserrat.regular,
41
+ fontSize: textSize.sm,
42
+ color: colors.text.secondary,
43
+ flex: 1,
44
+ lineHeight: 20,
45
+ }}>
46
+ {term}
47
+ </Text>
48
+ </View>
49
+ ))}
50
+ </ScrollView>
51
+ }
52
+ agreeText={buttonText}
53
+ onAgree={onAgree}
54
+ />
55
+ )
56
+ }
@@ -0,0 +1,98 @@
1
+ import { useRef, useEffect } from "react";
2
+ import { Animated, TouchableOpacity, View, Text } from "react-native";
3
+ import { useAppTheme } from "../../context/ThemeContext";
4
+ import LightModeIcon from "../../assets/customIcons/light_mode.svg";
5
+ import DarkModeIcon from "../../assets/customIcons/dark_mode.svg";
6
+ import { Fonts, textSize } from "../../tokens/typography";
7
+ import Svg, { Path } from "react-native-svg";
8
+
9
+ interface TextTheme {
10
+ label?: string;
11
+ }
12
+
13
+ const TRACK_W = 52;
14
+ const TRACK_H = 30;
15
+ const THUMB = 22;
16
+ const MARGIN = 4;
17
+ const TRAVEL = TRACK_W - THUMB - MARGIN * 2;
18
+
19
+ export default function ThemeToggle({ label }: TextTheme) {
20
+ const { isDark, toggleTheme, colors } = useAppTheme();
21
+ const translateX = useRef(new Animated.Value(isDark ? TRAVEL : 0)).current;
22
+
23
+ useEffect(() => {
24
+ Animated.spring(translateX, {
25
+ toValue: isDark ? TRAVEL : 0,
26
+ useNativeDriver: true,
27
+ bounciness: 6,
28
+ }).start();
29
+ }, [isDark]);
30
+
31
+ return (
32
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
33
+ {isDark
34
+ ? <DarkModeIcon width={22} height={22} color={colors.text.primary} />
35
+ : <LightModeIcon width={24} height={24} color={colors.text.primary} />
36
+ }
37
+
38
+ {label && (
39
+ <Text style={{
40
+ fontFamily: Fonts.montserrat.regular,
41
+ fontSize: textSize.md,
42
+ color: colors.text.primary,
43
+
44
+ }}>
45
+ {label}
46
+ </Text>
47
+ )}
48
+
49
+ <TouchableOpacity onPress={toggleTheme} activeOpacity={0.9}>
50
+ <View style={{ width: TRACK_W, height: TRACK_H }}>
51
+ {/* Track SVG — fill + border */}
52
+ <Svg
53
+ width={TRACK_W}
54
+ height={TRACK_H}
55
+ viewBox="0 0 58 34"
56
+ style={{ position: "absolute" }}
57
+ >
58
+ {/* Fill */}
59
+ <Path
60
+ d="M0.576172 16.5552C0.576172 7.71862 7.73962 0.555176 16.5762 0.555176H40.5762C49.4127 0.555176 56.5762 7.71862 56.5762 16.5552C56.5762 25.3917 49.4127 32.5552 40.5762 32.5552H16.5762C7.73961 32.5552 0.576172 25.3917 0.576172 16.5552Z"
61
+ fill={colors.primary}
62
+ />
63
+ {/* Border */}
64
+ <Path
65
+ d="M16.5762 0.555176C19.0518 0.621842 21.5273 0.685176 24.0029 0.745176C29.5273 0.87907 35.0518 0.996364 40.5762 1.09706C48.8146 1.06558 55.9659 8.51881 55.6907 16.5552C55.6907 16.5552 55.6907 16.5552 55.6907 16.5552C55.8007 24.604 48.5576 31.7464 40.5762 31.5552C32.5762 31.5552 24.5762 31.59 16.5762 31.6596C8.54491 31.9309 1.10353 24.7824 1.13849 16.5552C1.13849 16.5552 1.13849 16.5552 1.13849 16.5552C0.96865 10.834 4.30374 5.12979 9.49713 2.41467C11.6569 1.26135 14.098 0.618935 16.5762 0.555176C14.0986 0.48538 11.5935 1.00216 9.32675 2.07501C3.8814 4.58411 0.0515389 10.4081 0.0138575 16.5552C0.0138575 16.5552 0.0138575 16.5552 0.0138575 16.5552C-0.365635 25.3731 7.56226 33.5939 16.5762 33.4507C24.5762 33.5204 32.5762 33.5552 40.5762 33.5552C40.5762 33.5552 40.5762 33.5552 40.5762 33.5552C49.6399 33.7784 57.7661 25.5516 57.4616 16.5552C57.4616 16.5552 57.4616 16.5552 57.4616 16.5552C57.6009 7.54638 49.383 -0.369685 40.5762 0.0132933C35.0518 0.113988 29.5273 0.231282 24.0029 0.365176C21.5273 0.425176 19.0518 0.488509 16.5762 0.555176ZM16.5762 0.555176"
66
+ fill={colors.text.primary}
67
+ />
68
+ </Svg>
69
+
70
+ {/* Thumb */}
71
+ <Animated.View style={{
72
+ position: "absolute",
73
+ top: MARGIN,
74
+ left: MARGIN,
75
+ width: THUMB,
76
+ height: THUMB,
77
+ borderRadius: THUMB / 2,
78
+ backgroundColor: colors.text.primary,
79
+ transform: [{ translateX }],
80
+ overflow: "hidden",
81
+ justifyContent: "center",
82
+ alignItems: "center",
83
+ shadowColor: "#000",
84
+ shadowOffset: { width: 0, height: 2 },
85
+ shadowOpacity: 0.25,
86
+ shadowRadius: 3,
87
+ elevation: 4,
88
+ }}>
89
+ {isDark
90
+ ? <LightModeIcon width={20} height={20} color={colors.background.main} />
91
+ : <DarkModeIcon width={16} height={16} color={colors.background.main} />
92
+ }
93
+ </Animated.View>
94
+ </View>
95
+ </TouchableOpacity>
96
+ </View>
97
+ );
98
+ }
@@ -0,0 +1,163 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import {
3
+ Animated,
4
+ Modal,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ } from "react-native";
9
+ import { useAppTheme } from "../../context/ThemeContext";
10
+ import { Fonts, textSize } from "../../tokens/typography";
11
+ import CheckMarkIcon from "../../assets/customIcons/checkmark.svg";
12
+ import WarningIcon from "../../assets/customIcons/warning.svg";
13
+ import InformationIcon from "../../assets/customIcons/information.svg";
14
+ import CloseIcon from "../../assets/customIcons/close.svg";
15
+ interface ToastProps {
16
+ visible: boolean;
17
+ message: string;
18
+ type:"error"|"success"|"warning"|"info"
19
+ duration?: number;
20
+ onHide?: () => void;
21
+ }
22
+
23
+ export default function Toast({
24
+ visible,
25
+ message,
26
+ type = "success",
27
+ duration = 2500,
28
+ onHide,
29
+ }: ToastProps) {
30
+ const { colors } = useAppTheme();
31
+
32
+ const translateY = useRef(new Animated.Value(-120)).current;
33
+ const opacity = useRef(new Animated.Value(0)).current;
34
+
35
+ useEffect(() => {
36
+ if (!visible) return;
37
+
38
+ Animated.parallel([
39
+ Animated.timing(translateY, {
40
+ toValue: 0,
41
+ duration: 300,
42
+ useNativeDriver: true,
43
+ }),
44
+ Animated.timing(opacity, {
45
+ toValue: 1,
46
+ duration: 300,
47
+ useNativeDriver: true,
48
+ }),
49
+ ]).start();
50
+
51
+ const timer = setTimeout(() => {
52
+ Animated.parallel([
53
+ Animated.timing(translateY, {
54
+ toValue: -120,
55
+ duration: 250,
56
+ useNativeDriver: true,
57
+ }),
58
+ Animated.timing(opacity, {
59
+ toValue: 0,
60
+ duration: 250,
61
+ useNativeDriver: true,
62
+ }),
63
+ ]).start(() => {
64
+ onHide?.();
65
+ });
66
+ }, duration);
67
+
68
+ return () => clearTimeout(timer);
69
+ }, [visible]);
70
+
71
+ const contentColor = colors.text.primary;
72
+ if (!visible) return null;
73
+ const toastStyle = {
74
+ success: {
75
+ background: colors.semantic.success,
76
+ text: contentColor,
77
+ icon: <CheckMarkIcon width={20} height={20} color={contentColor} />,
78
+ },
79
+ error: {
80
+ background: colors.semantic.error,
81
+ text:contentColor,
82
+ icon: <CloseIcon width={20} height={20} color={contentColor}/>,
83
+ },
84
+ warning: {
85
+ background: colors.semantic.warning,
86
+ text: contentColor,
87
+ icon: <WarningIcon width={20} height={20} color={contentColor}/>,
88
+ },
89
+ info: {
90
+ background: colors.semantic.border,
91
+ text: contentColor,
92
+ icon: <InformationIcon width={20} height={20} color={contentColor}/>,
93
+ },
94
+ }[type];
95
+ return (
96
+ <Modal
97
+ visible={visible}
98
+ transparent
99
+ animationType="none"
100
+ statusBarTranslucent
101
+ >
102
+ <View style={styles.modal}>
103
+ <Animated.View
104
+ style={[
105
+ styles.container,
106
+ {
107
+ backgroundColor: toastStyle.background,
108
+ opacity,
109
+ transform: [{ translateY }],
110
+ },
111
+ ]}
112
+ >
113
+ {toastStyle.icon}
114
+ <Text
115
+ style={[
116
+ styles.text,
117
+ {
118
+ color: toastStyle.text,
119
+ },
120
+ ]}
121
+ >
122
+ {message}
123
+ </Text>
124
+ </Animated.View>
125
+ </View>
126
+ </Modal>
127
+ );
128
+ }
129
+
130
+ const styles = StyleSheet.create({
131
+ modal: {
132
+ flex: 1,
133
+ pointerEvents: "box-none",
134
+ },
135
+
136
+ container: {
137
+ flexDirection:"row",
138
+ gap:10,
139
+ position: "absolute",
140
+ top: 60,
141
+ left: 16,
142
+ right: 16,
143
+
144
+ borderRadius: 10,
145
+ paddingVertical: 14,
146
+ paddingHorizontal: 18,
147
+
148
+ elevation: 20,
149
+
150
+ shadowColor: "#000",
151
+ shadowOpacity: 0.18,
152
+ shadowRadius: 10,
153
+ shadowOffset: {
154
+ width: 0,
155
+ height: 4,
156
+ },
157
+ },
158
+
159
+ text: {
160
+ fontFamily: Fonts.montserrat.semiBold,
161
+ fontSize: textSize.sm,
162
+ },
163
+ });
@@ -0,0 +1,132 @@
1
+ import React from "react";
2
+ import { Modal, Pressable, Text, View } from "react-native";
3
+ import SharpButton from "./sharpButton";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import { Fonts, textSize } from "../../tokens/typography";
6
+
7
+ interface WarningModalProps {
8
+ visible: boolean;
9
+ icon?: React.ReactNode;
10
+ title?: string;
11
+ description: string;
12
+ confirmLabel?: string;
13
+ cancelLabel?: string;
14
+ onConfirm?: () => void;
15
+ onCancel?: () => void;
16
+ loading?:boolean;
17
+ mainColor?:string;
18
+ }
19
+
20
+ export default function WarningModal({
21
+ visible,
22
+ icon,
23
+ title,
24
+ description,
25
+ confirmLabel,
26
+ cancelLabel,
27
+ onConfirm,
28
+ onCancel,
29
+ loading,
30
+ mainColor,
31
+ }: WarningModalProps) {
32
+ const { colors } = useAppTheme();
33
+
34
+ return (
35
+ <Modal
36
+ visible={visible}
37
+ animationType="fade"
38
+ transparent
39
+ onRequestClose={onCancel}
40
+ statusBarTranslucent
41
+ >
42
+ {/* Overlay */}
43
+ <Pressable
44
+ style={{
45
+ flex: 1,
46
+ backgroundColor: "rgba(0,0,0,0.60)",
47
+ justifyContent: "center",
48
+ alignItems: "center",
49
+ padding: 24,
50
+ }}
51
+ onPress={onCancel}
52
+ >
53
+ {/* Card */}
54
+ <Pressable
55
+ onPress={() => {}}
56
+ style={{
57
+ width: "100%",
58
+ maxWidth: 380,
59
+ backgroundColor: colors.background.card,
60
+ borderRadius: 22,
61
+ padding: 24,
62
+ alignItems: "center",
63
+ }}
64
+ >
65
+ {icon && (
66
+ <View
67
+ style={{
68
+ marginBottom: 18,
69
+ }}
70
+ >
71
+ {icon}
72
+ </View>
73
+ )}
74
+
75
+ {title && (
76
+ <Text
77
+ style={{
78
+ fontFamily: Fonts.montserrat.bold,
79
+ fontSize: textSize.lg,
80
+ color: colors.text.primary,
81
+ textAlign: "center",
82
+ marginBottom: 10,
83
+ }}
84
+ >
85
+ {title}
86
+ </Text>
87
+ )}
88
+
89
+ <Text
90
+ style={{
91
+ fontFamily: Fonts.montserrat.regular,
92
+ fontSize: textSize.sm,
93
+ color: colors.text.secondary,
94
+ textAlign: "center",
95
+ lineHeight: 22,
96
+ marginBottom: 24,
97
+ }}
98
+ >
99
+ {description}
100
+ </Text>
101
+
102
+ <View
103
+ style={{
104
+ width: "100%",
105
+ gap: 12,
106
+ }}
107
+ >
108
+ {confirmLabel && (
109
+ <SharpButton
110
+ text={confirmLabel}
111
+ bgColor={mainColor}
112
+ textColor={colors.text.primary}
113
+ onPress={onConfirm}
114
+ loading={loading}
115
+ />
116
+ )}
117
+
118
+ {cancelLabel && (
119
+ <SharpButton
120
+ text={cancelLabel}
121
+ bgColor={colors.background.card}
122
+ borderColor={colors.semantic.border}
123
+ textColor={colors.semantic.border}
124
+ onPress={onCancel}
125
+ />
126
+ )}
127
+ </View>
128
+ </Pressable>
129
+ </Pressable>
130
+ </Modal>
131
+ );
132
+ }
@@ -0,0 +1,15 @@
1
+ // constants/languages.ts
2
+ import { LanguageType } from "../types/language";
3
+
4
+ export const LANGUAGES: LanguageType[] = [
5
+ {
6
+ code: "en",
7
+ name: "English",
8
+ logo: require("../assets/images/en.jpg"),
9
+ },
10
+ {
11
+ code: "fr",
12
+ name: "Français",
13
+ logo: require("../assets/images/fr.jpg"),
14
+ },
15
+ ];
@@ -0,0 +1,49 @@
1
+ import React, { createContext, useContext, useEffect, useState } from "react";
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
+
4
+ const LANGUAGE_KEY = "app_language_preference";
5
+
6
+ export type Language = "en" | "fr";
7
+
8
+ type LanguageContextValue = {
9
+ language: Language;
10
+ setLanguage: (lang: Language) => void;
11
+ isLoaded: boolean;
12
+ };
13
+
14
+ const LanguageContext = createContext<LanguageContextValue | null>(null);
15
+
16
+ export function LanguageProvider({
17
+ defaultLanguage = "en",
18
+ children,
19
+ }: {
20
+ defaultLanguage?: Language;
21
+ children: React.ReactNode;
22
+ }) {
23
+ const [language, setLanguageState] = useState<Language>(defaultLanguage);
24
+ const [isLoaded, setIsLoaded] = useState(false);
25
+
26
+ useEffect(() => {
27
+ AsyncStorage.getItem(LANGUAGE_KEY).then((saved) => {
28
+ if (saved === "en" || saved === "fr") setLanguageState(saved);
29
+ setIsLoaded(true);
30
+ });
31
+ }, []);
32
+
33
+ const setLanguage = async (lang: Language) => {
34
+ setLanguageState(lang);
35
+ await AsyncStorage.setItem(LANGUAGE_KEY, lang);
36
+ };
37
+
38
+ return (
39
+ <LanguageContext.Provider value={{ language, setLanguage, isLoaded }}>
40
+ {children}
41
+ </LanguageContext.Provider>
42
+ );
43
+ }
44
+
45
+ export function useLanguage() {
46
+ const ctx = useContext(LanguageContext);
47
+ if (!ctx) throw new Error("useLanguage must be used inside LanguageProvider");
48
+ return ctx;
49
+ }