@oxyhq/services 5.4.3 → 5.4.4

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 (189) hide show
  1. package/README.md +14 -0
  2. package/lib/commonjs/assets/assets/illustrations/HighFive.tsx +41 -0
  3. package/lib/commonjs/assets/icons/OxyServices.js +1 -1
  4. package/lib/commonjs/assets/illustrations/HighFive.js +61 -0
  5. package/lib/commonjs/assets/illustrations/HighFive.js.map +1 -0
  6. package/lib/commonjs/core/index.js +2 -2
  7. package/lib/commonjs/index.js +22 -22
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/node/createAuth.js +95 -0
  10. package/lib/commonjs/node/createAuth.js.map +1 -0
  11. package/lib/commonjs/node/index.js +15 -6
  12. package/lib/commonjs/node/index.js.map +1 -1
  13. package/lib/commonjs/package.json +1 -0
  14. package/lib/commonjs/ui/components/Avatar.js +3 -3
  15. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  16. package/lib/commonjs/ui/components/FollowButton.js +3 -3
  17. package/lib/commonjs/ui/components/GroupedSection.js +1 -1
  18. package/lib/commonjs/ui/components/OxyLogo.js +1 -1
  19. package/lib/commonjs/ui/components/OxyProvider.js +146 -141
  20. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  21. package/lib/commonjs/ui/components/OxySignInButton.js +2 -2
  22. package/lib/commonjs/ui/components/ProfileCard.js +2 -2
  23. package/lib/commonjs/ui/components/Section.js +1 -1
  24. package/lib/commonjs/ui/components/SectionTitle.js +1 -1
  25. package/lib/commonjs/ui/components/icon/index.js +1 -1
  26. package/lib/commonjs/ui/components/index.js +12 -12
  27. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +213 -0
  28. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -0
  29. package/lib/commonjs/ui/components/internal/TextField.js +576 -0
  30. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -0
  31. package/lib/commonjs/ui/context/OxyContext.js +1 -1
  32. package/lib/commonjs/ui/index.js +19 -11
  33. package/lib/commonjs/ui/index.js.map +1 -1
  34. package/lib/commonjs/ui/navigation/OxyRouter.js +23 -18
  35. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  36. package/lib/commonjs/ui/screens/AccountCenterScreen.js +18 -18
  37. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  38. package/lib/commonjs/ui/screens/AccountManagementDemo.js +3 -3
  39. package/lib/commonjs/ui/screens/AccountManagementDemo.js.map +1 -1
  40. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +4 -4
  41. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +5 -5
  42. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +3 -3
  43. package/lib/commonjs/ui/screens/AppInfoScreen.js +6 -6
  44. package/lib/commonjs/ui/screens/BillingManagementScreen.js +3 -3
  45. package/lib/commonjs/ui/screens/FeedbackScreen.js +1169 -0
  46. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -0
  47. package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -3
  48. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +3 -3
  49. package/lib/commonjs/ui/screens/ProfileScreen.js +2 -2
  50. package/lib/commonjs/ui/screens/SessionManagementScreen.js +2 -2
  51. package/lib/commonjs/ui/screens/SignInScreen.js +182 -304
  52. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  53. package/lib/commonjs/ui/screens/SignUpScreen.js +811 -712
  54. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  55. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +3 -3
  56. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js +2 -2
  57. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js +1 -1
  58. package/lib/commonjs/ui/store/index.js +52 -0
  59. package/lib/commonjs/ui/store/index.js.map +1 -0
  60. package/lib/commonjs/ui/styles/index.js +2 -2
  61. package/lib/commonjs/ui/styles/theme.js +1 -1
  62. package/lib/commonjs/utils/index.js +1 -1
  63. package/lib/module/assets/assets/illustrations/HighFive.tsx +41 -0
  64. package/lib/module/assets/icons/OxyServices.js +1 -1
  65. package/lib/module/assets/icons/OxyServices.js.map +1 -1
  66. package/lib/module/assets/illustrations/HighFive.js +55 -0
  67. package/lib/module/assets/illustrations/HighFive.js.map +1 -0
  68. package/lib/module/core/index.js +2 -2
  69. package/lib/module/core/index.js.map +1 -1
  70. package/lib/module/index.js +10 -10
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/node/createAuth.js +90 -0
  73. package/lib/module/node/createAuth.js.map +1 -0
  74. package/lib/module/node/index.js +8 -4
  75. package/lib/module/node/index.js.map +1 -1
  76. package/lib/module/package.json +1 -0
  77. package/lib/module/ui/components/Avatar.js +2 -2
  78. package/lib/module/ui/components/Avatar.js.map +1 -1
  79. package/lib/module/ui/components/FollowButton.js +3 -3
  80. package/lib/module/ui/components/FollowButton.js.map +1 -1
  81. package/lib/module/ui/components/GroupedSection.js +1 -1
  82. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  83. package/lib/module/ui/components/OxyLogo.js +1 -1
  84. package/lib/module/ui/components/OxyLogo.js.map +1 -1
  85. package/lib/module/ui/components/OxyProvider.js +143 -138
  86. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  87. package/lib/module/ui/components/OxySignInButton.js +2 -2
  88. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  89. package/lib/module/ui/components/ProfileCard.js +2 -2
  90. package/lib/module/ui/components/ProfileCard.js.map +1 -1
  91. package/lib/module/ui/components/Section.js +1 -1
  92. package/lib/module/ui/components/Section.js.map +1 -1
  93. package/lib/module/ui/components/SectionTitle.js +1 -1
  94. package/lib/module/ui/components/SectionTitle.js.map +1 -1
  95. package/lib/module/ui/components/icon/index.js +1 -1
  96. package/lib/module/ui/components/icon/index.js.map +1 -1
  97. package/lib/module/ui/components/index.js +12 -12
  98. package/lib/module/ui/components/index.js.map +1 -1
  99. package/lib/module/ui/components/internal/GroupedPillButtons.js +208 -0
  100. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -0
  101. package/lib/module/ui/components/internal/TextField.js +571 -0
  102. package/lib/module/ui/components/internal/TextField.js.map +1 -0
  103. package/lib/module/ui/context/OxyContext.js +1 -1
  104. package/lib/module/ui/context/OxyContext.js.map +1 -1
  105. package/lib/module/ui/index.js +12 -10
  106. package/lib/module/ui/index.js.map +1 -1
  107. package/lib/module/ui/navigation/OxyRouter.js +23 -18
  108. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  109. package/lib/module/ui/screens/AccountCenterScreen.js +5 -5
  110. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  111. package/lib/module/ui/screens/AccountManagementDemo.js +2 -2
  112. package/lib/module/ui/screens/AccountManagementDemo.js.map +1 -1
  113. package/lib/module/ui/screens/AccountOverviewScreen.js +4 -4
  114. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  115. package/lib/module/ui/screens/AccountSettingsScreen.js +5 -5
  116. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  117. package/lib/module/ui/screens/AccountSwitcherScreen.js +3 -3
  118. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  119. package/lib/module/ui/screens/AppInfoScreen.js +6 -6
  120. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  121. package/lib/module/ui/screens/BillingManagementScreen.js +3 -3
  122. package/lib/module/ui/screens/BillingManagementScreen.js.map +1 -1
  123. package/lib/module/ui/screens/FeedbackScreen.js +1164 -0
  124. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -0
  125. package/lib/module/ui/screens/FileManagementScreen.js +3 -3
  126. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  127. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +3 -3
  128. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  129. package/lib/module/ui/screens/ProfileScreen.js +2 -2
  130. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  131. package/lib/module/ui/screens/SessionManagementScreen.js +2 -2
  132. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  133. package/lib/module/ui/screens/SignInScreen.js +182 -304
  134. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  135. package/lib/module/ui/screens/SignUpScreen.js +810 -712
  136. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  137. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +3 -3
  138. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  139. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js +2 -2
  140. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  141. package/lib/module/ui/screens/karma/KarmaRulesScreen.js +1 -1
  142. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  143. package/lib/module/ui/store/index.js +44 -0
  144. package/lib/module/ui/store/index.js.map +1 -0
  145. package/lib/module/ui/styles/index.js +2 -2
  146. package/lib/module/ui/styles/index.js.map +1 -1
  147. package/lib/module/ui/styles/theme.js +1 -1
  148. package/lib/module/ui/styles/theme.js.map +1 -1
  149. package/lib/module/utils/index.js +1 -1
  150. package/lib/module/utils/index.js.map +1 -1
  151. package/lib/typescript/assets/illustrations/HighFive.d.ts +9 -0
  152. package/lib/typescript/assets/illustrations/HighFive.d.ts.map +1 -0
  153. package/lib/typescript/node/createAuth.d.ts +7 -0
  154. package/lib/typescript/node/createAuth.d.ts.map +1 -0
  155. package/lib/typescript/node/index.d.ts +2 -0
  156. package/lib/typescript/node/index.d.ts.map +1 -1
  157. package/lib/typescript/types/expo-vector-icons.d.ts +3 -0
  158. package/lib/typescript/types/express.d.ts +5 -0
  159. package/lib/typescript/types/react-redux.d.ts +5 -0
  160. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  161. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts +18 -0
  162. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -0
  163. package/lib/typescript/ui/components/internal/TextField.d.ts +25 -0
  164. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -0
  165. package/lib/typescript/ui/index.d.ts +2 -0
  166. package/lib/typescript/ui/index.d.ts.map +1 -1
  167. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  168. package/lib/typescript/ui/screens/FeedbackScreen.d.ts +5 -0
  169. package/lib/typescript/ui/screens/FeedbackScreen.d.ts.map +1 -0
  170. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  171. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  172. package/lib/typescript/ui/store/index.d.ts +19 -0
  173. package/lib/typescript/ui/store/index.d.ts.map +1 -0
  174. package/package.json +10 -25
  175. package/src/assets/illustrations/HighFive.tsx +41 -0
  176. package/src/node/createAuth.ts +116 -0
  177. package/src/node/index.ts +4 -0
  178. package/src/types/expo-vector-icons.d.ts +3 -0
  179. package/src/types/express.d.ts +5 -0
  180. package/src/types/react-redux.d.ts +5 -0
  181. package/src/ui/components/OxyProvider.tsx +136 -135
  182. package/src/ui/components/internal/GroupedPillButtons.tsx +253 -0
  183. package/src/ui/components/internal/TextField.tsx +694 -0
  184. package/src/ui/index.ts +6 -2
  185. package/src/ui/navigation/OxyRouter.tsx +8 -3
  186. package/src/ui/screens/FeedbackScreen.tsx +1042 -0
  187. package/src/ui/screens/SignInScreen.tsx +179 -222
  188. package/src/ui/screens/SignUpScreen.tsx +772 -608
  189. package/src/ui/store/index.ts +51 -0
@@ -0,0 +1,694 @@
1
+ import React, { useState, useCallback, forwardRef, useEffect, useRef, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ TouchableOpacity,
7
+ ActivityIndicator,
8
+ StyleSheet,
9
+ Platform,
10
+ TextInputProps,
11
+ Animated,
12
+ LayoutChangeEvent,
13
+ } from 'react-native';
14
+ import { Ionicons } from '@expo/vector-icons';
15
+ import Svg, { Path } from 'react-native-svg';
16
+ import { Animated as RNAnimated } from 'react-native';
17
+
18
+ export interface TextFieldProps extends Omit<TextInputProps, 'style'> {
19
+ label?: string;
20
+ icon?: string;
21
+ iconColor?: string;
22
+ error?: string;
23
+ success?: boolean;
24
+ loading?: boolean;
25
+ rightComponent?: React.ReactNode;
26
+ leftComponent?: React.ReactNode;
27
+ colors?: any;
28
+ containerStyle?: any;
29
+ inputStyle?: any;
30
+ labelStyle?: any;
31
+ errorStyle?: any;
32
+ variant?: 'outlined' | 'filled';
33
+ onFocus?: () => void;
34
+ onBlur?: () => void;
35
+ onChangeText?: (text: string) => void;
36
+ testID?: string;
37
+ }
38
+
39
+ const TextField = forwardRef<TextInput, TextFieldProps>(({
40
+ label,
41
+ icon,
42
+ iconColor,
43
+ error,
44
+ success = false,
45
+ loading = false,
46
+ rightComponent,
47
+ leftComponent,
48
+ colors,
49
+ containerStyle,
50
+ inputStyle,
51
+ labelStyle,
52
+ errorStyle,
53
+ variant = 'outlined',
54
+ onFocus,
55
+ onBlur,
56
+ onChangeText,
57
+ testID,
58
+ secureTextEntry,
59
+ value = '',
60
+ ...textInputProps
61
+ }, ref) => {
62
+ const [isFocused, setIsFocused] = useState(false);
63
+ const [showPassword, setShowPassword] = useState(false);
64
+ const [isLabelFloating, setIsLabelFloating] = useState(value ? true : false);
65
+ const [labelWidth, setLabelWidth] = useState(0);
66
+ const [labelLeft, setLabelLeft] = useState(0);
67
+ const [inputWidth, setInputWidth] = useState(0);
68
+ const [inputHeight, setInputHeight] = useState(64);
69
+ const borderRadius = 16;
70
+ const borderWidth = 2;
71
+
72
+ // Animation values
73
+ const labelAnim = useRef(new Animated.Value(value ? 1 : 0)).current;
74
+ const borderAnim = useRef(new Animated.Value(0)).current;
75
+
76
+ const handleFocus = useCallback(() => {
77
+ setIsFocused(true);
78
+ onFocus?.();
79
+
80
+ // Animate label to top
81
+ Animated.timing(labelAnim, {
82
+ toValue: 1,
83
+ duration: 200,
84
+ useNativeDriver: false,
85
+ }).start();
86
+
87
+ // Animate border
88
+ Animated.timing(borderAnim, {
89
+ toValue: 1,
90
+ duration: 200,
91
+ useNativeDriver: false,
92
+ }).start();
93
+ }, [onFocus, labelAnim, borderAnim]);
94
+
95
+ const handleBlur = useCallback(() => {
96
+ setIsFocused(false);
97
+ onBlur?.();
98
+
99
+ // Animate border back
100
+ Animated.timing(borderAnim, {
101
+ toValue: 0,
102
+ duration: 200,
103
+ useNativeDriver: false,
104
+ }).start();
105
+
106
+ // Keep label at top if there's a value
107
+ if (!value) {
108
+ Animated.timing(labelAnim, {
109
+ toValue: 0,
110
+ duration: 200,
111
+ useNativeDriver: false,
112
+ }).start();
113
+ }
114
+ }, [onBlur, borderAnim, labelAnim, value]);
115
+
116
+ const handleChangeText = useCallback((text: string) => {
117
+ onChangeText?.(text);
118
+
119
+ // Animate label if value changes from empty to filled or vice versa
120
+ const shouldShowLabel = text.length > 0;
121
+
122
+ if (shouldShowLabel !== isLabelFloating) {
123
+ setIsLabelFloating(shouldShowLabel);
124
+ Animated.timing(labelAnim, {
125
+ toValue: shouldShowLabel ? 1 : 0,
126
+ duration: 200,
127
+ useNativeDriver: false,
128
+ }).start();
129
+ }
130
+ }, [onChangeText, labelAnim, isLabelFloating]);
131
+
132
+ // Initialize label position based on current value
133
+ useEffect(() => {
134
+ if (value && !isLabelFloating) {
135
+ setIsLabelFloating(true);
136
+ labelAnim.setValue(1);
137
+ }
138
+ }, [value, isLabelFloating, labelAnim]);
139
+
140
+ const togglePasswordVisibility = useCallback(() => {
141
+ setShowPassword(!showPassword);
142
+ }, [showPassword]);
143
+
144
+ const getBorderColor = () => {
145
+ if (error) return colors?.error || '#D32F2F';
146
+ if (success) return colors?.success || '#2E7D32';
147
+ if (isFocused) return colors?.primary || '#d169e5';
148
+ return colors?.border || '#E0E0E0';
149
+ };
150
+
151
+ const getIconColor = () => {
152
+ if (isFocused) return colors?.primary || '#d169e5';
153
+ return iconColor || colors?.secondaryText || '#666666';
154
+ };
155
+
156
+ const getLabelColor = () => {
157
+ if (error) return colors?.error || '#D32F2F';
158
+ if (isFocused) return colors?.primary || '#d169e5';
159
+ return colors?.secondaryText || '#666666';
160
+ };
161
+
162
+ const getBackgroundColor = () => {
163
+ if (variant === 'filled') {
164
+ return colors?.inputBackground || '#F5F5F5';
165
+ }
166
+ return 'transparent';
167
+ };
168
+
169
+ const styles = createStyles(colors, variant);
170
+
171
+ const BASE_PADDING = 20;
172
+ const ICON_WIDTH = 22;
173
+ const ICON_MARGIN = 12;
174
+ const TEXT_LEFT = (icon || leftComponent) ? BASE_PADDING + ICON_WIDTH + ICON_MARGIN : BASE_PADDING;
175
+ const FLOAT_LEFT_OFFSET = 10;
176
+
177
+ const isLabelFloated = Boolean(value || isFocused);
178
+
179
+ // For web, make TextInput the primary element with absolute positioned decorations
180
+ if (Platform.OS === 'web') {
181
+ return (
182
+ <View style={[styles.container, containerStyle]}>
183
+ <View style={styles.webInputContainer}>
184
+ {/* TextInput as the primary element */}
185
+ <TextInput
186
+ ref={ref}
187
+ style={[
188
+ styles.webInput,
189
+ {
190
+ color: colors?.text || '#000000',
191
+ borderColor: 'transparent',
192
+ backgroundColor: getBackgroundColor(),
193
+ paddingLeft: TEXT_LEFT,
194
+ paddingRight: 60, // Space for right components
195
+ paddingTop: label ? 24 : 20, // Make room for floated label
196
+ paddingBottom: 8,
197
+ borderWidth: 0,
198
+ ...Platform.select({
199
+ web: { border: 'none', outline: 'none', boxShadow: 'none' },
200
+ default: {},
201
+ }),
202
+ },
203
+ inputStyle
204
+ ]}
205
+ onFocus={handleFocus}
206
+ onBlur={handleBlur}
207
+ onChangeText={handleChangeText}
208
+ secureTextEntry={secureTextEntry && !showPassword}
209
+ placeholderTextColor="transparent"
210
+ testID={testID}
211
+ autoComplete={secureTextEntry ? 'current-password' : 'off'}
212
+ spellCheck={false}
213
+ value={value}
214
+ {...textInputProps}
215
+ />
216
+
217
+ {/* SVG border with a gap for the floating label */}
218
+ <Svg
219
+ width={inputWidth}
220
+ height={inputHeight}
221
+ style={{ position: 'absolute', top: 0, left: 0, zIndex: 0 }}
222
+ pointerEvents="none"
223
+ >
224
+ {/* Calculate the path for the border with rounded corners and a gap for the label */}
225
+ <Path
226
+ d={(() => {
227
+ const y = borderWidth / 2;
228
+ const x1 = borderRadius + borderWidth / 2;
229
+ const x2 = inputWidth - borderRadius - borderWidth / 2;
230
+ const labelGapStart = isLabelFloated ? labelLeft - 4 : x1;
231
+ const labelGapEnd = isLabelFloated ? labelLeft + labelWidth + 4 : x2;
232
+ // Start at left arc
233
+ return `M${x1},${y}` +
234
+ ` A${borderRadius},${borderRadius} 0 0 1 ${borderWidth / 2},${y + borderRadius}` +
235
+ ` L${borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
236
+ ` A${borderRadius},${borderRadius} 0 0 1 ${x1},${inputHeight - borderWidth / 2}` +
237
+ ` L${x2},${inputHeight - borderWidth / 2}` +
238
+ ` A${borderRadius},${borderRadius} 0 0 1 ${inputWidth - borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
239
+ ` L${inputWidth - borderWidth / 2},${y + borderRadius}` +
240
+ ` A${borderRadius},${borderRadius} 0 0 1 ${x2},${y}` +
241
+ ` L${labelGapStart},${y}` +
242
+ ` M${labelGapEnd},${y}` +
243
+ ` L${x2},${y}`;
244
+ })()}
245
+ stroke={getBorderColor()}
246
+ strokeWidth={borderWidth}
247
+ fill="none"
248
+ />
249
+ </Svg>
250
+
251
+ {/* Floating label */}
252
+ {label && (
253
+ <Animated.Text
254
+ onLayout={e => {
255
+ setLabelWidth(e.nativeEvent.layout.width);
256
+ setLabelLeft(e.nativeEvent.layout.x);
257
+ }}
258
+ style={[
259
+ styles.webFloatingLabel,
260
+ {
261
+ color: getLabelColor(),
262
+ left: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [TEXT_LEFT, FLOAT_LEFT_OFFSET] }),
263
+ top: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [20, -14] }),
264
+ fontSize: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [16, 12] }),
265
+ backgroundColor: 'transparent',
266
+ paddingHorizontal: 4,
267
+ zIndex: 2,
268
+ },
269
+ labelStyle
270
+ ]}
271
+ >
272
+ {label}
273
+ </Animated.Text>
274
+ )}
275
+
276
+ {/* Left Icon - positioned absolutely */}
277
+ {icon && !leftComponent && (
278
+ <View style={styles.webLeftIcon}>
279
+ <Ionicons
280
+ name={icon as any}
281
+ size={22}
282
+ color={getIconColor()}
283
+ />
284
+ </View>
285
+ )}
286
+
287
+ {/* Left Component - positioned absolutely */}
288
+ {leftComponent && (
289
+ <View style={styles.webLeftComponent}>
290
+ {leftComponent}
291
+ </View>
292
+ )}
293
+
294
+ {/* Right Components - positioned absolutely */}
295
+ <View style={styles.webRightComponents}>
296
+ {loading && (
297
+ <ActivityIndicator
298
+ size="small"
299
+ color={colors?.primary || '#d169e5'}
300
+ style={styles.validationIndicator}
301
+ />
302
+ )}
303
+
304
+ {success && !loading && (
305
+ <Ionicons
306
+ name="checkmark-circle"
307
+ size={22}
308
+ color={colors?.success || '#2E7D32'}
309
+ style={styles.validationIndicator}
310
+ />
311
+ )}
312
+
313
+ {error && !loading && !success && (
314
+ <Ionicons
315
+ name="close-circle"
316
+ size={22}
317
+ color={colors?.error || '#D32F2F'}
318
+ style={styles.validationIndicator}
319
+ />
320
+ )}
321
+
322
+ {/* Password Toggle */}
323
+ {secureTextEntry && (
324
+ <TouchableOpacity
325
+ style={styles.passwordToggle}
326
+ onPress={togglePasswordVisibility}
327
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
328
+ {...(Platform.OS === 'web' && {
329
+ role: 'button',
330
+ tabIndex: 0,
331
+ onKeyPress: (e: any) => {
332
+ if (e.key === 'Enter' || e.key === ' ') {
333
+ e.preventDefault();
334
+ togglePasswordVisibility();
335
+ }
336
+ },
337
+ } as any)}
338
+ >
339
+ <Ionicons
340
+ name={showPassword ? "eye-off" : "eye"}
341
+ size={22}
342
+ color={colors?.secondaryText || '#666666'}
343
+ />
344
+ </TouchableOpacity>
345
+ )}
346
+
347
+ {/* Custom Right Component */}
348
+ {rightComponent}
349
+ </View>
350
+ </View>
351
+
352
+ {/* Error Message */}
353
+ {error && (
354
+ <View style={[styles.errorContainer, errorStyle]}>
355
+ <Ionicons
356
+ name="alert-circle"
357
+ size={16}
358
+ color={colors?.error || '#D32F2F'}
359
+ />
360
+ <Text style={[
361
+ styles.errorText,
362
+ { color: colors?.error || '#D32F2F' }
363
+ ]}>
364
+ {error}
365
+ </Text>
366
+ </View>
367
+ )}
368
+ </View>
369
+ );
370
+ }
371
+
372
+ // For mobile platforms, use Material Design structure
373
+ return (
374
+ <View style={[styles.container, containerStyle]}>
375
+ <View
376
+ style={[
377
+ styles.inputWrapper,
378
+ {
379
+ borderColor: 'transparent',
380
+ backgroundColor: getBackgroundColor(),
381
+ borderWidth: 0,
382
+ borderBottomWidth: variant === 'filled' ? 2 : (variant === 'outlined' ? 2 : 0),
383
+ },
384
+ ]}
385
+ onLayout={(e: LayoutChangeEvent) => {
386
+ setInputWidth(e.nativeEvent.layout.width);
387
+ setInputHeight(e.nativeEvent.layout.height);
388
+ }}
389
+ >
390
+ {/* Left Icon */}
391
+ {icon && !leftComponent && (
392
+ <Ionicons
393
+ name={icon as any}
394
+ size={22}
395
+ color={getIconColor()}
396
+ style={styles.inputIcon}
397
+ />
398
+ )}
399
+
400
+ {/* Left Component */}
401
+ {leftComponent}
402
+
403
+ {/* Input Content */}
404
+ <View style={styles.inputContent}>
405
+ {label && (
406
+ <>
407
+ {/* SVG border with a gap for the floating label */}
408
+ <Svg
409
+ width={inputWidth}
410
+ height={inputHeight}
411
+ style={{ position: 'absolute', top: 0, left: 0, zIndex: 0 }}
412
+ pointerEvents="none"
413
+ >
414
+ {/* Calculate the path for the border with rounded corners and a gap for the label */}
415
+ <Path
416
+ d={(() => {
417
+ const y = borderWidth / 2;
418
+ const x1 = borderRadius + borderWidth / 2;
419
+ const x2 = inputWidth - borderRadius - borderWidth / 2;
420
+ const labelGapStart = isLabelFloated ? labelLeft - 4 : x1;
421
+ const labelGapEnd = isLabelFloated ? labelLeft + labelWidth + 4 : x2;
422
+ // Start at left arc
423
+ return `M${x1},${y}` +
424
+ ` A${borderRadius},${borderRadius} 0 0 1 ${borderWidth / 2},${y + borderRadius}` +
425
+ ` L${borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
426
+ ` A${borderRadius},${borderRadius} 0 0 1 ${x1},${inputHeight - borderWidth / 2}` +
427
+ ` L${x2},${inputHeight - borderWidth / 2}` +
428
+ ` A${borderRadius},${borderRadius} 0 0 1 ${inputWidth - borderWidth / 2},${inputHeight - borderRadius - borderWidth / 2}` +
429
+ ` L${inputWidth - borderWidth / 2},${y + borderRadius}` +
430
+ ` A${borderRadius},${borderRadius} 0 0 1 ${x2},${y}` +
431
+ ` L${labelGapStart},${y}` +
432
+ ` M${labelGapEnd},${y}` +
433
+ ` L${x2},${y}`;
434
+ })()}
435
+ stroke={getBorderColor()}
436
+ strokeWidth={borderWidth}
437
+ fill="none"
438
+ />
439
+ </Svg>
440
+ {/* Floating label */}
441
+ <Animated.Text
442
+ onLayout={e => {
443
+ setLabelWidth(e.nativeEvent.layout.width);
444
+ setLabelLeft(e.nativeEvent.layout.x);
445
+ }}
446
+ style={[
447
+ styles.floatingLabel,
448
+ {
449
+ color: getLabelColor(),
450
+ left: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [TEXT_LEFT, FLOAT_LEFT_OFFSET] }),
451
+ top: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [20, -14] }),
452
+ fontSize: labelAnim.interpolate({ inputRange: [0, 1], outputRange: [16, 12] }),
453
+ zIndex: 2,
454
+ paddingHorizontal: 4,
455
+ backgroundColor: 'transparent',
456
+ },
457
+ labelStyle
458
+ ]}
459
+ >
460
+ {label}
461
+ </Animated.Text>
462
+ </>
463
+ )}
464
+ <TextInput
465
+ ref={ref}
466
+ style={[
467
+ styles.input,
468
+ {
469
+ color: colors?.text || '#000000',
470
+ backgroundColor: getBackgroundColor(),
471
+ paddingLeft: TEXT_LEFT,
472
+ paddingRight: 60, // Space for right components
473
+ paddingTop: label ? 24 : 20, // Make room for floated label
474
+ paddingBottom: 8,
475
+ borderWidth: 0,
476
+ borderColor: 'transparent',
477
+ ...Platform.select({
478
+ web: { border: 'none', outline: 'none', boxShadow: 'none' },
479
+ default: {},
480
+ }),
481
+ },
482
+ inputStyle
483
+ ]}
484
+ onFocus={handleFocus}
485
+ onBlur={handleBlur}
486
+ onChangeText={handleChangeText}
487
+ secureTextEntry={secureTextEntry && !showPassword}
488
+ placeholderTextColor="transparent"
489
+ testID={testID}
490
+ value={value}
491
+ {...textInputProps}
492
+ />
493
+ </View>
494
+
495
+ {/* Right Components */}
496
+ <View style={styles.rightComponents}>
497
+ {loading && (
498
+ <ActivityIndicator
499
+ size="small"
500
+ color={colors?.primary || '#d169e5'}
501
+ style={styles.validationIndicator}
502
+ />
503
+ )}
504
+
505
+ {success && !loading && (
506
+ <Ionicons
507
+ name="checkmark-circle"
508
+ size={22}
509
+ color={colors?.success || '#2E7D32'}
510
+ style={styles.validationIndicator}
511
+ />
512
+ )}
513
+
514
+ {error && !loading && !success && (
515
+ <Ionicons
516
+ name="close-circle"
517
+ size={22}
518
+ color={colors?.error || '#D32F2F'}
519
+ style={styles.validationIndicator}
520
+ />
521
+ )}
522
+
523
+ {/* Password Toggle */}
524
+ {secureTextEntry && (
525
+ <TouchableOpacity
526
+ style={styles.passwordToggle}
527
+ onPress={togglePasswordVisibility}
528
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
529
+ >
530
+ <Ionicons
531
+ name={showPassword ? "eye-off" : "eye"}
532
+ size={22}
533
+ color={colors?.secondaryText || '#666666'}
534
+ />
535
+ </TouchableOpacity>
536
+ )}
537
+
538
+ {/* Custom Right Component */}
539
+ {rightComponent}
540
+ </View>
541
+ </View>
542
+
543
+ {/* Error Message */}
544
+ {error && (
545
+ <View style={[styles.errorContainer, errorStyle]}>
546
+ <Ionicons
547
+ name="alert-circle"
548
+ size={16}
549
+ color={colors?.error || '#D32F2F'}
550
+ />
551
+ <Text style={[
552
+ styles.errorText,
553
+ { color: colors?.error || '#D32F2F' }
554
+ ]}>
555
+ {error}
556
+ </Text>
557
+ </View>
558
+ )}
559
+ </View>
560
+ );
561
+ });
562
+
563
+ const createStyles = (colors: any, variant: 'outlined' | 'filled') => StyleSheet.create({
564
+ container: {
565
+ width: '100%',
566
+ marginBottom: 24,
567
+ },
568
+ inputWrapper: {
569
+ flexDirection: 'row',
570
+ alignItems: 'center',
571
+ height: 64,
572
+ borderTopLeftRadius: 16,
573
+ borderTopRightRadius: 16,
574
+ borderBottomLeftRadius: 0,
575
+ borderBottomRightRadius: 0,
576
+ paddingHorizontal: 20,
577
+ backgroundColor: variant === 'filled' ? (colors?.inputBackground || '#F5F5F5') : 'transparent',
578
+ position: 'relative',
579
+ borderWidth: 0,
580
+ borderColor: 'transparent',
581
+ },
582
+ inputIcon: {
583
+ marginRight: 12,
584
+ width: 22,
585
+ height: 22,
586
+ justifyContent: 'center',
587
+ alignItems: 'center',
588
+ },
589
+ inputContent: {
590
+ flex: 1,
591
+ justifyContent: 'center',
592
+ position: 'relative',
593
+ height: 64,
594
+ },
595
+ floatingLabel: {
596
+ position: 'absolute',
597
+ fontWeight: '500',
598
+ lineHeight: 24,
599
+ backgroundColor: 'transparent',
600
+ paddingHorizontal: 4,
601
+ },
602
+ input: {
603
+ flex: 1,
604
+ fontSize: 16,
605
+ height: 24,
606
+ paddingVertical: 0,
607
+ marginTop: 8, // Space for floating label
608
+ borderWidth: 0,
609
+ borderColor: 'transparent',
610
+ },
611
+ // Web-specific styles
612
+ webInputContainer: {
613
+ position: 'relative',
614
+ height: 64,
615
+ },
616
+ webInput: {
617
+ width: '100%',
618
+ height: 64,
619
+ fontSize: 16,
620
+ paddingHorizontal: 20,
621
+ borderTopLeftRadius: 16,
622
+ borderTopRightRadius: 16,
623
+ borderBottomLeftRadius: 0,
624
+ borderBottomRightRadius: 0,
625
+ borderWidth: 0,
626
+ borderColor: 'transparent',
627
+ borderStyle: 'solid',
628
+ },
629
+ webFloatingLabel: {
630
+ position: 'absolute',
631
+ fontWeight: '500',
632
+ lineHeight: 24,
633
+ backgroundColor: 'transparent',
634
+ paddingHorizontal: 4,
635
+ },
636
+ webLeftIcon: {
637
+ position: 'absolute',
638
+ left: 20,
639
+ top: 21, // (64 - 22) / 2
640
+ zIndex: 1,
641
+ width: 22,
642
+ height: 22,
643
+ justifyContent: 'center',
644
+ alignItems: 'center',
645
+ },
646
+ webLeftComponent: {
647
+ position: 'absolute',
648
+ left: 20,
649
+ top: 0,
650
+ height: 64,
651
+ justifyContent: 'center',
652
+ zIndex: 1,
653
+ },
654
+ webRightComponents: {
655
+ position: 'absolute',
656
+ right: 20,
657
+ top: 0,
658
+ height: 64,
659
+ flexDirection: 'row',
660
+ alignItems: 'center',
661
+ zIndex: 1,
662
+ },
663
+ rightComponents: {
664
+ flexDirection: 'row',
665
+ alignItems: 'center',
666
+ },
667
+ validationIndicator: {
668
+ marginLeft: 8,
669
+ },
670
+ passwordToggle: {
671
+ padding: 4,
672
+ marginLeft: 8,
673
+ },
674
+ errorContainer: {
675
+ flexDirection: 'row',
676
+ alignItems: 'center',
677
+ padding: 12,
678
+ borderRadius: 12,
679
+ marginTop: 8,
680
+ gap: 8,
681
+ backgroundColor: (colors?.error || '#D32F2F') + '10',
682
+ borderWidth: 1,
683
+ borderColor: (colors?.error || '#D32F2F') + '30',
684
+ },
685
+ errorText: {
686
+ fontSize: 12,
687
+ fontWeight: '500',
688
+ flex: 1,
689
+ },
690
+ });
691
+
692
+ TextField.displayName = 'TextField';
693
+
694
+ export default TextField;