Skip to content

Commit f839300

Browse files
authored
Add X username to personal data (#882)
1 parent 8b9f6c3 commit f839300

File tree

6 files changed

+179
-73
lines changed

6 files changed

+179
-73
lines changed

packages/app/components/FormFields/TextField.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const TextField = (
3939
},
4040
} = useTsController<string>()
4141

42-
const { label, placeholder, isOptional, maxLength, isEmail } = useStringFieldInfo()
42+
const { label, placeholder, maxLength, isEmail } = useStringFieldInfo()
4343
const themeName = useThemeName()
4444
const id = useId()
4545
const disabled = isSubmitting
@@ -57,7 +57,7 @@ export const TextField = (
5757
color={props.labelProps?.color ?? '$olive'}
5858
{...props.labelProps}
5959
>
60-
{label} {isOptional && '(Optional)'}
60+
{label}
6161
</Label>
6262
)}
6363
<Shake shakeKey={error?.errorMessage}>

packages/app/features/account/settings/personal-info.tsx

+130-71
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { SubmitButton, YStack, isWeb, useToastController, XStack, Button, H1 } from '@my/ui'
1+
import {
2+
SubmitButton,
3+
YStack,
4+
isWeb,
5+
useToastController,
6+
XStack,
7+
Button,
8+
Text,
9+
Paragraph,
10+
} from '@my/ui'
211
import { SchemaForm } from 'app/utils/SchemaForm'
312
import { useSupabase } from 'app/utils/supabase/useSupabase'
413
import { useUser } from 'app/utils/useUser'
@@ -7,89 +16,139 @@ import type { z } from 'zod'
716
import { FormProvider, useForm } from 'react-hook-form'
817
import { VerifyCode } from 'app/features/auth/components/VerifyCode'
918
import { AuthUserSchema, useAuthUserMutation } from 'app/utils/useAuthUserMutation'
19+
import { useEffect, useState } from 'react'
20+
import { useProfileMutation } from 'app/utils/useUserPersonalDataMutation'
21+
22+
enum FormState {
23+
PersonalInfoForm = 'PersonalInfoForm',
24+
VerificationCode = 'VerificationCode',
25+
}
1026

1127
export const PersonalInfoScreen = () => {
12-
const { user } = useUser()
28+
const { user, profile } = useUser()
29+
1330
const supabase = useSupabase()
1431
const toast = useToastController()
1532
const router = useRouter()
1633
const form = useForm<z.infer<typeof AuthUserSchema>>() // Using react-hook-form
17-
const mutation = useAuthUserMutation()
34+
const { mutateAsync: mutateAuthAsync } = useAuthUserMutation()
35+
const { mutateAsync: mutateProfileAsync } = useProfileMutation()
36+
const [formState, setFormState] = useState<FormState>(FormState.PersonalInfoForm)
37+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
38+
39+
function handleSuccessAuthUpdate() {
40+
setFormState(FormState.VerificationCode)
41+
}
42+
43+
async function handleSubmit() {
44+
setErrorMessage(null)
45+
const values = form.getValues()
46+
47+
try {
48+
if (profile && profile.x_username !== values.xUsername) {
49+
await mutateProfileAsync(values)
50+
}
51+
52+
if (user && user.phone !== values.phone) {
53+
await mutateAuthAsync(values)
54+
handleSuccessAuthUpdate()
55+
}
56+
} catch (error) {
57+
console.error(error)
1858

19-
if (mutation.isError) {
20-
form.setError('phone', { type: 'custom', message: mutation.error.message })
59+
if (error?.message) {
60+
setErrorMessage(error.message)
61+
}
62+
}
2163
}
2264

65+
useEffect(() => {
66+
form.reset({ phone: user?.phone ?? '', xUsername: profile?.x_username ?? '' })
67+
}, [profile?.x_username, user?.phone, form.reset])
68+
69+
const verificationCode = (
70+
<VerifyCode
71+
type={'phone_change'}
72+
phone={form.getValues().phone}
73+
onSuccess={async () => {
74+
toast.show('Phone number updated')
75+
router.back()
76+
if (!isWeb) {
77+
await supabase.auth.refreshSession()
78+
}
79+
}}
80+
/>
81+
)
82+
83+
const personalInfoForm = (
84+
<SchemaForm
85+
form={form}
86+
schema={AuthUserSchema}
87+
onSubmit={handleSubmit}
88+
props={{
89+
phone: {
90+
'aria-label': 'Phone number',
91+
autoComplete: 'tel',
92+
keyboardType: 'phone-pad',
93+
autoCapitalize: 'none',
94+
bc: '$color0',
95+
labelProps: {
96+
color: '$color10',
97+
},
98+
},
99+
xUsername: {
100+
'aria-label': 'X username',
101+
labelProps: {
102+
color: '$color10',
103+
},
104+
bc: '$color0',
105+
pl: '$8',
106+
iconBefore: (
107+
<Text color="$color10" userSelect={'none'} lineHeight={8}>
108+
@
109+
</Text>
110+
),
111+
},
112+
}}
113+
renderAfter={({ submit }) => (
114+
<YStack ai={'flex-start'}>
115+
<SubmitButton
116+
f={1}
117+
marginTop={'$5'}
118+
fontWeight={'500'}
119+
onPress={() => submit()}
120+
theme="green"
121+
borderRadius={'$3'}
122+
px={'$size.1.5'}
123+
>
124+
<Button.Text ff={'$mono'} fontWeight={'600'} tt="uppercase" size={'$5'}>
125+
SAVE
126+
</Button.Text>
127+
</SubmitButton>
128+
{errorMessage && (
129+
<Paragraph marginTop={'$5'} theme="red" color="$color9">
130+
{errorMessage}
131+
</Paragraph>
132+
)}
133+
</YStack>
134+
)}
135+
>
136+
{(fields) => <>{Object.values(fields)}</>}
137+
</SchemaForm>
138+
)
139+
23140
return (
24141
<YStack w={'100%'} als={'center'}>
25-
<XStack w={'100%'}>
26-
<H1 size={'$9'} fontWeight={'600'} color="$color12">
27-
Personal Information
28-
</H1>
29-
</XStack>
30142
<XStack w={'100%'} $gtLg={{ paddingTop: '$6' }} $lg={{ jc: 'center' }}>
31143
<FormProvider {...form}>
32-
{form.formState.isSubmitSuccessful ? (
33-
<VerifyCode
34-
type={'phone_change'}
35-
phone={form.getValues().phone}
36-
onSuccess={async () => {
37-
toast.show('Phone number updated')
38-
router.back()
39-
if (!isWeb) {
40-
await supabase.auth.refreshSession()
41-
}
42-
}}
43-
/>
44-
) : (
45-
<SchemaForm
46-
form={form}
47-
schema={AuthUserSchema}
48-
onSubmit={(values) => mutation.mutate(values)}
49-
props={{
50-
phone: {
51-
'aria-label': 'Phone number',
52-
autoComplete: 'tel',
53-
keyboardType: 'phone-pad',
54-
autoCapitalize: 'none',
55-
bc: '$color0',
56-
labelProps: {
57-
color: '$color10',
58-
},
59-
},
60-
// email: {
61-
// 'aria-label': 'Email',
62-
// },
63-
// address: {
64-
// 'aria-label': 'Address',
65-
// },
66-
}}
67-
defaultValues={{
68-
phone: user?.phone ?? '',
69-
// email: user?.email ?? '',
70-
// address: '',
71-
}}
72-
renderAfter={({ submit }) => (
73-
<YStack ai={'flex-start'}>
74-
<SubmitButton
75-
f={1}
76-
marginTop={'$5'}
77-
fontWeight={'500'}
78-
onPress={() => submit()}
79-
theme="green"
80-
borderRadius={'$3'}
81-
px={'$size.1.5'}
82-
>
83-
<Button.Text ff={'$mono'} fontWeight={'600'} tt="uppercase" size={'$5'}>
84-
SAVE
85-
</Button.Text>
86-
</SubmitButton>
87-
</YStack>
88-
)}
89-
>
90-
{(fields) => <>{Object.values(fields)}</>}
91-
</SchemaForm>
92-
)}
144+
{(() => {
145+
switch (formState) {
146+
case FormState.PersonalInfoForm:
147+
return personalInfoForm
148+
case FormState.VerificationCode:
149+
return verificationCode
150+
}
151+
})()}
93152
</FormProvider>
94153
</XStack>
95154
</YStack>

packages/app/utils/useAuthUserMutation.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useToastController } from '@my/ui'
66

77
export const AuthUserSchema = z.object({
88
phone: formFields.text.describe('Phone'),
9+
xUsername: formFields.text.optional().describe('X'),
910
// email: formFields.text.describe('Email'),
1011
// address: formFields.text.describe('Address'),
1112
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useSupabase } from 'app/utils/supabase/useSupabase'
2+
import { useUser } from 'app/utils/useUser'
3+
import { useMutation, useQueryClient } from '@tanstack/react-query'
4+
import { useToastController } from '@my/ui'
5+
import type { z } from 'zod'
6+
import type { AuthUserSchema } from 'app/utils/useAuthUserMutation'
7+
8+
export const useProfileMutation = () => {
9+
const supabase = useSupabase()
10+
const { user } = useUser()
11+
const queryClient = useQueryClient()
12+
const toast = useToastController()
13+
14+
return useMutation({
15+
async mutationFn(data: z.infer<typeof AuthUserSchema>) {
16+
if (!user) {
17+
return
18+
}
19+
20+
const xUsernameToUpdate = data.xUsername || null
21+
22+
const { error: profileUpdateError } = await supabase
23+
.from('profiles')
24+
.update({ x_username: xUsernameToUpdate })
25+
.eq('id', user?.id)
26+
27+
if (profileUpdateError) {
28+
throw new Error(profileUpdateError.message)
29+
}
30+
},
31+
async onSuccess() {
32+
await queryClient.invalidateQueries({ queryKey: ['profile'] })
33+
toast.show('Personal data updated')
34+
},
35+
})
36+
}

supabase/database-generated.types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ export type Database = {
345345
name: string | null
346346
referral_code: string | null
347347
send_id: number
348+
x_username: string | null
348349
}
349350
Insert: {
350351
about?: string | null
@@ -354,6 +355,7 @@ export type Database = {
354355
name?: string | null
355356
referral_code?: string | null
356357
send_id?: number
358+
x_username?: string | null
357359
}
358360
Update: {
359361
about?: string | null
@@ -363,6 +365,7 @@ export type Database = {
363365
name?: string | null
364366
referral_code?: string | null
365367
send_id?: number
368+
x_username?: string | null
366369
}
367370
Relationships: [
368371
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
alter table "public"."profiles" add column "x_username" text;
2+
3+
alter table "public"."profiles" add constraint "profiles_x_username_update" CHECK ((length(x_username) <= 64)) not valid;
4+
5+
alter table "public"."profiles" validate constraint "profiles_x_username_update";
6+
7+

0 commit comments

Comments
 (0)