From 84ac90a51194ec348faf7eb20554b802cd0f7c0e Mon Sep 17 00:00:00 2001
From: Adarsh Tiwari <134617221+Adarsh9977@users.noreply.github.com>
Date: Wed, 6 Aug 2025 19:18:22 +0530
Subject: [PATCH 05/16] feat: added shadcn input otp (#770)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix https://github.com/antiwork/flexile/issues/768
- [x] update e2e tests
- [x] Added new input-otp component with separator
- Tests

- Login
https://github.com/user-attachments/assets/1926947e-6d21-4a49-ae4a-d597dd8fa092
- Signup
https://github.com/user-attachments/assets/5e680318-c326-4e5b-98ac-4975addb56e5
## Summary by CodeRabbit
## Summary by CodeRabbit
* **New Features**
* Introduced a segmented OTP input component for improved one-time
password entry during login and signup.
* Added new customizable OTP input UI elements for a more user-friendly
verification experience.
* **Improvements**
* The OTP verification code entry is now visually segmented and easier
to use.
* The OTP submit button is disabled until a complete 6-digit code is
entered.
* Updated OTP input handling in tests to align with the new segmented
input component.
* **Chores**
* Added a new dependency to support the enhanced OTP input
functionality.
---
e2e/helpers/auth.ts | 13 ++--
.../administrator/onboarding/signup.spec.ts | 7 +-
e2e/tests/login.spec.ts | 19 ++++--
frontend/app/(auth)/login/page.tsx | 39 ++++++++---
.../app/(auth)/signup/[[...rest]]/page.tsx | 39 ++++++++---
frontend/components/ui/input-otp.tsx | 66 +++++++++++++++++++
package.json | 1 +
pnpm-lock.yaml | 14 ++++
8 files changed, 167 insertions(+), 31 deletions(-)
create mode 100644 frontend/components/ui/input-otp.tsx
diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts
index c84232dbee..9a4f5fc4e9 100644
--- a/e2e/helpers/auth.ts
+++ b/e2e/helpers/auth.ts
@@ -16,7 +16,10 @@ export const login = async (page: Page, user: typeof users.$inferSelect) => {
await page.getByLabel("Verification code").waitFor();
// Use test OTP code - backend should accept this in test environment
- await page.getByLabel("Verification code").fill(TEST_OTP_CODE);
+ // The InputOTP component uses a hidden input for actual input
+ // Type into the OTP input container to trigger the input
+ await page.locator('[data-slot="input-otp"]').fill(TEST_OTP_CODE);
+
await page.getByRole("button", { name: "Continue" }).click();
// Wait for successful redirect
@@ -46,9 +49,11 @@ export const signup = async (page: Page, email: string) => {
// Wait for OTP step and enter verification code
await page.getByLabel("Verification code").waitFor();
- await page.getByLabel("Verification code").fill(TEST_OTP_CODE);
- await page.getByRole("button", { name: "Continue" }).click();
- // Wait for successful redirect to onboarding or dashboard
+ // The InputOTP component uses a hidden input for actual input
+ // Type into the OTP input container to trigger the input
+ await page.locator('[data-slot="input-otp"]').fill(TEST_OTP_CODE);
+
+ await page.getByRole("button", { name: "Continue" }).click(); // Wait for successful redirect to onboarding or dashboard
await page.waitForURL(/^(?!.*\/(signup|login)$).*/u);
};
diff --git a/e2e/tests/company/administrator/onboarding/signup.spec.ts b/e2e/tests/company/administrator/onboarding/signup.spec.ts
index 9b2b6b308f..eba1d3089f 100644
--- a/e2e/tests/company/administrator/onboarding/signup.spec.ts
+++ b/e2e/tests/company/administrator/onboarding/signup.spec.ts
@@ -27,10 +27,11 @@ test.describe("Company administrator signup", () => {
await page.getByRole("button", { name: "Sign up" }).click();
// Wait for OTP step and enter verification code
- await page.getByLabel("Verification code").waitFor();
- await page.getByLabel("Verification code").fill("000000"); // Test OTP code
- await page.getByRole("button", { name: "Continue" }).click();
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+ // No need to click the button as it should auto-submit
// Wait for redirect to dashboard
await page.waitForURL(/.*\/invoices.*/u);
diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts
index 20f61feab9..af54cb0c81 100644
--- a/e2e/tests/login.spec.ts
+++ b/e2e/tests/login.spec.ts
@@ -12,8 +12,15 @@ test("login", async ({ page }) => {
await page.getByLabel("Work email").fill(email);
await page.getByRole("button", { name: "Log in", exact: true }).click();
- await page.getByLabel("Verification code").fill("000000");
- await page.getByRole("button", { name: "Continue", exact: true }).click();
+
+ // Fill the OTP code using the InputOTP component's hidden input
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+
+ // No need to click the button as it should auto-submit
+ // Wait for navigation to complete after auto-submit
+ await page.waitForURL(/.*\/invoices.*/u);
await expect(page.getByRole("heading", { name: "Invoices" })).toBeVisible();
@@ -35,9 +42,13 @@ test("login with redirect_url", async ({ page }) => {
await page.getByLabel("Work email").fill(email);
await page.getByRole("button", { name: "Log in", exact: true }).click();
- await page.getByLabel("Verification code").fill("000000");
- await page.getByRole("button", { name: "Continue", exact: true }).click();
+ // Fill the OTP code using the InputOTP component's hidden input
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+
+ // No need to click the button as it should auto-submit
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "People" })).toBeVisible();
diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx
index 20779b524f..7d0c5d4fbd 100644
--- a/frontend/app/(auth)/login/page.tsx
+++ b/frontend/app/(auth)/login/page.tsx
@@ -1,17 +1,19 @@
"use client";
import Image from "next/image";
import Link from "next/link";
-import { Suspense } from "react";
+import { Suspense, useRef } from "react";
import { AuthAlerts } from "@/components/auth/AuthAlerts";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
+import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { useAuthApi } from "@/hooks/useAuthApi";
import { useOtpFlowState } from "@/hooks/useOtpFlowState";
import logo from "@/public/logo-icon.svg";
function LoginContent() {
+ const formRef = useRef
(null);
const [state, actions] = useOtpFlowState();
const { handleSendOtp, handleAuthenticate } = useAuthApi(
{
@@ -78,23 +80,40 @@ function LoginContent() {
void handleAuthenticate(e);
}}
className="space-y-4"
+ ref={formRef}
>
-
+
- actions.setOtp(e.target.value)}
maxLength={6}
- required
+ value={state.otp}
+ onChange={(value) => {
+ actions.setOtp(value);
+ if (value.length === 6 && !state.loading) {
+ setTimeout(() => formRef.current?.requestSubmit(), 100);
+ }
+ }}
disabled={state.loading}
- />
+ autoFocus
+ required
+ >
+
+
+
+
+
+
+
+
+
+
+
+
-