Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions packages/shadcn/registry-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
"type": "registry:block",
"title": "Email Link Auth Form",
"description": "A form allowing users to sign in via email link.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["input", "button", "form", "alert", "{{ DOMAIN }}/r/policies.json"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["input", "button", "field", "alert", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/email-link-auth-form.tsx",
Expand Down Expand Up @@ -126,8 +126,8 @@
"type": "registry:block",
"title": "Forgot Password Auth Form",
"description": "A form allowing users to reset their password via email.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/forgot-password-auth-form.tsx",
Expand Down Expand Up @@ -379,9 +379,9 @@
"type": "registry:block",
"title": "Phone Auth Form",
"description": "A form allowing users to authenticate using their phone number with SMS verification.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": [
"form",
"field",
"input",
"button",
"{{ DOMAIN }}/r/country-selector.json",
Expand Down Expand Up @@ -457,8 +457,8 @@
"type": "registry:block",
"title": "Sign In Auth Form",
"description": "A form allowing users to sign in with email and password.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/sign-in-auth-form.tsx",
Expand Down Expand Up @@ -496,8 +496,8 @@
"type": "registry:block",
"title": "Sign Up Auth Form",
"description": "A form allowing users to sign up with email and password.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies.json"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["input", "button", "field", "{{ DOMAIN }}/r/policies.json"],
"files": [
{
"path": "src/components/sign-up-auth-form.tsx",
Expand Down Expand Up @@ -535,8 +535,8 @@
"type": "registry:block",
"title": "SMS Multi-Factor Assertion Form",
"description": "A form allowing users to complete SMS-based multi-factor authentication during sign-in.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["form", "input", "button", "input-otp"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["field", "input", "button", "input-otp"],
"files": [
{
"path": "src/components/sms-multi-factor-assertion-form.tsx",
Expand All @@ -552,8 +552,8 @@
"type": "registry:block",
"title": "SMS Multi-Factor Enrollment Form",
"description": "A form allowing users to enroll SMS-based multi-factor authentication.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["form", "input", "button", "input-otp", "{{ DOMAIN }}/r/country-selector.json"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["field", "input", "button", "input-otp", "{{ DOMAIN }}/r/country-selector.json"],
"files": [
{
"path": "src/components/sms-multi-factor-enrollment-form.tsx",
Expand All @@ -569,8 +569,8 @@
"type": "registry:block",
"title": "TOTP Multi-Factor Assertion Form",
"description": "A form allowing users to complete TOTP-based multi-factor authentication during sign-in.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["form", "button", "input-otp"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["field", "button", "input-otp"],
"files": [
{
"path": "src/components/totp-multi-factor-assertion-form.tsx",
Expand All @@ -586,8 +586,8 @@
"type": "registry:block",
"title": "TOTP Multi-Factor Enrollment Form",
"description": "A form allowing users to enroll TOTP-based multi-factor authentication with QR code generation.",
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}"],
"registryDependencies": ["form", "input", "button", "input-otp"],
"dependencies": ["{{ DEP | @firebase-oss/ui-react@beta }}", "react-hook-form", "@hookform/resolvers"],
"registryDependencies": ["field", "input", "button", "input-otp"],
"files": [
{
"path": "src/components/totp-multi-factor-enrollment-form.tsx",
Expand Down
15 changes: 15 additions & 0 deletions packages/shadcn/src/components/email-link-auth-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ describe("<EmailLinkAuthForm />", () => {
expect(container.querySelector("button[type='submit']")).toBeInTheDocument();
});

it("should associate the email label with input via htmlFor/id", () => {
const mockUI = createMockUI();

const { container } = render(
<FirebaseUIProvider ui={mockUI}>
<EmailLinkAuthForm />
</FirebaseUIProvider>
);

expect(container.querySelector('[data-slot="field-label"][for="email"]')).toBeInTheDocument();
expect(container.querySelector("input#email")).toBeInTheDocument();
expect(container.querySelector("input#email")?.getAttribute("aria-invalid")).toBe("false");
expect(container.querySelectorAll('[data-slot="field-error"]').length).toBe(0);
});

it("should call the onEmailSent callback when the form is submitted successfully", async () => {
const mockAction = vi.fn().mockResolvedValue(undefined);
vi.mocked(useEmailLinkAuthFormAction).mockReturnValue(mockAction);
Expand Down
26 changes: 12 additions & 14 deletions packages/shadcn/src/components/email-link-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import {
type EmailLinkAuthFormProps,
} from "@firebase-oss/ui-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, FormProvider, Controller } from "react-hook-form";

import { Policies } from "@/components/policies";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";

export type { EmailLinkAuthFormProps };
Expand Down Expand Up @@ -74,27 +74,25 @@ export function EmailLinkAuthForm(props: EmailLinkAuthFormProps) {
}

return (
<Form {...form}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
<FormField
<Controller
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{getTranslation(ui, "labels", "emailAddress")}</FormLabel>
<FormControl>
<Input {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
render={({ field, fieldState }) => (
<Field data-invalid={!!fieldState.error}>
<FieldLabel htmlFor="email">{getTranslation(ui, "labels", "emailAddress")}</FieldLabel>
<Input {...field} id="email" type="email" aria-invalid={!!fieldState.error} />
{fieldState.error && <FieldError>{fieldState.error.message}</FieldError>}
</Field>
)}
/>
Comment thread
just1and0 marked this conversation as resolved.
<Policies />
<Button type="submit" disabled={ui.state !== "idle"}>
{getTranslation(ui, "labels", "sendSignInLink")}
</Button>
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
{form.formState.errors.root && <FieldError>{form.formState.errors.root.message}</FieldError>}
</form>
</Form>
</FormProvider>
);
}
15 changes: 15 additions & 0 deletions packages/shadcn/src/components/forgot-password-auth-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ describe("<ForgotPasswordAuthForm />", () => {
expect(container.querySelector("button[type='submit']")).toBeInTheDocument();
});

it("should associate the email label with input via htmlFor/id", () => {
const mockUI = createMockUI();

const { container } = render(
<FirebaseUIProvider ui={mockUI}>
<ForgotPasswordAuthForm />
</FirebaseUIProvider>
);

expect(container.querySelector('[data-slot="field-label"][for="email"]')).toBeInTheDocument();
expect(container.querySelector("input#email")).toBeInTheDocument();
expect(container.querySelector("input#email")?.getAttribute("aria-invalid")).toBe("false");
expect(container.querySelectorAll('[data-slot="field-error"]').length).toBe(0);
});

it("should render with back to sign in callback", () => {
const onBackToSignInClickMock = vi.fn();
const mockUI = createMockUI({
Expand Down
26 changes: 12 additions & 14 deletions packages/shadcn/src/components/forgot-password-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import {
useUI,
type ForgotPasswordAuthFormProps,
} from "@firebase-oss/ui-react";
import { useForm } from "react-hook-form";
import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { FirebaseUIError, getTranslation } from "@firebase-oss/ui-core";
import { useState } from "react";

import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "./policies";
Expand Down Expand Up @@ -69,32 +69,30 @@ export function ForgotPasswordAuthForm(props: ForgotPasswordAuthFormProps) {
}

return (
<Form {...form}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
<FormField
<Controller
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{getTranslation(ui, "labels", "emailAddress")}</FormLabel>
<FormControl>
<Input {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
render={({ field, fieldState }) => (
<Field data-invalid={!!fieldState.error}>
<FieldLabel htmlFor="email">{getTranslation(ui, "labels", "emailAddress")}</FieldLabel>
<Input {...field} id="email" type="email" aria-invalid={!!fieldState.error} />
{fieldState.error && <FieldError>{fieldState.error.message}</FieldError>}
</Field>
)}
/>
Comment thread
just1and0 marked this conversation as resolved.
<Policies />
<Button type="submit" disabled={ui.state !== "idle"}>
{getTranslation(ui, "labels", "resetPassword")}
</Button>
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
{form.formState.errors.root && <FieldError>{form.formState.errors.root.message}</FieldError>}
{props.onBackToSignInClick ? (
<Button type="button" variant="link" size="sm" onClick={props.onBackToSignInClick}>
<span className="text-xs">&larr; {getTranslation(ui, "labels", "backToSignIn")}</span>
</Button>
) : null}
</form>
</Form>
</FormProvider>
);
}
2 changes: 1 addition & 1 deletion packages/shadcn/src/components/phone-auth-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe("<PhoneAuthForm />", () => {
expect(container.querySelector("input[name='verificationCode']")).toBeInTheDocument();
});

const description = container.querySelector('[data-slot="form-description"]');
const description = container.querySelector('[data-slot="field-description"]');
expect(description).toBeInTheDocument();
expect(description).toHaveTextContent("Enter the verification code sent to your phone number");
});
Expand Down
74 changes: 35 additions & 39 deletions packages/shadcn/src/components/phone-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { useState } from "react";
import type { UserCredential } from "firebase/auth";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useForm, FormProvider, Controller } from "react-hook-form";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
FirebaseUIError,
Expand All @@ -38,7 +38,7 @@ import {
type PhoneAuthVerifyFormSchema,
} from "@firebase-oss/ui-core";

import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Policies } from "@/components/policies";
Expand Down Expand Up @@ -75,37 +75,35 @@ function VerifyPhoneNumberForm(props: VerifyPhoneNumberFormProps) {
}

return (
<Form {...form}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
<FormField
<Controller
control={form.control}
name="verificationCode"
render={({ field }) => (
<FormItem>
<FormLabel>{getTranslation(ui, "labels", "verificationCode")}</FormLabel>
<FormDescription>{getTranslation(ui, "prompts", "smsVerificationPrompt")}</FormDescription>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormMessage />
</FormItem>
render={({ field, fieldState }) => (
<Field data-invalid={!!fieldState.error}>
<FieldLabel htmlFor="verificationCode">{getTranslation(ui, "labels", "verificationCode")}</FieldLabel>
<FieldDescription>{getTranslation(ui, "prompts", "smsVerificationPrompt")}</FieldDescription>
<InputOTP id="verificationCode" maxLength={6} {...field} aria-invalid={!!fieldState.error}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
{fieldState.error && <FieldError>{fieldState.error.message}</FieldError>}
</Field>
)}
/>
Comment thread
just1and0 marked this conversation as resolved.
<Button type="submit" disabled={ui.state !== "idle"}>
{getTranslation(ui, "labels", "verifyCode")}
</Button>
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
{form.formState.errors.root && <FieldError>{form.formState.errors.root.message}</FieldError>}
</form>
</Form>
</FormProvider>
);
}

Expand Down Expand Up @@ -141,32 +139,30 @@ function PhoneNumberForm(props: PhoneNumberFormProps) {
}

return (
<Form {...form}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
<FormField
<Controller
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>{getTranslation(ui, "labels", "phoneNumber")}</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<CountrySelector ref={countrySelector} />
<Input {...field} type="tel" />
</div>
</FormControl>
<FormMessage />
</FormItem>
render={({ field, fieldState }) => (
<Field data-invalid={!!fieldState.error}>
<FieldLabel htmlFor="phoneNumber">{getTranslation(ui, "labels", "phoneNumber")}</FieldLabel>
<div className="flex items-center gap-2">
<CountrySelector ref={countrySelector} />
<Input {...field} id="phoneNumber" type="tel" aria-invalid={!!fieldState.error} />
</div>
{fieldState.error && <FieldError>{fieldState.error.message}</FieldError>}
</Field>
)}
/>
Comment thread
just1and0 marked this conversation as resolved.
<div ref={recaptchaContainerRef} />
<Policies />
<Button type="submit" disabled={ui.state !== "idle"}>
{getTranslation(ui, "labels", "sendCode")}
</Button>
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
{form.formState.errors.root && <FieldError>{form.formState.errors.root.message}</FieldError>}
</form>
</Form>
</FormProvider>
);
}

Expand Down
Loading
Loading