@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
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -9,165 +9,610 @@ import {
9
9
  Platform,
10
10
  KeyboardAvoidingView,
11
11
  ScrollView,
12
- TextStyle,
13
12
  Animated,
14
- Dimensions,
15
13
  StatusBar,
14
+ Alert,
16
15
  } from 'react-native';
17
16
  import { BaseScreenProps } from '../navigation/types';
18
17
  import { useOxy } from '../context/OxyContext';
19
- import { fontFamilies, useThemeColors, createCommonStyles } from '../styles';
20
- import OxyLogo from '../components/OxyLogo';
21
- import { BottomSheetScrollView, BottomSheetView } from '../components/bottomSheet';
18
+ import { useThemeColors, createCommonStyles } from '../styles';
19
+ import { BottomSheetScrollView } from '../components/bottomSheet';
22
20
  import { Ionicons } from '@expo/vector-icons';
23
21
  import Svg, { Path, Circle } from 'react-native-svg';
24
22
  import { toast } from '../../lib/sonner';
23
+ import HighFive from '../../assets/illustrations/HighFive';
24
+ import GroupedPillButtons from '../components/internal/GroupedPillButtons';
25
+ import TextField from '../components/internal/TextField';
26
+
27
+ // Types for better type safety
28
+ interface FormData {
29
+ username: string;
30
+ email: string;
31
+ password: string;
32
+ confirmPassword: string;
33
+ }
34
+
35
+ interface ValidationState {
36
+ status: 'idle' | 'validating' | 'valid' | 'invalid';
37
+ message: string;
38
+ }
39
+
40
+ interface PasswordVisibility {
41
+ password: boolean;
42
+ confirmPassword: boolean;
43
+ }
44
+
45
+ // Constants
46
+ const USERNAME_MIN_LENGTH = 3;
47
+ const PASSWORD_MIN_LENGTH = 8;
48
+ const VALIDATION_DEBOUNCE_MS = 800;
49
+ const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
50
+
51
+ // Email validation regex
52
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
53
+
54
+ // Styles factory function
55
+ const createStyles = (colors: any, theme: string) => StyleSheet.create({
56
+ container: {
57
+ flex: 1,
58
+ },
59
+ scrollContent: {
60
+ flexGrow: 1,
61
+ paddingHorizontal: 24,
62
+ paddingTop: 4,
63
+ paddingBottom: 20,
64
+ },
65
+ stepContainer: {
66
+ flex: 1,
67
+ justifyContent: 'flex-start',
68
+ alignItems: 'flex-start',
69
+ },
70
+ modernHeader: {
71
+ alignItems: 'flex-start',
72
+ width: '100%',
73
+ marginBottom: 24,
74
+ },
75
+ modernTitle: {
76
+ fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
77
+ fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
78
+ fontSize: 42,
79
+ lineHeight: 48,
80
+ marginBottom: 12,
81
+ textAlign: 'left',
82
+ letterSpacing: -1,
83
+ },
84
+ modernSubtitle: {
85
+ fontSize: 18,
86
+ lineHeight: 24,
87
+ textAlign: 'left',
88
+ opacity: 0.8,
89
+ marginBottom: 24,
90
+ },
91
+ welcomeImageContainer: {
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ marginVertical: 20,
95
+ },
96
+ welcomeTitle: {
97
+ fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
98
+ fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
99
+ fontSize: 42,
100
+ lineHeight: 48,
101
+ marginBottom: 12,
102
+ textAlign: 'left',
103
+ letterSpacing: -1,
104
+ },
105
+ welcomeText: {
106
+ fontSize: 18,
107
+ lineHeight: 24,
108
+ textAlign: 'left',
109
+ opacity: 0.8,
110
+ marginBottom: 24,
111
+ },
112
+ stepTitle: {
113
+ fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
114
+ fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
115
+ fontSize: 42,
116
+ lineHeight: 48,
117
+ marginBottom: 12,
118
+ textAlign: 'left',
119
+ letterSpacing: -1,
120
+ },
121
+ inputContainer: {
122
+ width: '100%',
123
+ marginBottom: 24,
124
+ },
125
+ premiumInputWrapper: {
126
+ flexDirection: 'row',
127
+ alignItems: 'center',
128
+ height: 56,
129
+ borderRadius: 16,
130
+ paddingHorizontal: 20,
131
+ borderWidth: 2,
132
+ backgroundColor: colors.inputBackground,
133
+ },
134
+ inputIcon: {
135
+ marginRight: 12,
136
+ },
137
+ inputContent: {
138
+ flex: 1,
139
+ },
140
+ modernLabel: {
141
+ fontSize: 12,
142
+ fontWeight: '500',
143
+ marginBottom: 2,
144
+ },
145
+ modernInput: {
146
+ flex: 1,
147
+ fontSize: 16,
148
+ height: '100%',
149
+ },
150
+ validationIndicator: {
151
+ marginLeft: 8,
152
+ },
153
+ validationCard: {
154
+ flexDirection: 'row',
155
+ alignItems: 'center',
156
+ padding: 12,
157
+ borderRadius: 12,
158
+ marginTop: 8,
159
+ gap: 8,
160
+ },
161
+ validationIconContainer: {
162
+ width: 32,
163
+ height: 32,
164
+ borderRadius: 16,
165
+ justifyContent: 'center',
166
+ alignItems: 'center',
167
+ marginRight: 12,
168
+ },
169
+ validationTextContainer: {
170
+ flex: 1,
171
+ },
172
+ validationTitle: {
173
+ fontSize: 12,
174
+ fontWeight: '600',
175
+ marginBottom: 2,
176
+ },
177
+ validationSubtitle: {
178
+ fontSize: 11,
179
+ opacity: 0.8,
180
+ },
181
+ passwordToggle: {
182
+ padding: 4,
183
+ },
184
+ passwordHint: {
185
+ fontSize: 12,
186
+ marginTop: 4,
187
+ },
188
+ button: {
189
+ flexDirection: 'row',
190
+ alignItems: 'center',
191
+ justifyContent: 'center',
192
+ paddingVertical: 18,
193
+ paddingHorizontal: 32,
194
+ borderRadius: 16,
195
+ marginVertical: 8,
196
+ shadowOffset: {
197
+ width: 0,
198
+ height: 4,
199
+ },
200
+ shadowOpacity: 0.3,
201
+ shadowRadius: 8,
202
+ elevation: 6,
203
+ gap: 8,
204
+ width: '100%',
205
+ },
206
+ buttonText: {
207
+ color: '#FFFFFF',
208
+ fontSize: 16,
209
+ fontWeight: '600',
210
+ letterSpacing: 0.5,
211
+ },
212
+ footerTextContainer: {
213
+ flexDirection: 'row',
214
+ justifyContent: 'center',
215
+ marginTop: 16,
216
+ },
217
+ footerText: {
218
+ fontSize: 15,
219
+ },
220
+ linkText: {
221
+ fontSize: 14,
222
+ lineHeight: 20,
223
+ fontWeight: '600',
224
+ textDecorationLine: 'underline',
225
+ },
226
+ userInfoContainer: {
227
+ padding: 20,
228
+ marginVertical: 20,
229
+ borderRadius: 24,
230
+ alignItems: 'center',
231
+ shadowColor: '#000',
232
+ shadowOpacity: 0.04,
233
+ shadowOffset: { width: 0, height: 1 },
234
+ shadowRadius: 4,
235
+ elevation: 1,
236
+ },
237
+ userInfoText: {
238
+ fontSize: 16,
239
+ marginBottom: 8,
240
+ textAlign: 'center',
241
+ },
242
+ actionButtonsContainer: {
243
+ marginTop: 24,
244
+ },
245
+ navigationButtons: {
246
+ flexDirection: 'row',
247
+ justifyContent: 'center',
248
+ marginTop: 16,
249
+ marginBottom: 8,
250
+ width: '100%',
251
+ gap: 8,
252
+ },
253
+ navButton: {
254
+ flexDirection: 'row',
255
+ alignItems: 'center',
256
+ paddingVertical: 6,
257
+ paddingHorizontal: 12,
258
+ gap: 6,
259
+ minWidth: 70,
260
+ borderWidth: 1,
261
+ shadowOffset: {
262
+ width: 0,
263
+ height: 2,
264
+ },
265
+ shadowOpacity: 0.1,
266
+ shadowRadius: 4,
267
+ elevation: 2,
268
+ },
269
+ backButton: {
270
+ backgroundColor: 'transparent',
271
+ borderTopLeftRadius: 35,
272
+ borderBottomLeftRadius: 35,
273
+ borderTopRightRadius: 12,
274
+ borderBottomRightRadius: 12,
275
+ },
276
+ nextButton: {
277
+ backgroundColor: 'transparent',
278
+ borderTopRightRadius: 35,
279
+ borderBottomRightRadius: 35,
280
+ borderTopLeftRadius: 12,
281
+ borderBottomLeftRadius: 12,
282
+ },
283
+ navButtonText: {
284
+ fontSize: 13,
285
+ fontWeight: '500',
286
+ },
287
+ progressContainer: {
288
+ flexDirection: 'row',
289
+ justifyContent: 'center',
290
+ marginBottom: 20,
291
+ marginTop: 8,
292
+ },
293
+ progressDot: {
294
+ height: 10,
295
+ width: 10,
296
+ borderRadius: 5,
297
+ marginHorizontal: 6,
298
+ borderWidth: 2,
299
+ borderColor: '#fff',
300
+ shadowColor: colors.primary,
301
+ shadowOpacity: 0.08,
302
+ shadowOffset: { width: 0, height: 1 },
303
+ shadowRadius: 2,
304
+ elevation: 1,
305
+ },
306
+ summaryContainer: {
307
+ padding: 0,
308
+ marginBottom: 24,
309
+ width: '100%',
310
+ },
311
+ summaryRow: {
312
+ flexDirection: 'row',
313
+ marginBottom: 10,
314
+ },
315
+ summaryLabel: {
316
+ fontSize: 15,
317
+ width: 90,
318
+ },
319
+ summaryValue: {
320
+ fontSize: 15,
321
+ fontWeight: '600',
322
+ flex: 1,
323
+ },
324
+ });
25
325
 
26
- const SignUpScreen: React.FC<BaseScreenProps> = ({
27
- navigate,
28
- goBack,
29
- theme,
30
- }) => {
31
- // Form data states
32
- const [username, setUsername] = useState('');
33
- const [email, setEmail] = useState('');
34
- const [password, setPassword] = useState('');
35
- const [confirmPassword, setConfirmPassword] = useState('');
36
- const [showPassword, setShowPassword] = useState(false);
37
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
38
- const [errorMessage, setErrorMessage] = useState('');
39
-
40
- // Multi-step form states
41
- const [currentStep, setCurrentStep] = useState(0);
42
- const [isInputFocused, setIsInputFocused] = useState(false);
43
- const [isValidating, setIsValidating] = useState(false);
44
- const [validationStatus, setValidationStatus] = useState<'idle' | 'validating' | 'valid' | 'invalid'>('idle');
326
+ // Custom hooks for better separation of concerns
327
+ const useFormValidation = (oxyServices: any) => {
328
+ const [validationState, setValidationState] = useState<ValidationState>({
329
+ status: 'idle',
330
+ message: ''
331
+ });
45
332
 
46
- // Cache for validation results
47
333
  const validationCache = useRef<Map<string, { available: boolean; timestamp: number }>>(new Map());
48
334
 
49
- const fadeAnim = useRef(new Animated.Value(1)).current;
50
- const slideAnim = useRef(new Animated.Value(0)).current;
51
- const heightAnim = useRef(new Animated.Value(400)).current;
52
- const [containerHeight, setContainerHeight] = useState(400);
335
+ const validateUsername = useCallback(async (username: string): Promise<boolean> => {
336
+ if (!username || username.length < USERNAME_MIN_LENGTH) {
337
+ setValidationState({ status: 'invalid', message: '' });
338
+ return false;
339
+ }
53
340
 
54
- const { signUp, isLoading, user, isAuthenticated, oxyServices } = useOxy();
341
+ // Check cache first
342
+ const cached = validationCache.current.get(username);
343
+ const now = Date.now();
344
+ if (cached && (now - cached.timestamp) < CACHE_DURATION_MS) {
345
+ const isValid = cached.available;
346
+ setValidationState({
347
+ status: isValid ? 'valid' : 'invalid',
348
+ message: isValid ? '' : 'Username is already taken'
349
+ });
350
+ return isValid;
351
+ }
55
352
 
56
- const colors = useThemeColors(theme);
57
- const commonStyles = createCommonStyles(theme);
353
+ setValidationState({ status: 'validating', message: '' });
58
354
 
59
- // Memoized styles to prevent rerenders
60
- const styles = useMemo(() => createStyles(colors, theme), [colors, theme]);
355
+ try {
356
+ const result = await oxyServices.checkUsernameAvailability(username);
357
+ const isValid = result.available;
358
+
359
+ // Cache the result
360
+ validationCache.current.set(username, {
361
+ available: isValid,
362
+ timestamp: now
363
+ });
364
+
365
+ setValidationState({
366
+ status: isValid ? 'valid' : 'invalid',
367
+ message: isValid ? '' : (result.message || 'Username is already taken')
368
+ });
369
+
370
+ return isValid;
371
+ } catch (error: any) {
372
+ console.error('Username validation error:', error);
373
+ setValidationState({
374
+ status: 'invalid',
375
+ message: 'Unable to validate username. Please try again.'
376
+ });
377
+ return false;
378
+ }
379
+ }, [oxyServices]);
61
380
 
62
- // Input focus animations
63
- const handleInputFocus = useCallback(() => {
64
- setIsInputFocused(true);
381
+ const validateEmail = useCallback((email: string): boolean => {
382
+ return EMAIL_REGEX.test(email);
65
383
  }, []);
66
384
 
67
- const handleInputBlur = useCallback(() => {
68
- setIsInputFocused(false);
385
+ const validatePassword = useCallback((password: string): boolean => {
386
+ return password.length >= PASSWORD_MIN_LENGTH;
69
387
  }, []);
70
388
 
71
- // Memoized input change handlers
72
- const handleUsernameChange = useCallback((text: string) => {
73
- setUsername(text);
74
- if (validationStatus === 'invalid') {
75
- setErrorMessage('');
76
- setValidationStatus('idle');
77
- }
78
- }, [validationStatus]);
389
+ const validatePasswordsMatch = useCallback((password: string, confirmPassword: string): boolean => {
390
+ return password === confirmPassword;
391
+ }, []);
79
392
 
80
- const handleEmailChange = useCallback((text: string) => {
81
- setEmail(text);
82
- setErrorMessage('');
393
+ // Cleanup cache on unmount
394
+ useEffect(() => {
395
+ return () => {
396
+ validationCache.current.clear();
397
+ };
83
398
  }, []);
84
399
 
85
- const handlePasswordChange = useCallback((text: string) => {
86
- setPassword(text);
87
- setErrorMessage('');
400
+ return {
401
+ validationState,
402
+ validateUsername,
403
+ validateEmail,
404
+ validatePassword,
405
+ validatePasswordsMatch
406
+ };
407
+ };
408
+
409
+ const useFormData = () => {
410
+ const [formData, setFormData] = useState<FormData>({
411
+ username: '',
412
+ email: '',
413
+ password: '',
414
+ confirmPassword: ''
415
+ });
416
+
417
+ const [passwordVisibility, setPasswordVisibility] = useState<PasswordVisibility>({
418
+ password: false,
419
+ confirmPassword: false
420
+ });
421
+
422
+ const updateField = useCallback((field: keyof FormData, value: string) => {
423
+ setFormData(prev => ({ ...prev, [field]: value }));
88
424
  }, []);
89
425
 
90
- const handleConfirmPasswordChange = useCallback((text: string) => {
91
- setConfirmPassword(text);
92
- setErrorMessage('');
426
+ const togglePasswordVisibility = useCallback((field: keyof PasswordVisibility) => {
427
+ setPasswordVisibility(prev => ({ ...prev, [field]: !prev[field] }));
93
428
  }, []);
94
429
 
95
- // Username availability validation using core services
96
- const validateUsername = useCallback(async (usernameToValidate: string) => {
97
- if (!usernameToValidate || usernameToValidate.length < 3) {
98
- setValidationStatus('invalid');
99
- return false;
100
- }
430
+ const resetForm = useCallback(() => {
431
+ setFormData({
432
+ username: '',
433
+ email: '',
434
+ password: '',
435
+ confirmPassword: ''
436
+ });
437
+ setPasswordVisibility({
438
+ password: false,
439
+ confirmPassword: false
440
+ });
441
+ }, []);
101
442
 
102
- // Check cache first (cache valid for 5 minutes)
103
- const cached = validationCache.current.get(usernameToValidate);
104
- const now = Date.now();
105
- if (cached && (now - cached.timestamp) < 5 * 60 * 1000) {
106
- setValidationStatus(cached.available ? 'valid' : 'invalid');
107
- setErrorMessage(cached.available ? '' : 'Username is already taken');
108
- return cached.available;
109
- }
443
+ return {
444
+ formData,
445
+ passwordVisibility,
446
+ updateField,
447
+ togglePasswordVisibility,
448
+ resetForm
449
+ };
450
+ };
110
451
 
111
- setIsValidating(true);
112
- setValidationStatus('validating');
452
+ // Reusable components
453
+ const ValidationIndicator: React.FC<{ status: ValidationState['status']; colors: any; styles: any }> = React.memo(({ status, colors, styles }) => {
454
+ if (status === 'validating') {
455
+ return <ActivityIndicator size="small" color={colors.primary} style={styles.validationIndicator} />;
456
+ }
457
+ if (status === 'valid') {
458
+ return <Ionicons name="checkmark-circle" size={22} color={colors.success} style={styles.validationIndicator} />;
459
+ }
460
+ if (status === 'invalid') {
461
+ return <Ionicons name="close-circle" size={22} color={colors.error} style={styles.validationIndicator} />;
462
+ }
463
+ return null;
464
+ });
113
465
 
114
- try {
115
- const result = await oxyServices.checkUsernameAvailability(usernameToValidate);
116
-
117
- if (result.available) {
118
- setValidationStatus('valid');
119
- setErrorMessage('');
120
-
121
- // Cache the result
122
- validationCache.current.set(usernameToValidate, {
123
- available: true,
124
- timestamp: now
125
- });
126
-
127
- return true;
128
- } else {
129
- setValidationStatus('invalid');
130
- setErrorMessage(result.message || 'Username is already taken');
131
-
132
- // Cache the result
133
- validationCache.current.set(usernameToValidate, {
134
- available: false,
135
- timestamp: now
136
- });
137
-
138
- return false;
466
+ const ValidationMessage: React.FC<{ validationState: ValidationState; colors: any; styles: any }> = React.memo(({ validationState, colors, styles }) => {
467
+ if (validationState.status === 'idle' || !validationState.message) return null;
468
+
469
+ const isSuccess = validationState.status === 'valid';
470
+ const backgroundColor = isSuccess ? colors.success + '10' : colors.error + '10';
471
+ const borderColor = isSuccess ? colors.success + '30' : colors.error + '30';
472
+ const iconColor = isSuccess ? colors.success : colors.error;
473
+ const iconName = isSuccess ? 'checkmark-circle' : 'alert-circle';
474
+ const title = isSuccess ? 'Username Available' : 'Username Taken';
475
+ const subtitle = isSuccess ? 'Good choice! This username is available' : validationState.message;
476
+
477
+ return (
478
+ <View style={[styles.validationCard, { backgroundColor, borderColor }]}>
479
+ <View style={[styles.validationIconContainer, { backgroundColor: iconColor + '20' }]}>
480
+ <Ionicons name={iconName} size={16} color={iconColor} />
481
+ </View>
482
+ <View style={styles.validationTextContainer}>
483
+ <Text style={[styles.validationTitle, { color: iconColor }]}>
484
+ {title}
485
+ </Text>
486
+ <Text style={[styles.validationSubtitle, { color: colors.secondaryText }]}>
487
+ {subtitle}
488
+ </Text>
489
+ </View>
490
+ </View>
491
+ );
492
+ });
493
+
494
+ const FormInput: React.FC<{
495
+ icon: string;
496
+ label: string;
497
+ value: string;
498
+ onChangeText: (text: string) => void;
499
+ secureTextEntry?: boolean;
500
+ keyboardType?: 'default' | 'email-address';
501
+ autoCapitalize?: 'none' | 'sentences';
502
+ autoCorrect?: boolean;
503
+ testID?: string;
504
+ colors: any;
505
+ styles: any;
506
+ borderColor?: string;
507
+ rightComponent?: React.ReactNode;
508
+ }> = React.memo(({
509
+ icon,
510
+ label,
511
+ value,
512
+ onChangeText,
513
+ secureTextEntry = false,
514
+ keyboardType = 'default',
515
+ autoCapitalize = 'sentences',
516
+ autoCorrect = true,
517
+ testID,
518
+ colors,
519
+ styles,
520
+ borderColor,
521
+ rightComponent
522
+ }) => (
523
+ <View style={styles.inputContainer}>
524
+ <View style={[
525
+ styles.premiumInputWrapper,
526
+ {
527
+ borderColor: borderColor || colors.border,
528
+ backgroundColor: colors.inputBackground,
529
+ shadowColor: colors.primary,
530
+ shadowOffset: { width: 0, height: 4 },
531
+ shadowOpacity: 0.1,
532
+ shadowRadius: 12,
533
+ elevation: 3,
139
534
  }
140
- } catch (error: any) {
141
- console.error('Username validation error:', error);
142
- setValidationStatus('invalid');
143
- setErrorMessage('Unable to validate username. Please try again.');
144
- return false;
145
- } finally {
146
- setIsValidating(false);
147
- }
148
- }, [oxyServices]);
535
+ ]}>
536
+ <Ionicons
537
+ name={icon as any}
538
+ size={22}
539
+ color={colors.secondaryText}
540
+ style={styles.inputIcon}
541
+ />
542
+ <View style={styles.inputContent}>
543
+ <Text style={[styles.modernLabel, { color: colors.secondaryText }]}>
544
+ {label}
545
+ </Text>
546
+ <TextInput
547
+ style={[styles.modernInput, { color: colors.text }]}
548
+ value={value}
549
+ onChangeText={onChangeText}
550
+ secureTextEntry={secureTextEntry}
551
+ keyboardType={keyboardType}
552
+ autoCapitalize={autoCapitalize}
553
+ autoCorrect={autoCorrect}
554
+ testID={testID}
555
+ placeholderTextColor="transparent"
556
+ />
557
+ </View>
558
+ {rightComponent}
559
+ </View>
560
+ </View>
561
+ ));
562
+
563
+ const ProgressIndicator: React.FC<{ currentStep: number; totalSteps: number; colors: any; styles: any }> = React.memo(({ currentStep, totalSteps, colors, styles }) => (
564
+ <View style={styles.progressContainer}>
565
+ {Array.from({ length: totalSteps }, (_, index) => (
566
+ <View
567
+ key={index}
568
+ style={[
569
+ styles.progressDot,
570
+ currentStep === index ?
571
+ { backgroundColor: colors.primary, width: 24 } :
572
+ { backgroundColor: colors.border }
573
+ ]}
574
+ />
575
+ ))}
576
+ </View>
577
+ ));
578
+
579
+ // Main component
580
+ const SignUpScreen: React.FC<BaseScreenProps> = ({
581
+ navigate,
582
+ goBack,
583
+ onAuthenticated,
584
+ theme,
585
+ }) => {
586
+ const { signUp, isLoading, user, isAuthenticated, oxyServices } = useOxy();
587
+ const colors = useThemeColors(theme);
588
+
589
+ // Form state
590
+ const { formData, passwordVisibility, updateField, togglePasswordVisibility, resetForm } = useFormData();
591
+ const { validationState, validateUsername, validateEmail, validatePassword, validatePasswordsMatch } = useFormValidation(oxyServices);
592
+
593
+ // UI state
594
+ const [currentStep, setCurrentStep] = useState(0);
595
+ const [errorMessage, setErrorMessage] = useState('');
596
+
597
+ // Animation refs
598
+ const fadeAnim = useRef(new Animated.Value(1)).current;
599
+ const slideAnim = useRef(new Animated.Value(0)).current;
600
+
601
+ // Memoized styles
602
+ const styles = useMemo(() => createStyles(colors, theme), [colors, theme]);
149
603
 
150
604
  // Debounced username validation
151
605
  useEffect(() => {
152
- if (!username || username.length < 3) {
153
- setValidationStatus('idle');
154
- setErrorMessage('');
606
+ if (!formData.username || formData.username.length < USERNAME_MIN_LENGTH) {
155
607
  return;
156
608
  }
157
609
 
158
610
  const timeoutId = setTimeout(() => {
159
- validateUsername(username);
160
- }, 800);
611
+ validateUsername(formData.username);
612
+ }, VALIDATION_DEBOUNCE_MS);
161
613
 
162
614
  return () => clearTimeout(timeoutId);
163
- }, [username, validateUsername]);
164
-
165
- // Cleanup cache on unmount
166
- useEffect(() => {
167
- return () => {
168
- validationCache.current.clear();
169
- };
170
- }, []);
615
+ }, [formData.username, validateUsername]);
171
616
 
172
617
  // Animation functions
173
618
  const animateTransition = useCallback((nextStep: number) => {
@@ -206,90 +651,88 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
206
651
  }
207
652
  }, [currentStep, animateTransition]);
208
653
 
209
- const validateEmail = useCallback((email: string) => {
210
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
211
- return emailRegex.test(email);
212
- }, []);
213
-
214
- const handleSignUp = useCallback(async () => {
215
- if (!username || !email || !password || !confirmPassword) {
216
- toast.error('Please fill in all fields');
217
- return;
218
- }
219
-
220
- if (!validateEmail(email)) {
221
- toast.error('Please enter a valid email address');
654
+ // Form validation helpers
655
+ const isIdentityStepValid = useCallback(() => {
656
+ return formData.username &&
657
+ formData.email &&
658
+ validateEmail(formData.email) &&
659
+ validationState.status === 'valid';
660
+ }, [formData.username, formData.email, validateEmail, validationState.status]);
661
+
662
+ const isSecurityStepValid = useCallback(() => {
663
+ return formData.password &&
664
+ validatePassword(formData.password) &&
665
+ validatePasswordsMatch(formData.password, formData.confirmPassword);
666
+ }, [formData.password, formData.confirmPassword, validatePassword, validatePasswordsMatch]);
667
+
668
+ // Custom next handlers for validation
669
+ const handleIdentityNext = useCallback(() => {
670
+ if (!isIdentityStepValid()) {
671
+ toast.error('Please enter a valid username and email.');
222
672
  return;
223
673
  }
674
+ nextStep();
675
+ }, [isIdentityStepValid, nextStep]);
224
676
 
225
- if (validationStatus !== 'valid') {
226
- toast.error('Please enter a valid username');
677
+ const handleSecurityNext = useCallback(() => {
678
+ if (!isSecurityStepValid()) {
679
+ toast.error('Please enter a valid password and confirm it.');
227
680
  return;
228
681
  }
682
+ nextStep();
683
+ }, [isSecurityStepValid, nextStep]);
229
684
 
230
- if (password !== confirmPassword) {
231
- toast.error('Passwords do not match');
232
- return;
233
- }
234
-
235
- if (password.length < 8) {
236
- toast.error('Password must be at least 8 characters long');
685
+ // Sign up handler
686
+ const handleSignUp = useCallback(async () => {
687
+ if (!isIdentityStepValid() || !isSecurityStepValid()) {
688
+ toast.error('Please fill in all fields correctly');
237
689
  return;
238
690
  }
239
691
 
240
692
  try {
241
693
  setErrorMessage('');
242
- await signUp(username, email, password);
694
+ const user = await signUp(formData.username, formData.email, formData.password);
243
695
  toast.success('Account created successfully! Welcome to Oxy!');
696
+
697
+ if (onAuthenticated) {
698
+ onAuthenticated(user);
699
+ }
700
+
701
+ resetForm();
244
702
  } catch (error: any) {
245
703
  toast.error(error.message || 'Sign up failed');
246
704
  }
247
- }, [username, email, password, confirmPassword, validationStatus, validateEmail, signUp]);
705
+ }, [formData, isIdentityStepValid, isSecurityStepValid, signUp, onAuthenticated, resetForm]);
248
706
 
249
707
  // Step components
250
- const renderWelcomeStep = useMemo(() => (
708
+ const renderWelcomeStep = useCallback(() => (
251
709
  <Animated.View style={[
252
710
  styles.stepContainer,
253
711
  { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }
254
712
  ]}>
255
- <View style={styles.welcomeImageContainer}>
256
- {/* Large illustration, not inside a circle */}
257
- <Svg width={220} height={120} viewBox="0 0 220 120">
258
- {/* Example: Abstract friendly illustration */}
259
- <Path
260
- d="M30 100 Q60 20 110 60 Q160 100 190 40"
261
- stroke={colors.primary}
262
- strokeWidth="8"
263
- fill="none"
264
- />
265
- <Circle cx="60" cy="60" r="18" fill={colors.primary} opacity="0.18" />
266
- <Circle cx="110" cy="60" r="24" fill={colors.primary} opacity="0.25" />
267
- <Circle cx="170" cy="50" r="14" fill={colors.primary} opacity="0.15" />
268
- {/* Smiling face */}
269
- <Circle cx="110" cy="60" r="32" fill="#fff" opacity="0.7" />
270
- <Circle cx="100" cy="55" r="4" fill={colors.primary} />
271
- <Circle cx="120" cy="55" r="4" fill={colors.primary} />
272
- <Path
273
- d="M104 68 Q110 75 116 68"
274
- stroke={colors.primary}
275
- strokeWidth="2"
276
- fill="none"
277
- strokeLinecap="round"
278
- />
279
- </Svg>
280
- </View>
713
+ <HighFive width={100} height={100} />
281
714
 
282
- <Text style={[styles.welcomeText, { color: colors.text }]}>
283
- We're excited to have you join us. Let's get your account set up in just a few easy steps.
284
- </Text>
715
+ <View style={styles.modernHeader}>
716
+ <Text style={[styles.modernTitle, { color: colors.text }]}>
717
+ Welcome to Oxy
718
+ </Text>
719
+ <Text style={[styles.modernSubtitle, { color: colors.secondaryText }]}>
720
+ We're excited to have you join us. Let's get your account set up in just a few easy steps.
721
+ </Text>
722
+ </View>
285
723
 
286
- <TouchableOpacity
287
- style={[styles.button, { backgroundColor: colors.primary }]}
288
- onPress={nextStep}
289
- testID="welcome-next-button"
290
- >
291
- <Text style={styles.buttonText}>Get Started</Text>
292
- </TouchableOpacity>
724
+ <GroupedPillButtons
725
+ buttons={[
726
+ {
727
+ text: 'Get Started',
728
+ onPress: nextStep,
729
+ icon: 'arrow-forward',
730
+ variant: 'primary',
731
+ testID: 'welcome-next-button',
732
+ },
733
+ ]}
734
+ colors={colors}
735
+ />
293
736
 
294
737
  <View style={styles.footerTextContainer}>
295
738
  <Text style={[styles.footerText, { color: colors.text }]}>
@@ -302,255 +745,179 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
302
745
  </Animated.View>
303
746
  ), [fadeAnim, slideAnim, colors, nextStep, navigate, styles]);
304
747
 
305
- const renderIdentityStep = useMemo(() => (
748
+ const renderIdentityStep = useCallback(() => (
306
749
  <Animated.View style={[
307
750
  styles.stepContainer,
308
751
  { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }
309
752
  ]}>
310
- <Text style={[styles.stepTitle, { color: colors.text }]}>Who are you?</Text>
311
-
312
- <View style={styles.inputContainer}>
313
- <Text style={[styles.label, { color: colors.text }]}>Username</Text>
314
- <View style={{ position: 'relative' }}>
315
- <TextInput
316
- style={[
317
- styles.input,
318
- { backgroundColor: colors.inputBackground, borderColor: colors.border, color: colors.text }
319
- ]}
320
- placeholder="Choose a username"
321
- placeholderTextColor={colors.placeholder}
322
- value={username}
323
- onChangeText={handleUsernameChange}
324
- autoCapitalize="none"
325
- testID="username-input"
326
- />
327
- {validationStatus === 'validating' && (
328
- <ActivityIndicator size="small" color={colors.primary} style={styles.validationIndicator} />
329
- )}
330
- {validationStatus === 'valid' && (
331
- <Ionicons name="checkmark-circle" size={20} color={colors.success} style={styles.validationIndicator} />
332
- )}
333
- {validationStatus === 'invalid' && username.length >= 3 && (
334
- <Ionicons name="close-circle" size={20} color={colors.error} style={styles.validationIndicator} />
335
- )}
336
- </View>
337
-
338
- {/* Validation feedback */}
339
- {validationStatus === 'valid' && (
340
- <View style={[styles.validationSuccessCard, { backgroundColor: colors.success + '15' }]}>
341
- <Ionicons name="checkmark-circle" size={16} color={colors.success} />
342
- <Text style={[styles.validationText, { color: colors.success }]}>
343
- Username is available
344
- </Text>
345
- </View>
346
- )}
347
-
348
- {validationStatus === 'invalid' && username.length >= 3 && (
349
- <View style={[styles.validationErrorCard, { backgroundColor: colors.error + '15' }]}>
350
- <Ionicons name="alert-circle" size={16} color={colors.error} />
351
- <Text style={[styles.validationText, { color: colors.error }]}>
352
- {errorMessage || 'Username is already taken'}
353
- </Text>
354
- </View>
355
- )}
753
+ <View style={styles.modernHeader}>
754
+ <Text style={[styles.stepTitle, { color: colors.text }]}>Who are you?</Text>
356
755
  </View>
357
756
 
358
- <View style={styles.inputContainer}>
359
- <Text style={[styles.label, { color: colors.text }]}>Email</Text>
360
- <TextInput
361
- style={[
362
- styles.input,
363
- { backgroundColor: colors.inputBackground, borderColor: colors.border, color: colors.text }
364
- ]}
365
- placeholder="Enter your email"
366
- placeholderTextColor={colors.placeholder}
367
- value={email}
368
- onChangeText={handleEmailChange}
369
- autoCapitalize="none"
370
- keyboardType="email-address"
371
- testID="email-input"
372
- />
373
- </View>
757
+ <TextField
758
+ icon="person-outline"
759
+ label="Username"
760
+ value={formData.username}
761
+ onChangeText={(text) => {
762
+ updateField('username', text);
763
+ setErrorMessage('');
764
+ }}
765
+ autoCapitalize="none"
766
+ autoCorrect={false}
767
+ testID="username-input"
768
+ colors={colors}
769
+ variant="filled"
770
+ error={validationState.status === 'invalid' ? validationState.message : undefined}
771
+ loading={validationState.status === 'validating'}
772
+ success={validationState.status === 'valid'}
773
+ />
374
774
 
375
- <View style={styles.navigationButtons}>
376
- <TouchableOpacity
377
- style={[styles.navButton, styles.backButton, { borderColor: colors.border }]}
378
- onPress={prevStep}
379
- >
380
- <Text style={[styles.navButtonText, { color: colors.text }]}>Back</Text>
381
- </TouchableOpacity>
775
+ <ValidationMessage validationState={validationState} colors={colors} styles={styles} />
776
+
777
+ <TextField
778
+ icon="mail-outline"
779
+ label="Email"
780
+ value={formData.email}
781
+ onChangeText={(text) => {
782
+ updateField('email', text);
783
+ }}
784
+ keyboardType="email-address"
785
+ autoCapitalize="none"
786
+ autoCorrect={false}
787
+ testID="email-input"
788
+ colors={colors}
789
+ variant="filled"
790
+ error={formData.email && !validateEmail(formData.email) ? 'Please enter a valid email address' : undefined}
791
+ />
382
792
 
383
- <TouchableOpacity
384
- style={[styles.navButton, styles.nextButton, { backgroundColor: colors.primary }]}
385
- onPress={nextStep}
386
- disabled={!username || !email || !validateEmail(email) || validationStatus !== 'valid'}
387
- >
388
- <Text style={[styles.navButtonText, { color: '#FFFFFF' }]}>Next</Text>
389
- </TouchableOpacity>
390
- </View>
793
+ <GroupedPillButtons
794
+ buttons={[
795
+ {
796
+ text: 'Back',
797
+ onPress: prevStep,
798
+ icon: 'arrow-back',
799
+ variant: 'transparent',
800
+ },
801
+ {
802
+ text: 'Next',
803
+ onPress: handleIdentityNext,
804
+ icon: 'arrow-forward',
805
+ variant: 'primary',
806
+ },
807
+ ]}
808
+ colors={colors}
809
+ />
391
810
  </Animated.View>
392
- ), [fadeAnim, slideAnim, colors, username, email, validationStatus, errorMessage, handleUsernameChange, handleEmailChange, validateEmail, prevStep, nextStep, styles]);
811
+ ), [fadeAnim, slideAnim, colors, formData, validationState, updateField, setErrorMessage, prevStep, handleIdentityNext, styles]);
393
812
 
394
- const renderSecurityStep = useMemo(() => (
813
+ const renderSecurityStep = useCallback(() => (
395
814
  <Animated.View style={[
396
815
  styles.stepContainer,
397
816
  { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }
398
817
  ]}>
399
- <Text style={[styles.stepTitle, { color: colors.text }]}>Secure your account</Text>
400
-
401
- <View style={styles.inputContainer}>
402
- <Text style={[styles.label, { color: colors.text }]}>Password</Text>
403
- <View style={{ position: 'relative' }}>
404
- <TextInput
405
- style={[
406
- styles.input,
407
- { backgroundColor: colors.inputBackground, borderColor: colors.border, color: colors.text }
408
- ]}
409
- placeholder="Create a password"
410
- placeholderTextColor={colors.placeholder}
411
- value={password}
412
- onChangeText={handlePasswordChange}
413
- secureTextEntry={!showPassword}
414
- testID="password-input"
415
- />
416
- <TouchableOpacity
417
- style={styles.passwordToggle}
418
- onPress={() => setShowPassword(!showPassword)}
419
- >
420
- <Ionicons
421
- name={showPassword ? 'eye-off' : 'eye'}
422
- size={20}
423
- color={colors.placeholder}
424
- />
425
- </TouchableOpacity>
426
- </View>
427
- <Text style={[styles.passwordHint, { color: colors.secondaryText }]}>
428
- Password must be at least 8 characters long
429
- </Text>
818
+ <View style={styles.modernHeader}>
819
+ <Text style={[styles.stepTitle, { color: colors.text }]}>Secure your account</Text>
430
820
  </View>
431
821
 
432
- <View style={styles.inputContainer}>
433
- <Text style={[styles.label, { color: colors.text }]}>Confirm Password</Text>
434
- <View style={{ position: 'relative' }}>
435
- <TextInput
436
- style={[
437
- styles.input,
438
- { backgroundColor: colors.inputBackground, borderColor: colors.border, color: colors.text }
439
- ]}
440
- placeholder="Confirm your password"
441
- placeholderTextColor={colors.placeholder}
442
- value={confirmPassword}
443
- onChangeText={handleConfirmPasswordChange}
444
- secureTextEntry={!showConfirmPassword}
445
- testID="confirm-password-input"
446
- />
447
- <TouchableOpacity
448
- style={styles.passwordToggle}
449
- onPress={() => setShowConfirmPassword(!showConfirmPassword)}
450
- >
451
- <Ionicons
452
- name={showConfirmPassword ? 'eye-off' : 'eye'}
453
- size={20}
454
- color={colors.placeholder}
455
- />
456
- </TouchableOpacity>
457
- </View>
458
- </View>
822
+ <TextField
823
+ icon="lock-closed-outline"
824
+ label="Password"
825
+ value={formData.password}
826
+ onChangeText={(text) => {
827
+ updateField('password', text);
828
+ }}
829
+ secureTextEntry={!passwordVisibility.password}
830
+ autoCapitalize="none"
831
+ autoCorrect={false}
832
+ testID="password-input"
833
+ colors={colors}
834
+ variant="filled"
835
+ error={formData.password && !validatePassword(formData.password) ? `Password must be at least ${PASSWORD_MIN_LENGTH} characters` : undefined}
836
+ />
459
837
 
460
- <View style={styles.navigationButtons}>
461
- <TouchableOpacity
462
- style={[styles.navButton, styles.backButton, { borderColor: colors.border }]}
463
- onPress={prevStep}
464
- >
465
- <Text style={[styles.navButtonText, { color: colors.text }]}>Back</Text>
466
- </TouchableOpacity>
838
+ <Text style={[styles.passwordHint, { color: colors.secondaryText }]}>Password must be at least {PASSWORD_MIN_LENGTH} characters long</Text>
839
+
840
+ <TextField
841
+ icon="lock-closed-outline"
842
+ label="Confirm Password"
843
+ value={formData.confirmPassword}
844
+ onChangeText={(text) => {
845
+ updateField('confirmPassword', text);
846
+ }}
847
+ secureTextEntry={!passwordVisibility.confirmPassword}
848
+ autoCapitalize="none"
849
+ autoCorrect={false}
850
+ testID="confirm-password-input"
851
+ colors={colors}
852
+ variant="filled"
853
+ error={formData.confirmPassword && !validatePasswordsMatch(formData.password, formData.confirmPassword) ? 'Passwords do not match' : undefined}
854
+ />
467
855
 
468
- <TouchableOpacity
469
- style={[styles.navButton, styles.nextButton, { backgroundColor: colors.primary }]}
470
- onPress={nextStep}
471
- disabled={!password || password.length < 8 || password !== confirmPassword}
472
- >
473
- <Text style={[styles.navButtonText, { color: '#FFFFFF' }]}>Next</Text>
474
- </TouchableOpacity>
475
- </View>
856
+ <GroupedPillButtons
857
+ buttons={[
858
+ {
859
+ text: 'Back',
860
+ onPress: prevStep,
861
+ icon: 'arrow-back',
862
+ variant: 'transparent',
863
+ },
864
+ {
865
+ text: 'Next',
866
+ onPress: handleSecurityNext,
867
+ icon: 'arrow-forward',
868
+ variant: 'primary',
869
+ },
870
+ ]}
871
+ colors={colors}
872
+ />
476
873
  </Animated.View>
477
- ), [fadeAnim, slideAnim, colors, password, confirmPassword, showPassword, showConfirmPassword, handlePasswordChange, handleConfirmPasswordChange, prevStep, nextStep, styles]);
874
+ ), [fadeAnim, slideAnim, colors, formData, passwordVisibility, updateField, setErrorMessage, togglePasswordVisibility, prevStep, handleSecurityNext, styles]);
478
875
 
479
- const renderSummaryStep = useMemo(() => (
876
+ const renderSummaryStep = useCallback(() => (
480
877
  <Animated.View style={[
481
878
  styles.stepContainer,
482
879
  { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }
483
880
  ]}>
484
- <Text style={[styles.stepTitle, { color: colors.text }]}>Ready to join</Text>
881
+ <View style={styles.modernHeader}>
882
+ <Text style={[styles.stepTitle, { color: colors.text }]}>Ready to join</Text>
883
+ </View>
485
884
 
486
885
  <View style={styles.summaryContainer}>
487
886
  <View style={styles.summaryRow}>
488
887
  <Text style={[styles.summaryLabel, { color: colors.secondaryText }]}>Username:</Text>
489
- <Text style={[styles.summaryValue, { color: colors.text }]}>{username}</Text>
888
+ <Text style={[styles.summaryValue, { color: colors.text }]}>{formData.username}</Text>
490
889
  </View>
491
890
 
492
891
  <View style={styles.summaryRow}>
493
892
  <Text style={[styles.summaryLabel, { color: colors.secondaryText }]}>Email:</Text>
494
- <Text style={[styles.summaryValue, { color: colors.text }]}>{email}</Text>
893
+ <Text style={[styles.summaryValue, { color: colors.text }]}>{formData.email}</Text>
495
894
  </View>
496
895
  </View>
497
896
 
498
- <TouchableOpacity
499
- style={[styles.button, { backgroundColor: colors.primary }]}
500
- onPress={handleSignUp}
501
- disabled={isLoading}
502
- testID="signup-button"
503
- >
504
- {isLoading ? (
505
- <ActivityIndicator color="#FFFFFF" size="small" />
506
- ) : (
507
- <Text style={styles.buttonText}>Create Account</Text>
508
- )}
509
- </TouchableOpacity>
510
-
511
- <View style={styles.navigationButtons}>
512
- <TouchableOpacity
513
- style={[styles.navButton, styles.backButton, { borderColor: colors.border }]}
514
- onPress={prevStep}
515
- >
516
- <Text style={[styles.navButtonText, { color: colors.text }]}>Back</Text>
517
- </TouchableOpacity>
518
- </View>
897
+ <GroupedPillButtons
898
+ buttons={[
899
+ {
900
+ text: 'Back',
901
+ onPress: prevStep,
902
+ icon: 'arrow-back',
903
+ variant: 'transparent',
904
+ },
905
+ {
906
+ text: 'Create Account',
907
+ onPress: handleSignUp,
908
+ icon: 'checkmark',
909
+ variant: 'primary',
910
+ disabled: isLoading,
911
+ loading: isLoading,
912
+ testID: 'signup-button',
913
+ },
914
+ ]}
915
+ colors={colors}
916
+ />
519
917
  </Animated.View>
520
- ), [fadeAnim, slideAnim, colors, username, email, isLoading, handleSignUp, prevStep, styles]);
521
-
522
- const renderProgressIndicators = useMemo(() => (
523
- <View style={styles.progressContainer}>
524
- {[0, 1, 2, 3].map((step) => (
525
- <View
526
- key={step}
527
- style={[
528
- styles.progressDot,
529
- currentStep === step ?
530
- { backgroundColor: colors.primary, width: 24 } :
531
- { backgroundColor: colors.border }
532
- ]}
533
- />
534
- ))}
535
- </View>
536
- ), [currentStep, colors, styles]);
537
-
538
- const renderCurrentStep = useCallback(() => {
539
- switch (currentStep) {
540
- case 0:
541
- return renderWelcomeStep;
542
- case 1:
543
- return renderIdentityStep;
544
- case 2:
545
- return renderSecurityStep;
546
- case 3:
547
- return renderSummaryStep;
548
- default:
549
- return renderWelcomeStep;
550
- }
551
- }, [currentStep, renderWelcomeStep, renderIdentityStep, renderSecurityStep, renderSummaryStep]);
918
+ ), [fadeAnim, slideAnim, colors, formData, isLoading, handleSignUp, prevStep, styles]);
552
919
 
553
- // If user is already authenticated, show user info and account center option
920
+ // If user is already authenticated, show user info
554
921
  if (user && isAuthenticated) {
555
922
  return (
556
923
  <KeyboardAvoidingView
@@ -594,6 +961,17 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
594
961
  );
595
962
  }
596
963
 
964
+ // Render current step
965
+ const renderCurrentStep = useCallback(() => {
966
+ switch (currentStep) {
967
+ case 0: return renderWelcomeStep();
968
+ case 1: return renderIdentityStep();
969
+ case 2: return renderSecurityStep();
970
+ case 3: return renderSummaryStep();
971
+ default: return renderWelcomeStep();
972
+ }
973
+ }, [currentStep, renderWelcomeStep, renderIdentityStep, renderSecurityStep, renderSummaryStep]);
974
+
597
975
  return (
598
976
  <KeyboardAvoidingView
599
977
  style={[styles.container, { backgroundColor: colors.background }]}
@@ -609,225 +987,11 @@ const SignUpScreen: React.FC<BaseScreenProps> = ({
609
987
  showsVerticalScrollIndicator={false}
610
988
  keyboardShouldPersistTaps="handled"
611
989
  >
612
- {renderProgressIndicators}
990
+ <ProgressIndicator currentStep={currentStep} totalSteps={4} colors={colors} styles={styles} />
613
991
  {renderCurrentStep()}
614
992
  </ScrollView>
615
993
  </KeyboardAvoidingView>
616
994
  );
617
995
  };
618
996
 
619
- // Memoized styles creation
620
- const createStyles = (colors: any, theme: string) => StyleSheet.create({
621
- container: {
622
- flex: 1,
623
- },
624
- scrollContent: {
625
- flexGrow: 1,
626
- paddingHorizontal: 24,
627
- paddingTop: 40,
628
- paddingBottom: 40,
629
- },
630
- stepContainer: {
631
- flex: 1,
632
- justifyContent: 'center',
633
- alignItems: 'center',
634
- minHeight: 500,
635
- },
636
- welcomeImageContainer: {
637
- alignItems: 'center',
638
- justifyContent: 'center',
639
- marginVertical: 30,
640
- },
641
- welcomeTitle: {
642
- fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
643
- fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
644
- fontSize: 42,
645
- lineHeight: 48,
646
- marginBottom: 24,
647
- textAlign: 'left',
648
- letterSpacing: -1,
649
- },
650
- welcomeText: {
651
- fontSize: 16,
652
- textAlign: 'left',
653
- marginBottom: 30,
654
- lineHeight: 24,
655
- },
656
- stepTitle: {
657
- fontFamily: Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold',
658
- fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
659
- fontSize: 34,
660
- marginBottom: 20,
661
- color: colors.primary,
662
- maxWidth: '90%',
663
- textAlign: 'left',
664
- },
665
- inputContainer: {
666
- marginBottom: 18,
667
- width: '100%',
668
- },
669
- label: {
670
- fontSize: 15,
671
- marginBottom: 8,
672
- fontWeight: '500',
673
- letterSpacing: 0.1,
674
- },
675
- input: {
676
- height: 48,
677
- borderRadius: 16,
678
- paddingHorizontal: 16,
679
- borderWidth: 1,
680
- fontSize: 16,
681
- marginBottom: 2,
682
- },
683
- validationIndicator: {
684
- position: 'absolute',
685
- right: 16,
686
- top: 14,
687
- },
688
- validationSuccessCard: {
689
- flexDirection: 'row',
690
- alignItems: 'center',
691
- padding: 12,
692
- borderRadius: 12,
693
- marginTop: 8,
694
- gap: 8,
695
- },
696
- validationErrorCard: {
697
- flexDirection: 'row',
698
- alignItems: 'center',
699
- padding: 12,
700
- borderRadius: 12,
701
- marginTop: 8,
702
- gap: 8,
703
- },
704
- validationText: {
705
- fontSize: 12,
706
- fontWeight: '500',
707
- },
708
- passwordToggle: {
709
- position: 'absolute',
710
- right: 16,
711
- top: 14,
712
- padding: 4,
713
- },
714
- passwordHint: {
715
- fontSize: 12,
716
- marginTop: 4,
717
- },
718
- button: {
719
- height: 48,
720
- borderRadius: 24,
721
- alignItems: 'center',
722
- justifyContent: 'center',
723
- marginTop: 24,
724
- shadowColor: colors.primary,
725
- shadowOpacity: 0.12,
726
- shadowOffset: { width: 0, height: 2 },
727
- shadowRadius: 8,
728
- elevation: 2,
729
- width: '100%',
730
- },
731
- buttonText: {
732
- color: '#FFFFFF',
733
- fontSize: 17,
734
- fontWeight: '700',
735
- letterSpacing: 0.2,
736
- },
737
- footerTextContainer: {
738
- flexDirection: 'row',
739
- justifyContent: 'center',
740
- marginTop: 28,
741
- },
742
- footerText: {
743
- fontSize: 15,
744
- },
745
- linkText: {
746
- fontSize: 15,
747
- fontWeight: '700',
748
- },
749
- userInfoContainer: {
750
- padding: 20,
751
- marginVertical: 20,
752
- borderRadius: 24,
753
- alignItems: 'center',
754
- shadowColor: '#000',
755
- shadowOpacity: 0.04,
756
- shadowOffset: { width: 0, height: 1 },
757
- shadowRadius: 4,
758
- elevation: 1,
759
- },
760
- userInfoText: {
761
- fontSize: 16,
762
- marginBottom: 8,
763
- textAlign: 'center',
764
- },
765
- actionButtonsContainer: {
766
- marginTop: 24,
767
- },
768
- navigationButtons: {
769
- flexDirection: 'row',
770
- justifyContent: 'space-between',
771
- alignItems: 'center',
772
- marginTop: 28,
773
- width: '100%',
774
- },
775
- navButton: {
776
- borderRadius: 24,
777
- height: 44,
778
- alignItems: 'center',
779
- justifyContent: 'center',
780
- paddingHorizontal: 28,
781
- backgroundColor: '#F3E5F5',
782
- },
783
- backButton: {
784
- backgroundColor: 'transparent',
785
- borderWidth: 1,
786
- },
787
- nextButton: {
788
- minWidth: 100,
789
- },
790
- navButtonText: {
791
- fontSize: 16,
792
- fontWeight: '700',
793
- },
794
- progressContainer: {
795
- flexDirection: 'row',
796
- justifyContent: 'center',
797
- marginBottom: 20,
798
- marginTop: 8,
799
- },
800
- progressDot: {
801
- height: 10,
802
- width: 10,
803
- borderRadius: 5,
804
- marginHorizontal: 6,
805
- borderWidth: 2,
806
- borderColor: '#fff',
807
- shadowColor: colors.primary,
808
- shadowOpacity: 0.08,
809
- shadowOffset: { width: 0, height: 1 },
810
- shadowRadius: 2,
811
- elevation: 1,
812
- },
813
- summaryContainer: {
814
- padding: 0,
815
- marginBottom: 24,
816
- width: '100%',
817
- },
818
- summaryRow: {
819
- flexDirection: 'row',
820
- marginBottom: 10,
821
- },
822
- summaryLabel: {
823
- fontSize: 15,
824
- width: 90,
825
- },
826
- summaryValue: {
827
- fontSize: 15,
828
- fontWeight: '600',
829
- flex: 1,
830
- },
831
- });
832
-
833
997
  export default SignUpScreen;