nx-react-native-cli 2.7.0 → 3.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 (135) hide show
  1. package/lib/index.cjs +43 -43
  2. package/package.json +1 -1
  3. package/templates/19.7.0/apps/mobile/android/app/src/main/java/com/appsmobile/MainActivity.kt +5 -1
  4. package/templates/19.7.0/apps/mobile/ios/AppsMobile/AppDelegate.mm +6 -0
  5. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png +0 -0
  6. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png +0 -0
  7. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png +0 -0
  8. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png +0 -0
  9. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-29.png +0 -0
  10. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png +0 -0
  11. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png +0 -0
  12. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png +0 -0
  13. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png +0 -0
  14. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png +0 -0
  15. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png +0 -0
  16. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png +0 -0
  17. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png +0 -0
  18. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png +0 -0
  19. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png +0 -0
  20. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png +0 -0
  21. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon@2x.png +0 -0
  22. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png +0 -0
  23. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon@3x.png +0 -0
  24. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png +0 -0
  25. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/AppIcon~ipad.png +0 -0
  26. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/AppIcon.appiconset/Contents.json +134 -0
  27. package/templates/19.7.0/apps/mobile/ios/AppsMobile/Images.xcassets/Contents.json +6 -0
  28. package/templates/21.2.2/apps/mobile/android/app/src/main/java/com/mobile/MainActivity.kt +5 -1
  29. package/templates/21.2.2/apps/mobile/ios/Mobile/AppDelegate.mm +6 -0
  30. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png +0 -0
  31. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png +0 -0
  32. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png +0 -0
  33. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png +0 -0
  34. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-29.png +0 -0
  35. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png +0 -0
  36. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png +0 -0
  37. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png +0 -0
  38. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png +0 -0
  39. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png +0 -0
  40. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png +0 -0
  41. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png +0 -0
  42. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png +0 -0
  43. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png +0 -0
  44. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png +0 -0
  45. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png +0 -0
  46. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon@2x.png +0 -0
  47. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png +0 -0
  48. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon@3x.png +0 -0
  49. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png +0 -0
  50. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/AppIcon~ipad.png +0 -0
  51. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/AppIcon.appiconset/Contents.json +134 -0
  52. package/templates/21.2.2/apps/mobile/ios/Mobile/Images.xcassets/Contents.json +6 -0
  53. package/templates/shared/apps/mobile/android/app/src/main/AndroidManifest.xml +28 -0
  54. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  55. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  56. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png +0 -0
  57. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  58. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png +0 -0
  59. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  60. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png +0 -0
  61. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  62. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png +0 -0
  63. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  64. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png +0 -0
  65. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  66. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png +0 -0
  67. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  68. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png +0 -0
  69. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  70. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png +0 -0
  71. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  72. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png +0 -0
  73. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  74. package/templates/shared/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png +0 -0
  75. package/templates/shared/apps/mobile/android/app/src/main/res/styles.xml +14 -0
  76. package/templates/shared/apps/mobile/run-android.sh +2 -1
  77. package/templates/shared/apps/mobile/scripts/setup-ios-dev-scheme.rb +101 -1
  78. package/templates/shared/apps/mobile/src/app/index.tsx +16 -47
  79. package/templates/shared/apps/mobile/src/app/query-client.ts +40 -0
  80. package/templates/shared/apps/mobile/src/assets/images/logo.png +0 -0
  81. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.component.tsx +134 -0
  82. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert-manager.types.ts +18 -0
  83. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/alert.service.ts +27 -0
  84. package/templates/shared/apps/mobile/src/components/atoms/AlertManager/index.ts +3 -0
  85. package/templates/shared/apps/mobile/src/components/atoms/BottomSheet/bottom-sheet.component.tsx +14 -8
  86. package/templates/shared/apps/mobile/src/components/atoms/Button/button.component.tsx +1 -1
  87. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/date-modal-input.component.tsx +69 -0
  88. package/templates/shared/apps/mobile/src/components/atoms/DateModalInput/index.ts +1 -0
  89. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/date-picker.component.tsx +44 -0
  90. package/templates/shared/apps/mobile/src/components/atoms/DatePicker/index.ts +1 -0
  91. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/date-text-input.component.tsx +218 -0
  92. package/templates/shared/apps/mobile/src/components/atoms/DateTextInput/index.ts +1 -0
  93. package/templates/shared/apps/mobile/src/components/atoms/Divider/divider-component.tsx +1 -1
  94. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/gradient-background.component.tsx +45 -0
  95. package/templates/shared/apps/mobile/src/components/atoms/GradientBackground/index.ts +1 -0
  96. package/templates/shared/apps/mobile/src/components/atoms/InputLayout/input-layout.component.tsx +12 -4
  97. package/templates/shared/apps/mobile/src/components/atoms/KeyboardAccessory/keyboard-accessory.component.tsx +6 -3
  98. package/templates/shared/apps/mobile/src/components/atoms/KeyboardAwareScrollView/keyboard-aware-scroll-view.component.tsx +1 -0
  99. package/templates/shared/apps/mobile/src/components/atoms/Modal/modal.component.tsx +2 -0
  100. package/templates/shared/apps/mobile/src/components/atoms/ScreenLoader/screen-loader.component.tsx +6 -1
  101. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/index.ts +1 -0
  102. package/templates/shared/apps/mobile/src/components/atoms/SelectDropdown/select-dropdown.component.tsx +223 -0
  103. package/templates/shared/apps/mobile/src/components/atoms/Skeleton/skeleton.component.tsx +1 -1
  104. package/templates/shared/apps/mobile/src/components/atoms/TextInput/bottom-sheet-text-input.component.tsx +4 -3
  105. package/templates/shared/apps/mobile/src/components/atoms/TextInput/text-input.component.tsx +8 -4
  106. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/index.ts +1 -0
  107. package/templates/shared/apps/mobile/src/components/atoms/ThemeManager/theme-manager.component.tsx +27 -0
  108. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/index.ts +3 -0
  109. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.component.tsx +109 -0
  110. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast-manager.types.ts +10 -0
  111. package/templates/shared/apps/mobile/src/components/atoms/ToastManager/toast.service.ts +27 -0
  112. package/templates/shared/apps/mobile/src/components/atoms/Typography/typography.component.tsx +1 -1
  113. package/templates/shared/apps/mobile/src/components/atoms/index.ts +8 -0
  114. package/templates/shared/apps/mobile/src/components/molecules/BackButton/back-button.component.tsx +1 -1
  115. package/templates/shared/apps/mobile/src/components/molecules/ScreenContainer/screen-container.component.tsx +4 -24
  116. package/templates/shared/apps/mobile/src/components/molecules/ScreenHeader/screen-header.component.tsx +2 -2
  117. package/templates/shared/apps/mobile/src/hooks/index.ts +1 -0
  118. package/templates/shared/apps/mobile/src/hooks/usePushNotifications.hook.ts +104 -0
  119. package/templates/shared/apps/mobile/src/hooks/useToggleDarkMode.hook.tsx +24 -2
  120. package/templates/shared/apps/mobile/src/icons/alert-triangle.svg +5 -0
  121. package/templates/shared/apps/mobile/src/icons/check-circle.svg +4 -0
  122. package/templates/shared/apps/mobile/src/icons/chevron-down.svg +1 -0
  123. package/templates/shared/apps/mobile/src/icons/chevron-right.svg +1 -0
  124. package/templates/shared/apps/mobile/src/icons/index.ts +18 -1
  125. package/templates/shared/apps/mobile/src/icons/info.svg +5 -0
  126. package/templates/shared/apps/mobile/src/icons/x-circle.svg +5 -0
  127. package/templates/shared/apps/mobile/src/routes/index.tsx +26 -15
  128. package/templates/shared/apps/mobile/src/screens/LandingScreen/landing.screen.tsx +232 -8
  129. package/templates/shared/apps/mobile/src/stores/local-storage.store.ts +9 -5
  130. package/templates/shared/apps/mobile/src/stores/theme.slice.ts +15 -0
  131. package/templates/shared/apps/mobile/src/stores/user.slice.ts +5 -1
  132. package/templates/shared/apps/mobile/src/tailwind/index.ts +3 -3
  133. package/templates/shared/apps/mobile/tailwind.config.js +14 -0
  134. package/templates/shared/patches/react-native-animatable+1.4.0.patch +71 -0
  135. package/templates/shared/apps/mobile/src/assets/images/.gitkeep +0 -0
@@ -0,0 +1,218 @@
1
+ import dayjs from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
3
+ import React, { ReactNode, Ref, useCallback, useEffect, useRef, useState } from 'react';
4
+ import { Pressable, StyleProp, TextInput, ViewStyle } from 'react-native';
5
+
6
+ import { Typography } from '@/components/atoms/Typography';
7
+ import CONFIG from '@/config';
8
+ import {
9
+ defaultInputContainerStyle,
10
+ defaultInputTextStyle,
11
+ focusedInputStyle,
12
+ tw,
13
+ } from '@/tailwind';
14
+
15
+ dayjs.extend(customParseFormat);
16
+
17
+ const DATE_FORMAT = 'MM/DD/YYYY';
18
+ const GUIDE_TEMPLATE = 'MM/DD/YYYY';
19
+ const MAX_DIGITS = 8;
20
+ const MONTH_SEPARATOR_INDEX = 2;
21
+ const DAY_SEPARATOR_INDEX = 4;
22
+
23
+ const fontFamily = CONFIG.IS_IOS ? { fontFamily: 'Menlo' } : { fontFamily: 'monospace' };
24
+
25
+ type Props = {
26
+ error?: string;
27
+ onChange: (date: Date | undefined) => void;
28
+ onValidationError?: (error: string | undefined) => void;
29
+ ref?: Ref<TextInput>;
30
+ renderRight?: (date: Date) => ReactNode;
31
+ value: Date | undefined;
32
+ };
33
+
34
+ const MONTH_FIRST_DIGIT_INDEX = 0;
35
+ const MONTH_SECOND_DIGIT_INDEX = 1;
36
+ const DAY_FIRST_DIGIT_INDEX = 2;
37
+ const DAY_SECOND_DIGIT_INDEX = 3;
38
+ const YEAR_FIRST_DIGIT_INDEX = 4;
39
+ const MAX_MONTH_FIRST_DIGIT = 1;
40
+ const MAX_DAY_FIRST_DIGIT = 3;
41
+
42
+ function isDigitValid(digit: string, index: number, previous: string): boolean {
43
+ const d = Number(digit);
44
+
45
+ if (index === MONTH_FIRST_DIGIT_INDEX) {
46
+ return d <= MAX_MONTH_FIRST_DIGIT;
47
+ }
48
+
49
+ if (index === MONTH_SECOND_DIGIT_INDEX) {
50
+ return previous[0] === '0' ? d >= 1 : d <= MONTH_SEPARATOR_INDEX;
51
+ }
52
+
53
+ if (index === DAY_FIRST_DIGIT_INDEX) {
54
+ return d <= MAX_DAY_FIRST_DIGIT;
55
+ }
56
+
57
+ if (index === DAY_SECOND_DIGIT_INDEX) {
58
+ return previous[MONTH_SEPARATOR_INDEX] === '3' ? d <= 1 : d >= 0;
59
+ }
60
+
61
+ if (index === YEAR_FIRST_DIGIT_INDEX) {
62
+ return d === 1 || d === MONTH_SEPARATOR_INDEX;
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ function filterValidDigits(raw: string): string {
69
+ let result = '';
70
+
71
+ for (let i = 0; i < raw.length && result.length < MAX_DIGITS; i += 1) {
72
+ if (isDigitValid(raw[i], result.length, result)) {
73
+ result += raw[i];
74
+ }
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ function extractDigits(date: Date | undefined): string {
81
+ if (!date) {
82
+ return '';
83
+ }
84
+
85
+ return dayjs(date).format(DATE_FORMAT).replace(/\D/g, '');
86
+ }
87
+
88
+ function buildFilledPortion(digits: string): string {
89
+ let result = '';
90
+
91
+ for (let i = 0; i < digits.length && i < MAX_DIGITS; i += 1) {
92
+ if (i === MONTH_SEPARATOR_INDEX || i === DAY_SEPARATOR_INDEX) {
93
+ result += '/';
94
+ }
95
+ result += digits[i];
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ function buildGuideSuffix(digits: string): string {
102
+ const filled = buildFilledPortion(digits);
103
+
104
+ return GUIDE_TEMPLATE.slice(filled.length);
105
+ }
106
+
107
+ function parseDate(digits: string): Date | undefined {
108
+ if (digits.length !== MAX_DIGITS) {
109
+ return undefined;
110
+ }
111
+
112
+ const masked = buildFilledPortion(digits);
113
+ const parsed = dayjs(masked, DATE_FORMAT, true);
114
+
115
+ if (parsed.isValid()) {
116
+ return parsed.toDate();
117
+ }
118
+
119
+ return undefined;
120
+ }
121
+
122
+ export function DateTextInput(props: Props) {
123
+ const { error, onChange, onValidationError, ref, renderRight, value } = props;
124
+ const [digits, setDigits] = useState(() => extractDigits(value));
125
+ const [isFocused, setIsFocused] = useState(false);
126
+ const [invalidDateError, setInvalidDateError] = useState<string | undefined>();
127
+ const inputRef = useRef<TextInput>(null);
128
+
129
+ useEffect(() => {
130
+ if (!ref) {
131
+ return;
132
+ }
133
+
134
+ if (typeof ref === 'function') {
135
+ ref(inputRef.current);
136
+ } else {
137
+ (ref as React.MutableRefObject<TextInput | null>).current = inputRef.current;
138
+ }
139
+ }, [ref]);
140
+
141
+ const filledPortion = buildFilledPortion(digits);
142
+ const guideSuffix = buildGuideSuffix(digits);
143
+ const parsedDate = parseDate(digits);
144
+ const displayError = invalidDateError || error;
145
+
146
+ const containerStyle: StyleProp<ViewStyle> = [
147
+ defaultInputContainerStyle,
148
+ tw`dark:border-divider dark:bg-surface items-center`,
149
+ focusedInputStyle(isFocused),
150
+ displayError && tw`border-red-500`,
151
+ ];
152
+
153
+ const handleChangeText = useCallback(
154
+ (text: string) => {
155
+ const rawDigits = text.replace(/\D/g, '');
156
+ const newDigits = filterValidDigits(rawDigits);
157
+ setDigits(newDigits);
158
+
159
+ if (newDigits.length === MAX_DIGITS) {
160
+ const masked = buildFilledPortion(newDigits);
161
+ const parsed = dayjs(masked, DATE_FORMAT, true);
162
+
163
+ if (parsed.isValid()) {
164
+ setInvalidDateError(undefined);
165
+ onValidationError?.(undefined);
166
+ onChange(parsed.toDate());
167
+
168
+ return;
169
+ }
170
+
171
+ const errorMsg = 'Please enter a valid date';
172
+ setInvalidDateError(errorMsg);
173
+ onValidationError?.(errorMsg);
174
+ } else {
175
+ setInvalidDateError(undefined);
176
+ onValidationError?.(undefined);
177
+ }
178
+
179
+ onChange(undefined);
180
+ },
181
+ [onChange, onValidationError],
182
+ );
183
+
184
+ const handleBlur = useCallback(() => {
185
+ setIsFocused(false);
186
+ }, []);
187
+
188
+ const handleFocus = useCallback(() => {
189
+ setIsFocused(true);
190
+ }, []);
191
+
192
+ const handlePress = useCallback(() => {
193
+ inputRef.current?.focus();
194
+ }, []);
195
+
196
+ return (
197
+ <Pressable style={containerStyle} onPress={handlePress}>
198
+ <TextInput
199
+ ref={inputRef}
200
+ keyboardType="number-pad"
201
+ style={[
202
+ defaultInputTextStyle,
203
+ tw`dark:text-foreground`,
204
+ { ...fontFamily, opacity: 0, position: 'absolute' },
205
+ ]}
206
+ value={digits}
207
+ onBlur={handleBlur}
208
+ onChangeText={handleChangeText}
209
+ onFocus={handleFocus}
210
+ />
211
+ <Typography style={[defaultInputTextStyle, tw`dark:text-foreground`, fontFamily]}>
212
+ {filledPortion}
213
+ <Typography style={[tw`text-placeholder`, { ...fontFamily }]}>{guideSuffix}</Typography>
214
+ </Typography>
215
+ {parsedDate && renderRight?.(parsedDate)}
216
+ </Pressable>
217
+ );
218
+ }
@@ -0,0 +1 @@
1
+ export * from './date-text-input.component';
@@ -9,5 +9,5 @@ type Props = DefaultComponentProps & {};
9
9
  export function Divider(props: Props) {
10
10
  const { style } = props;
11
11
 
12
- return <View style={[tw`h-[1px] w-full bg-gray-200`, style]}></View>;
12
+ return <View style={[tw`dark:bg-divider h-[1px] w-full bg-gray-200`, style]}></View>;
13
13
  }
@@ -0,0 +1,45 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ import { Canvas, Fill, LinearGradient, vec } from '@shopify/react-native-skia';
3
+ import React from 'react';
4
+ import { StyleSheet, useWindowDimensions } from 'react-native';
5
+
6
+ import { useLocalStorageStore } from '@/stores';
7
+ import { colors } from '@/tailwind';
8
+
9
+ type Props = {
10
+ variant?: 'bottomSheet' | 'login' | 'welcome';
11
+ };
12
+
13
+ const DARK_GRADIENTS: Record<string, string[]> = {
14
+ bottomSheet: [colors.sheet, colors.background],
15
+ login: [colors.secondary[950], colors.background, colors.secondary[950]],
16
+ welcome: [colors.background, colors.secondary[950], colors.background],
17
+ };
18
+
19
+ const LIGHT_GRADIENTS: Record<string, string[]> = {
20
+ bottomSheet: [colors.gray[100], colors.white],
21
+ login: [colors.gray[50], colors.white, colors.gray[50]],
22
+ welcome: [colors.white, colors.gray[50], colors.white],
23
+ };
24
+
25
+ export function GradientBackground(props: Props) {
26
+ const { variant = 'login' } = props;
27
+ const colorScheme = useLocalStorageStore((s) => s.colorScheme);
28
+ const isDark = colorScheme === 'dark';
29
+ const { height, width } = useWindowDimensions();
30
+
31
+ const gradientColors = isDark ? DARK_GRADIENTS[variant] : LIGHT_GRADIENTS[variant];
32
+
33
+ return (
34
+ <Canvas style={StyleSheet.absoluteFill}>
35
+ {/* Base linear gradient */}
36
+ <Fill>
37
+ <LinearGradient
38
+ colors={gradientColors}
39
+ end={vec(width / 2, height)}
40
+ start={vec(width / 2, 0)}
41
+ />
42
+ </Fill>
43
+ </Canvas>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export * from './gradient-background.component';
@@ -9,24 +9,32 @@ type Props = DefaultComponentProps & {
9
9
  children?: React.ReactNode;
10
10
  isRequired?: boolean;
11
11
  label?: string;
12
+ subtitle?: string;
12
13
  textStyle?: StyleProp<TextStyle>;
13
14
  };
14
15
 
15
16
  export function InputLayout(props: Props) {
16
- const { children, error, isRequired, label, style, textStyle } = props;
17
+ const { children, error, isRequired, label, style, subtitle, textStyle } = props;
17
18
 
18
19
  return (
19
20
  <View style={[style]}>
20
21
  {label && (
21
- <Typography style={[tw`mb-2 text-gray-600`, textStyle]}>
22
+ <Typography style={[tw`mb-2 text-gray-600 dark:text-gray-300`, textStyle]}>
22
23
  {label}
23
24
  {isRequired && <Typography style={tw`text-red-600`}>{isRequired && '*'}</Typography>}
24
25
  </Typography>
25
26
  )}
26
27
  {children}
27
28
  {!!error && (
28
- <View style={tw`items-end`}>
29
- <Typography style={tw`text-right text-red-500`}>{error}</Typography>
29
+ <View style={tw`mt-1 items-start`}>
30
+ <Typography style={tw`text-right text-xs text-red-500`}>{error}</Typography>
31
+ </View>
32
+ )}
33
+ {!!subtitle && !error && (
34
+ <View style={tw`mt-1 items-start`}>
35
+ <Typography style={tw`dark:text-subtitle text-right text-xs text-gray-400`}>
36
+ {subtitle}
37
+ </Typography>
30
38
  </View>
31
39
  )}
32
40
  </View>
@@ -15,9 +15,12 @@ export function KeyboardAccessory(props: Props) {
15
15
 
16
16
  return (
17
17
  <InputAccessoryView nativeID={nativeID}>
18
- <View style={tw`flex-row items-center justify-end bg-[#313132] px-2`}>
19
- <Button buttonStyle={tw`my-2 rounded-lg bg-[#717172]`} onPress={() => Keyboard.dismiss()}>
20
- <KeyboardHideIcon style={tw`text-white`} />
18
+ <View style={tw`flex-row items-center justify-end bg-gray-100 px-2 dark:bg-[#313132]`}>
19
+ <Button
20
+ buttonStyle={tw`my-2 rounded-lg bg-gray-200 dark:bg-[#717172]`}
21
+ onPress={() => Keyboard.dismiss()}
22
+ >
23
+ <KeyboardHideIcon style={tw`text-gray-600 dark:text-white`} />
21
24
  </Button>
22
25
  </View>
23
26
  </InputAccessoryView>
@@ -67,6 +67,7 @@ export function KeyboardAwareScrollView(props: Props) {
67
67
  refreshControl={refreshControl}
68
68
  scrollEventThrottle={16}
69
69
  ScrollViewComponent={ScrollViewComponent}
70
+ showsVerticalScrollIndicator={false}
70
71
  style={style}
71
72
  onScroll={onScroll}
72
73
  {...rest}
@@ -30,6 +30,8 @@ function IOSModal(props: ModalProps) {
30
30
 
31
31
  return (
32
32
  <RNModal
33
+ hideModalContentWhileAnimating
34
+ useNativeDriver
33
35
  animationIn="fadeIn"
34
36
  animationInTiming={FADE_IN_DURATION}
35
37
  animationOut="fadeOut"
@@ -10,7 +10,12 @@ export function ScreenLoader(props: Props) {
10
10
  const { style } = props;
11
11
 
12
12
  return (
13
- <View style={[tw`h-full w-full items-center justify-center bg-gray-50 p-8`, style]}>
13
+ <View
14
+ style={[
15
+ tw`dark:bg-background h-full w-full items-center justify-center bg-gray-50 p-8`,
16
+ style,
17
+ ]}
18
+ >
14
19
  <ActivityIndicator color={colors.primary[400]} />
15
20
  </View>
16
21
  );
@@ -0,0 +1 @@
1
+ export * from './select-dropdown.component';
@@ -0,0 +1,223 @@
1
+ /* eslint-disable max-params */
2
+ /* eslint-disable no-magic-numbers */
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Dimensions, Pressable, ScrollView, View } from 'react-native';
5
+
6
+ import { Modal, Typography } from '@/components';
7
+ import { ChevronDownIcon, ChevronRightIcon } from '@/icons';
8
+ import { colors, defaultInputContainerStyle, defaultInputTextStyle, tw } from '@/tailwind';
9
+ import { DefaultComponentProps } from '@/types';
10
+
11
+ const DROPDOWN_OFFSET = 8;
12
+ const MAX_MENU_HEIGHT = 320;
13
+ const OPTION_ROW_HEIGHT = 48;
14
+
15
+ type DropdownOption = {
16
+ id: string;
17
+ label: string;
18
+ };
19
+
20
+ type DropdownLayout = {
21
+ height: number;
22
+ width: number;
23
+ x: number;
24
+ y: number;
25
+ };
26
+
27
+ type Props = DefaultComponentProps & {
28
+ label?: string;
29
+ options: DropdownOption[];
30
+ placeholder?: string;
31
+ selectedId?: string;
32
+ onSelect: (id: string) => void;
33
+ };
34
+
35
+ type OptionItemProps = {
36
+ isLast: boolean;
37
+ isSelected: boolean;
38
+ label: string;
39
+ onPress: () => void;
40
+ };
41
+
42
+ function OptionItem(props: OptionItemProps) {
43
+ const { isLast, isSelected, label, onPress } = props;
44
+
45
+ return (
46
+ <Pressable
47
+ style={[
48
+ tw`flex-row items-center justify-between px-4 py-3`,
49
+ !isLast && tw`dark:border-divider border-b border-gray-200`,
50
+ ]}
51
+ onPress={onPress}
52
+ >
53
+ <View style={tw`flex-1`}>
54
+ <Typography
55
+ style={[
56
+ tw`text-base`,
57
+ isSelected ? tw`text-primary-500 font-medium` : tw`dark:text-foreground text-gray-800`,
58
+ ]}
59
+ >
60
+ {label}
61
+ </Typography>
62
+ </View>
63
+ </Pressable>
64
+ );
65
+ }
66
+
67
+ export function SelectDropdown(props: Props) {
68
+ const { label, onSelect, options, placeholder = 'Select', selectedId, style } = props;
69
+ const [isOpen, setIsOpen] = useState<boolean>(false);
70
+ const [dropdownLayout, setDropdownLayout] = useState<DropdownLayout | null>(null);
71
+ const triggerRef = useRef<View>(null);
72
+
73
+ const selectedOption = useMemo(
74
+ () => options.find((option) => option.id === selectedId),
75
+ [options, selectedId],
76
+ );
77
+ const selectedLabel = selectedOption?.label ?? placeholder;
78
+ const lastIndex = options.length - 1;
79
+
80
+ const handleToggle = useCallback(() => {
81
+ setIsOpen((prev) => !prev);
82
+ }, []);
83
+
84
+ const handleClose = useCallback(() => {
85
+ setIsOpen(false);
86
+ }, []);
87
+
88
+ const handleSelect = useCallback(
89
+ (optionId: string) => {
90
+ onSelect(optionId);
91
+ setIsOpen(false);
92
+ },
93
+ [onSelect],
94
+ );
95
+
96
+ useEffect(() => {
97
+ if (!isOpen) {
98
+ return;
99
+ }
100
+
101
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
102
+ setDropdownLayout({ height, width, x, y });
103
+ });
104
+ }, [isOpen]);
105
+
106
+ const optionPressHandlers = useMemo(() => {
107
+ const handlers: Record<string, () => void> = {};
108
+
109
+ options.forEach((option) => {
110
+ handlers[option.id] = () => handleSelect(option.id);
111
+ });
112
+
113
+ return handlers;
114
+ }, [handleSelect, options]);
115
+
116
+ function renderOption(option: DropdownOption, index: number) {
117
+ const handlePress = optionPressHandlers[option.id];
118
+
119
+ return (
120
+ <OptionItem
121
+ key={option.id}
122
+ isLast={index === lastIndex}
123
+ isSelected={option.id === selectedId}
124
+ label={option.label}
125
+ onPress={handlePress}
126
+ />
127
+ );
128
+ }
129
+
130
+ function renderMenu() {
131
+ if (!dropdownLayout) {
132
+ return null;
133
+ }
134
+
135
+ const screenHeight = Dimensions.get('window').height;
136
+ const estimatedMenuHeight = Math.min(MAX_MENU_HEIGHT, options.length * OPTION_ROW_HEIGHT);
137
+ const spaceBelow = screenHeight - (dropdownLayout.y + dropdownLayout.height) - DROPDOWN_OFFSET;
138
+ const spaceAbove = dropdownLayout.y - DROPDOWN_OFFSET;
139
+ const canOpenBelow = spaceBelow >= estimatedMenuHeight;
140
+ const canOpenAbove = spaceAbove >= estimatedMenuHeight;
141
+ const shouldOpenBelow =
142
+ canOpenBelow || (!canOpenBelow && !canOpenAbove && spaceBelow >= spaceAbove);
143
+ const preferredTop = shouldOpenBelow
144
+ ? dropdownLayout.y + dropdownLayout.height + DROPDOWN_OFFSET
145
+ : dropdownLayout.y - estimatedMenuHeight - DROPDOWN_OFFSET;
146
+ const clampedTop = Math.min(
147
+ Math.max(0, preferredTop),
148
+ Math.max(0, screenHeight - estimatedMenuHeight - DROPDOWN_OFFSET),
149
+ );
150
+
151
+ return (
152
+ <View
153
+ style={[
154
+ tw`dark:border-divider dark:bg-sheet absolute z-50 max-h-80 rounded-xl border border-gray-200 bg-white`,
155
+ {
156
+ elevation: 5,
157
+ left: dropdownLayout.x,
158
+ shadowColor: colors.gray[900],
159
+ shadowOffset: {
160
+ height: 2,
161
+ width: 0,
162
+ },
163
+ shadowOpacity: 0.15,
164
+ shadowRadius: 4,
165
+ top: clampedTop,
166
+ width: dropdownLayout.width,
167
+ },
168
+ ]}
169
+ >
170
+ <ScrollView showsVerticalScrollIndicator={false} style={tw`max-h-80`}>
171
+ {options.map(renderOption)}
172
+ </ScrollView>
173
+ </View>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <>
179
+ {label && (
180
+ <Typography style={tw`mb-1 text-sm font-medium text-gray-700 dark:text-gray-300`}>
181
+ {label}
182
+ </Typography>
183
+ )}
184
+ <Pressable
185
+ ref={triggerRef}
186
+ style={[
187
+ defaultInputContainerStyle,
188
+ tw`dark:border-divider dark:bg-surface items-center justify-between`,
189
+ style,
190
+ ]}
191
+ onPress={handleToggle}
192
+ >
193
+ <View style={tw`flex-1`}>
194
+ <Typography
195
+ style={[
196
+ defaultInputTextStyle,
197
+ tw`dark:text-foreground`,
198
+ !selectedOption && tw`dark:text-placeholder text-gray-400`,
199
+ ]}
200
+ >
201
+ {selectedLabel}
202
+ </Typography>
203
+ </View>
204
+ {isOpen ? (
205
+ <ChevronDownIcon height={20} style={tw`dark:text-subtitle text-gray-500`} width={20} />
206
+ ) : (
207
+ <ChevronRightIcon height={20} style={tw`dark:text-subtitle text-gray-500`} width={20} />
208
+ )}
209
+ </Pressable>
210
+
211
+ <Modal
212
+ containerStyle={tw`flex-1 p-0`}
213
+ isVisible={isOpen}
214
+ onBackButtonPress={handleClose}
215
+ onBackdropPress={handleClose}
216
+ >
217
+ <View pointerEvents="box-none" style={tw`flex-1`}>
218
+ {renderMenu()}
219
+ </View>
220
+ </Modal>
221
+ </>
222
+ );
223
+ }
@@ -36,7 +36,7 @@ export function Skeleton(props: Props) {
36
36
  }
37
37
 
38
38
  return (
39
- <Animated.View style={[tw`rounded-lg bg-gray-200`, animatedStyle, style]}>
39
+ <Animated.View style={[tw`dark:bg-surface rounded-lg bg-gray-200`, animatedStyle, style]}>
40
40
  <View style={tw`opacity-0`}>{children}</View>
41
41
  </Animated.View>
42
42
  );
@@ -88,6 +88,7 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
88
88
  <View
89
89
  style={[
90
90
  defaultInputContainerStyle,
91
+ tw`dark:border-divider dark:bg-surface`,
91
92
  focusedInputStyle(isFocused),
92
93
  disabledInputStyle(isDisabled),
93
94
  style,
@@ -100,8 +101,8 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
100
101
  multiline={multiline}
101
102
  placeholder={placeholder}
102
103
  placeholderTextColor={colors.gray[500]}
103
- selectionColor={colors.primary}
104
- style={[defaultInputTextStyle, textStyle]}
104
+ selectionColor={colors.primary[400]}
105
+ style={[defaultInputTextStyle, tw`dark:text-foreground`, textStyle]}
105
106
  value={value}
106
107
  onBlur={(e) => handleOnBlur(e as NativeSyntheticEvent<TextInputFocusEventData>)}
107
108
  onChangeText={handleOnChangeText}
@@ -115,7 +116,7 @@ export function BottomSheetTextInput(props: BottomSheetTextInputProps) {
115
116
  testID="clear-button"
116
117
  onPress={handleOnClearPress}
117
118
  >
118
- <CrossIcon />
119
+ <CrossIcon style={tw`text-subtitle`} />
119
120
  </Pressable>
120
121
  )}
121
122
  </View>
@@ -11,6 +11,7 @@ import {
11
11
  import { DefaultNameInputProps } from './constants';
12
12
 
13
13
  import { CrossIcon } from '@/icons';
14
+ import { useLocalStorageStore } from '@/stores';
14
15
  import {
15
16
  colors,
16
17
  defaultInputContainerStyle,
@@ -43,6 +44,7 @@ export function TextInput(props: TextInputProps) {
43
44
  value,
44
45
  ...extraProps
45
46
  } = props;
47
+ const colorScheme = useLocalStorageStore((s) => s.colorScheme);
46
48
  const [isClearButtonVisible, setIsClearButtonVisible] = useState<boolean>(false);
47
49
  const [isFocused, setFocused] = useState<boolean>(false);
48
50
 
@@ -81,6 +83,7 @@ export function TextInput(props: TextInputProps) {
81
83
  <View
82
84
  style={[
83
85
  defaultInputContainerStyle,
86
+ tw`dark:border-divider dark:bg-surface`,
84
87
  focusedInputStyle(isFocused),
85
88
  disabledInputStyle(isDisabled),
86
89
  style,
@@ -89,12 +92,13 @@ export function TextInput(props: TextInputProps) {
89
92
  <RNTextInput
90
93
  {...DefaultNameInputProps}
91
94
  ref={textInputRef}
95
+ cursorColor={colors.primary[400]}
92
96
  editable={!isDisabled}
93
97
  multiline={multiline}
94
98
  placeholder={placeholder}
95
- placeholderTextColor={colors.gray[500]}
96
- selectionColor={colors.primary}
97
- style={[defaultInputTextStyle, textStyle]}
99
+ placeholderTextColor={colorScheme === 'dark' ? colors.placeholder : colors.gray[400]}
100
+ selectionColor={colors.primary[400]}
101
+ style={[defaultInputTextStyle, tw`dark:text-foreground`, textStyle]}
98
102
  value={value}
99
103
  onBlur={handleOnBlur}
100
104
  onChangeText={handleOnChangeText}
@@ -108,7 +112,7 @@ export function TextInput(props: TextInputProps) {
108
112
  testID="clear-button"
109
113
  onPress={handleOnClearPress}
110
114
  >
111
- <CrossIcon style={tw`text-gray-400`} />
115
+ <CrossIcon style={tw`dark:text-subtitle text-gray-400`} />
112
116
  </Pressable>
113
117
  )}
114
118
  </View>
@@ -0,0 +1 @@
1
+ export * from './theme-manager.component';