Skip to content

Commit 666be94

Browse files
committed
feat(shadcn): WIP Phone auth form
1 parent 8dfd52a commit 666be94

File tree

2 files changed

+165
-1
lines changed

2 files changed

+165
-1
lines changed

packages/shadcn/src/registry/country-selector.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111

1212
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1313

14+
export type { CountrySelectorRef };
15+
1416
export const CountrySelector = forwardRef<CountrySelectorRef, CountrySelectorProps>((props, ref) => {
1517
const countries = useCountries();
1618
const defaultCountry = useDefaultCountry();
@@ -35,7 +37,7 @@ export const CountrySelector = forwardRef<CountrySelectorRef, CountrySelectorPro
3537

3638
return (
3739
<Select value={selected.code} onValueChange={setCountry}>
38-
<SelectTrigger>
40+
<SelectTrigger className="w-[120px]">
3941
<SelectValue>
4042
{selected.emoji} {selected.dialCode}
4143
</SelectValue>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"use client";
2+
3+
import {
4+
CountrySelector,
5+
PhoneAuthFormProps,
6+
usePhoneAuthNumberFormSchema,
7+
usePhoneAuthVerifyFormSchema,
8+
usePhoneNumberFormAction,
9+
useRecaptchaVerifier,
10+
useUI,
11+
useVerifyPhoneNumberFormAction,
12+
} from "@firebase-ui/react";
13+
import { useState } from "react";
14+
import type { UserCredential } from "firebase/auth";
15+
import { useRef } from "react";
16+
import { useForm } from "react-hook-form";
17+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
18+
import {
19+
FirebaseUIError,
20+
formatPhoneNumber,
21+
getTranslation,
22+
PhoneAuthNumberFormSchema,
23+
PhoneAuthVerifyFormSchema,
24+
} from "@firebase-ui/core";
25+
26+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
27+
import { Input } from "@/components/ui/input";
28+
import { Button } from "@/components/ui/button";
29+
import { Policies } from "@/registry/policies";
30+
import { CountrySelectorRef } from "@/registry/country-selector";
31+
32+
type VerifyPhoneNumberFormProps = {
33+
verificationId: string;
34+
onSuccess: (credential: UserCredential) => void;
35+
};
36+
37+
function VerifyPhoneNumberForm(props: VerifyPhoneNumberFormProps) {
38+
const ui = useUI();
39+
const schema = usePhoneAuthVerifyFormSchema();
40+
const action = useVerifyPhoneNumberFormAction();
41+
42+
const form = useForm<PhoneAuthVerifyFormSchema>({
43+
resolver: standardSchemaResolver(schema),
44+
defaultValues: {
45+
verificationId: props.verificationId,
46+
verificationCode: "",
47+
},
48+
});
49+
50+
async function onSubmit(values: PhoneAuthVerifyFormSchema) {
51+
try {
52+
const credential = await action(values);
53+
props.onSuccess(credential);
54+
} catch (error) {
55+
const message = error instanceof FirebaseUIError ? error.message : String(error);
56+
form.setError("root", { message });
57+
}
58+
}
59+
60+
return (
61+
<Form {...form}>
62+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
63+
<FormField
64+
control={form.control}
65+
name="verificationCode"
66+
render={({ field }) => (
67+
<FormItem>
68+
<FormLabel>{getTranslation(ui, "labels", "verificationCode")}</FormLabel>
69+
<FormControl>
70+
<Input {...field} />
71+
</FormControl>
72+
<FormMessage />
73+
</FormItem>
74+
)}
75+
/>
76+
<Button type="submit" disabled={ui.state !== "idle"}>
77+
{getTranslation(ui, "labels", "verifyCode")}
78+
</Button>
79+
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
80+
</form>
81+
</Form>
82+
);
83+
}
84+
85+
type PhoneNumberFormProps = {
86+
onSubmit: (verificationId: string) => void;
87+
};
88+
89+
function PhoneNumberForm(props: PhoneNumberFormProps) {
90+
const ui = useUI();
91+
const recaptchaContainerRef = useRef<HTMLDivElement>(null);
92+
const recaptchaVerifier = useRecaptchaVerifier(recaptchaContainerRef);
93+
const countrySelector = useRef<CountrySelectorRef>(null);
94+
const action = usePhoneNumberFormAction();
95+
const schema = usePhoneAuthNumberFormSchema();
96+
97+
const form = useForm<PhoneAuthNumberFormSchema>({
98+
resolver: standardSchemaResolver(schema),
99+
defaultValues: {
100+
phoneNumber: "",
101+
},
102+
});
103+
104+
async function onSubmit(values: PhoneAuthNumberFormSchema) {
105+
try {
106+
const formatted = formatPhoneNumber(values.phoneNumber, countrySelector.current!.getCountry());
107+
const verificationId = await action({ phoneNumber: formatted, recaptchaVerifier: recaptchaVerifier! });
108+
props.onSubmit(verificationId);
109+
} catch (error) {
110+
const message = error instanceof FirebaseUIError ? error.message : String(error);
111+
form.setError("root", { message });
112+
}
113+
}
114+
115+
return (
116+
<Form {...form}>
117+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
118+
<FormField
119+
control={form.control}
120+
name="phoneNumber"
121+
render={({ field }) => (
122+
<FormItem>
123+
<FormLabel>{getTranslation(ui, "labels", "phoneNumber")}</FormLabel>
124+
<FormControl>
125+
<div className="flex items-center gap-2">
126+
<CountrySelector ref={countrySelector} />
127+
<Input {...field} type="tel" />
128+
</div>
129+
</FormControl>
130+
<FormMessage />
131+
</FormItem>
132+
)}
133+
/>
134+
<div ref={recaptchaContainerRef} />
135+
<Policies />
136+
<Button type="submit" disabled={ui.state !== "idle"}>
137+
{getTranslation(ui, "labels", "sendCode")}
138+
</Button>
139+
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
140+
</form>
141+
</Form>
142+
);
143+
}
144+
145+
export type { PhoneAuthFormProps };
146+
147+
export function PhoneAuthForm(props: PhoneAuthFormProps) {
148+
const [verificationId, setVerificationId] = useState<string | null>(null);
149+
150+
if (!verificationId) {
151+
return <PhoneNumberForm onSubmit={setVerificationId} />;
152+
}
153+
154+
return (
155+
<VerifyPhoneNumberForm
156+
verificationId={verificationId}
157+
onSuccess={(credential) => {
158+
props.onSignIn?.(credential);
159+
}}
160+
/>
161+
);
162+
}

0 commit comments

Comments
 (0)