mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-12 16:57:55 +00:00
Compare commits
3 Commits
fix/oidc-c
...
scottmcken
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40bb8b4472 | ||
|
|
e7b44ee54e | ||
|
|
ea21cbdcd4 |
@@ -19,7 +19,6 @@
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -623,8 +622,6 @@
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||
|
||||
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Form, FormControl, FormField, FormItem } from "../ui/form";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "../ui/input-otp";
|
||||
import { Input } from "../ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { totpSchema, TotpSchema } from "@/schemas/totp-schema";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRef } from "react";
|
||||
import z from "zod";
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +15,7 @@ interface Props {
|
||||
export const TotpForm = (props: Props) => {
|
||||
const { formId, onSubmit } = props;
|
||||
const { t } = useTranslation();
|
||||
const autoSubmittedRef = useRef(false);
|
||||
|
||||
z.config({
|
||||
customError: (iss) =>
|
||||
@@ -29,14 +26,19 @@ export const TotpForm = (props: Props) => {
|
||||
resolver: zodResolver(totpSchema),
|
||||
});
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
form.setValue("code", value, { shouldDirty: true, shouldValidate: true });
|
||||
|
||||
if (value.length === 6) {
|
||||
onSubmit({ code: value });
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||||
form.setValue("code", value, { shouldDirty: true, shouldValidate: false });
|
||||
if (value.length === 6 && !autoSubmittedRef.current) {
|
||||
autoSubmittedRef.current = true;
|
||||
form.handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
autoSubmittedRef.current = false;
|
||||
};
|
||||
|
||||
// Note: This is not the best UX, ideally we would want https://github.com/guilhermerodz/input-otp
|
||||
// but some password managers cannot autofill the inputs (see #92) so, simple input it is
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
@@ -46,25 +48,17 @@ export const TotpForm = (props: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
autoFocus
|
||||
maxLength={6}
|
||||
placeholder="XXXXXX"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
className="text-center"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -74,7 +74,7 @@ export const TotpPage = () => {
|
||||
<CardTitle className="text-xl">{t("totpTitle")}</CardTitle>
|
||||
<CardDescription>{t("totpSubtitle")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<CardContent>
|
||||
<TotpForm
|
||||
formId={formId}
|
||||
onSubmit={(values) => totpMutation.mutate(values)}
|
||||
|
||||
Reference in New Issue
Block a user