@@ -2,10 +2,17 @@ import { usePrivy } from "@privy-io/expo";
2
2
import React , { memo , useCallback , useEffect , useState } from "react" ;
3
3
import { Alert , TextStyle , ViewStyle } from "react-native" ;
4
4
import { create } from "zustand" ;
5
- import { Screen } from "@/components/screen/screen" ;
5
+ import { z } from "zod" ;
6
+
6
7
import { Center } from "@/design-system/Center" ;
7
8
import { VStack } from "@/design-system/VStack" ;
8
- // import { useProfile } from "../hooks/useProfile";
9
+ import { ThemedStyle , useAppTheme } from "@/theme/use-app-theme" ;
10
+ import { captureErrorWithToast } from "@/utils/capture-error" ;
11
+ import { ValidationError } from "@/utils/api/api.error" ;
12
+ import { useAddPfp } from "../../../hooks/use-add-pfp" ;
13
+ import { isAxiosError } from "axios" ;
14
+
15
+ import { Screen } from "@/components/screen/screen" ;
9
16
import { useAuthStore } from "@/features/authentication/authentication.store" ;
10
17
import { useMultiInboxStore } from "@/features/authentication/multi-inbox.store" ;
11
18
import { useCreateUser } from "@/features/current-user/use-create-user" ;
@@ -17,10 +24,47 @@ import { ProfileContactCardEditableNameInput } from "@/features/profiles/compone
17
24
import { ProfileContactCardLayout } from "@/features/profiles/components/profile-contact-card/profile-contact-card-layout" ;
18
25
import { validateProfileName } from "@/features/profiles/utils/validate-profile-name" ;
19
26
import { useHeader } from "@/navigation/use-header" ;
20
- import { ThemedStyle , useAppTheme } from "@/theme/use-app-theme" ;
21
- import { ValidationError } from "@/utils/api/api.error" ;
22
- import { captureErrorWithToast } from "@/utils/capture-error" ;
23
- import { useAddPfp } from "../../../hooks/use-add-pfp" ;
27
+ import { formatRandomUserName } from "@/features/onboarding/utils/format-random-user-name" ;
28
+ import { profileValidationSchema } from "@/features/profiles/schemas/profile-validation.schema" ;
29
+
30
+ // Request validation schema
31
+ const createUserRequestSchema = z . object ( {
32
+ inboxId : z . string ( ) ,
33
+ privyUserId : z . string ( ) ,
34
+ smartContractWalletAddress : z . string ( ) ,
35
+ profile : profileValidationSchema . pick ( { name : true , username : true } ) ,
36
+ } ) ;
37
+
38
+ type IOnboardingContactCardStore = {
39
+ name : string ;
40
+ username : string ;
41
+ nameValidationError : string ;
42
+ avatar : string ;
43
+ actions : {
44
+ setName : ( name : string ) => void ;
45
+ setUsername : ( username : string ) => void ;
46
+ setNameValidationError : ( nameValidationError : string ) => void ;
47
+ setAvatar : ( avatar : string ) => void ;
48
+ reset : ( ) => void ;
49
+ } ;
50
+ } ;
51
+
52
+ const useOnboardingContactCardStore = create < IOnboardingContactCardStore > (
53
+ ( set ) => ( {
54
+ name : "" ,
55
+ username : "" ,
56
+ nameValidationError : "" ,
57
+ avatar : "" ,
58
+ actions : {
59
+ setName : ( name : string ) => set ( { name } ) ,
60
+ setUsername : ( username : string ) => set ( { username } ) ,
61
+ setNameValidationError : ( nameValidationError : string ) =>
62
+ set ( { nameValidationError } ) ,
63
+ setAvatar : ( avatar : string ) => set ( { avatar } ) ,
64
+ reset : ( ) => set ( { name : "" , username : "" , nameValidationError : "" , avatar : "" } ) ,
65
+ } ,
66
+ } )
67
+ ) ;
24
68
25
69
export function OnboardingContactCardScreen ( ) {
26
70
const { themed } = useAppTheme ( ) ;
@@ -32,6 +76,19 @@ export function OnboardingContactCardScreen() {
32
76
const handleRealContinue = useCallback ( async ( ) => {
33
77
try {
34
78
const currentSender = useMultiInboxStore . getState ( ) . currentSender ;
79
+ const store = useOnboardingContactCardStore . getState ( ) ;
80
+
81
+ // Validate profile data first using profileValidationSchema
82
+ const profileValidation = profileValidationSchema . safeParse ( {
83
+ name : store . name ,
84
+ username : store . username ,
85
+ } ) ;
86
+
87
+ if ( ! profileValidation . success ) {
88
+ const errorMessage =
89
+ profileValidation . error . errors [ 0 ] ?. message || "Invalid profile data" ;
90
+ throw new ValidationError ( { message : errorMessage } ) ;
91
+ }
35
92
36
93
if ( ! currentSender ) {
37
94
throw new Error ( "No current sender found, please logout" ) ;
@@ -41,15 +98,25 @@ export function OnboardingContactCardScreen() {
41
98
throw new Error ( "No Privy user found, please logout" ) ;
42
99
}
43
100
44
- await createUserAsync ( {
45
- inboxId : currentSender ?. inboxId ,
46
- privyUserId : privyUser ?. id ,
47
- smartContractWalletAddress : currentSender ?. ethereumAddress ,
101
+ // Create and validate the request payload
102
+ const payload = {
103
+ inboxId : currentSender . inboxId ,
104
+ privyUserId : privyUser . id ,
105
+ smartContractWalletAddress : currentSender . ethereumAddress ,
48
106
profile : {
49
- name : useOnboardingContactCardStore . getState ( ) . name ,
107
+ name : store . name ,
108
+ username : store . username ,
50
109
} ,
51
- } ) ;
110
+ } ;
111
+
112
+ // Validate the payload against our schema
113
+ const validationResult = createUserRequestSchema . safeParse ( payload ) ;
114
+
115
+ if ( ! validationResult . success ) {
116
+ throw new Error ( "Invalid request data. Please check your input." ) ;
117
+ }
52
118
119
+ await createUserAsync ( validationResult . data ) ;
53
120
useAuthStore . getState ( ) . actions . setStatus ( "signedIn" ) ;
54
121
55
122
// TODO: Notification permissions screen
@@ -62,14 +129,20 @@ export function OnboardingContactCardScreen() {
62
129
// }
63
130
} catch ( error ) {
64
131
if ( error instanceof ValidationError ) {
65
- useOnboardingContactCardStore
66
- . getState ( )
67
- . actions . setNameValidationError ( Object . values ( error . errors ) [ 0 ] ) ;
68
132
captureErrorWithToast ( error , {
69
- message : Object . values ( error . errors ) [ 0 ] ,
133
+ message : error . message ,
70
134
} ) ;
135
+ } else if ( isAxiosError ( error ) ) {
136
+ const userMessage =
137
+ error . response ?. status === 409
138
+ ? "This username is already taken"
139
+ : "Failed to create profile. Please try again." ;
140
+
141
+ captureErrorWithToast ( error , { message : userMessage } ) ;
71
142
} else {
72
- captureErrorWithToast ( error ) ;
143
+ captureErrorWithToast ( error , {
144
+ message : "An unexpected error occurred. Please try again." ,
145
+ } ) ;
73
146
}
74
147
}
75
148
} , [ createUserAsync , privyUser ] ) ;
@@ -173,32 +246,35 @@ const $subtitleStyle: ThemedStyle<TextStyle> = ({ spacing }) => ({
173
246
marginBottom : spacing . sm ,
174
247
} ) ;
175
248
176
- const ProfileContactCardNameInput = memo (
177
- function ProfileContactCardNameInput ( ) {
178
- const [ nameValidationError , setNameValidationError ] = useState < string > ( ) ;
249
+ const ProfileContactCardNameInput = memo ( function ProfileContactCardNameInput ( ) {
250
+ const [ nameValidationError , setNameValidationError ] = useState < string > ( ) ;
179
251
180
- const handleDisplayNameChange = useCallback ( ( text : string ) => {
181
- const { isValid, error } = validateProfileName ( text ) ;
252
+ const handleDisplayNameChange = useCallback ( ( text : string ) => {
253
+ const { isValid, error } = validateProfileName ( text ) ;
182
254
183
- if ( ! isValid ) {
184
- setNameValidationError ( error ) ;
185
- } else {
186
- setNameValidationError ( undefined ) ;
187
- }
255
+ if ( ! isValid ) {
256
+ setNameValidationError ( error ) ;
257
+ useOnboardingContactCardStore . getState ( ) . actions . setUsername ( "" ) ;
258
+ return ;
259
+ }
188
260
189
- useOnboardingContactCardStore . getState ( ) . actions . setName ( text ) ;
190
- } , [ ] ) ;
191
-
192
- return (
193
- < ProfileContactCardEditableNameInput
194
- defaultValue = { useOnboardingContactCardStore . getState ( ) . name }
195
- onChangeText = { handleDisplayNameChange }
196
- status = { nameValidationError ? "error" : undefined }
197
- helper = { nameValidationError }
198
- />
199
- ) ;
200
- } ,
201
- ) ;
261
+ setNameValidationError ( undefined ) ;
262
+ const username = formatRandomUserName ( { displayName : text } ) ;
263
+
264
+ const store = useOnboardingContactCardStore . getState ( ) ;
265
+ store . actions . setName ( text ) ;
266
+ store . actions . setUsername ( username ) ;
267
+ } , [ ] ) ;
268
+
269
+ return (
270
+ < ProfileContactCardEditableNameInput
271
+ defaultValue = { useOnboardingContactCardStore . getState ( ) . name }
272
+ onChangeText = { handleDisplayNameChange }
273
+ status = { nameValidationError ? "error" : undefined }
274
+ helper = { nameValidationError }
275
+ />
276
+ ) ;
277
+ } ) ;
202
278
203
279
const ProfileContactCardAvatar = memo ( function ProfileContactCardAvatar ( ) {
204
280
const { asset, addPFP } = useAddPfp ( ) ;
@@ -221,39 +297,3 @@ const ProfileContactCardAvatar = memo(function ProfileContactCardAvatar() {
221
297
/>
222
298
) ;
223
299
} ) ;
224
-
225
- type IOnboardingContactCardState = {
226
- name : string ;
227
- nameValidationError : string ;
228
- avatar : string ;
229
- } ;
230
-
231
- type IOnboardingContactCardActions = {
232
- setName : ( name : string ) => void ;
233
- setNameValidationError : ( nameValidationError : string ) => void ;
234
- setAvatar : ( avatar : string ) => void ;
235
- reset : ( ) => void ;
236
- } ;
237
-
238
- type IOnboardingContactCardStore = IOnboardingContactCardState & {
239
- actions : IOnboardingContactCardActions ;
240
- } ;
241
-
242
- const initialState : IOnboardingContactCardState = {
243
- name : "" ,
244
- nameValidationError : "" ,
245
- avatar : "" ,
246
- } ;
247
-
248
- const useOnboardingContactCardStore = create < IOnboardingContactCardStore > (
249
- ( set , get ) => ( {
250
- ...initialState ,
251
- actions : {
252
- setName : ( name : string ) => set ( { name } ) ,
253
- setNameValidationError : ( nameValidationError : string ) =>
254
- set ( { nameValidationError } ) ,
255
- setAvatar : ( avatar : string ) => set ( { avatar } ) ,
256
- reset : ( ) => set ( initialState ) ,
257
- } ,
258
- } ) ,
259
- ) ;
0 commit comments