+ {t.rich("loginLegalDisclaimer", {
+ termsOfService: (chunks) => (
+
+ {chunks}
+
+ ),
+ privacyPolicy: (chunks) => (
+
+ {chunks}
+
+ )
+ })}
+
+ )}
+
{isInvite && (
@@ -99,15 +137,36 @@ export default async function Page(props: {
)}
-
+ {useSmartLogin ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
{(!signUpDisabled || isInvite) && (
@@ -124,6 +183,31 @@ export default async function Page(props: {
)}
+
+ {!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? (
+
+ ) : null}
>
);
}
+
+function buildQueryString(searchParams: {
+ [key: string]: string | string[] | undefined;
+}): string {
+ const params = new URLSearchParams();
+ const redirect = searchParams.redirect;
+ const forceLogin = searchParams.forceLogin;
+
+ if (redirect && typeof redirect === "string") {
+ params.set("redirect", redirect);
+ }
+ if (forceLogin && typeof forceLogin === "string") {
+ params.set("forceLogin", forceLogin);
+ }
+ const queryString = params.toString();
+ return queryString ? `?${queryString}` : "";
+}
diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx
index b4f4fddd..9ab7b7e6 100644
--- a/src/app/auth/signup/page.tsx
+++ b/src/app/auth/signup/page.tsx
@@ -14,6 +14,7 @@ export default async function Page(props: {
searchParams: Promise<{
redirect: string | undefined;
email: string | undefined;
+ fromSmartLogin: string | undefined;
}>;
}) {
const searchParams = await props.searchParams;
@@ -73,6 +74,7 @@ export default async function Page(props: {
inviteToken={inviteToken}
inviteId={inviteId}
emailParam={searchParams.email}
+ fromSmartLogin={searchParams.fromSmartLogin === "true"}
/>
diff --git a/src/components/DashboardLoginForm.tsx b/src/components/DashboardLoginForm.tsx
index 5082f00d..f57eb8b1 100644
--- a/src/components/DashboardLoginForm.tsx
+++ b/src/components/DashboardLoginForm.tsx
@@ -69,22 +69,6 @@ export default function DashboardLoginForm({
- {showOrgLogin && (
-
-
-
-
-
- )}
);
}
-
-function buildQueryString(searchParams: {
- [key: string]: string | string[] | undefined;
-}): string {
- const params = new URLSearchParams();
- const redirect = searchParams.redirect;
- const forceLogin = searchParams.forceLogin;
-
- if (redirect && typeof redirect === "string") {
- params.set("redirect", redirect);
- }
- if (forceLogin && typeof forceLogin === "string") {
- params.set("forceLogin", forceLogin);
- }
- const queryString = params.toString();
- return queryString ? `?${queryString}` : "";
-}
diff --git a/src/components/DeviceLoginForm.tsx b/src/components/DeviceLoginForm.tsx
index 777c1c03..c6972d56 100644
--- a/src/components/DeviceLoginForm.tsx
+++ b/src/components/DeviceLoginForm.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect, useCallback } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
@@ -13,7 +13,13 @@ import {
FormLabel,
FormMessage
} from "@/components/ui/form";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardDescription
+} from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -25,12 +31,12 @@ import {
InputOTPSlot
} from "@/components/ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
-import { AlertTriangle } from "lucide-react";
+import { AlertTriangle, Loader2 } from "lucide-react";
import { DeviceAuthConfirmation } from "@/components/DeviceAuthConfirmation";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import BrandingLogo from "./BrandingLogo";
import { useTranslations } from "next-intl";
-import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import UserProfileCard from "@/components/UserProfileCard";
const createFormSchema = (t: (key: string) => string) =>
z.object({
@@ -61,6 +67,8 @@ export default function DeviceLoginForm({
const api = createApiClient({ env });
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
+ const [validatingInitialCode, setValidatingInitialCode] = useState(false);
+ const [verifyingInitialCode, setVerifyingInitialCode] = useState(false);
const [metadata, setMetadata] = useState(null);
const [code, setCode] = useState("");
const { isUnlocked } = useLicenseStatusContext();
@@ -75,39 +83,88 @@ export default function DeviceLoginForm({
}
});
- async function onSubmit(data: z.infer) {
- setError(null);
- setLoading(true);
+ const validateCode = useCallback(
+ async (codeToValidate: string, skipConfirmation = false) => {
+ setError(null);
+ setLoading(true);
- try {
- // split code and add dash if missing
- if (!data.code.includes("-") && data.code.length === 8) {
- data.code = data.code.slice(0, 4) + "-" + data.code.slice(4);
- }
-
- // First check - get metadata
- const res = await api.post(
- "/device-web-auth/verify?forceLogin=true",
- {
- code: data.code.toUpperCase(),
- verify: false
+ try {
+ // split code and add dash if missing
+ let formattedCode = codeToValidate;
+ if (
+ !formattedCode.includes("-") &&
+ formattedCode.length === 8
+ ) {
+ formattedCode =
+ formattedCode.slice(0, 4) +
+ "-" +
+ formattedCode.slice(4);
}
- );
- if (res.data.success && res.data.data.metadata) {
- setMetadata(res.data.data.metadata);
- setCode(data.code.toUpperCase());
- } else {
- setError(t("deviceCodeInvalidOrExpired"));
+ // First check - get metadata
+ const res = await api.post(
+ "/device-web-auth/verify?forceLogin=true",
+ {
+ code: formattedCode.toUpperCase(),
+ verify: false
+ }
+ );
+
+ if (res.data.success && res.data.data.metadata) {
+ setCode(formattedCode.toUpperCase());
+
+ // If skipping confirmation (initial code), go straight to verify
+ if (skipConfirmation) {
+ setVerifyingInitialCode(true);
+ try {
+ await api.post("/device-web-auth/verify", {
+ code: formattedCode.toUpperCase(),
+ verify: true
+ });
+ router.push("/auth/login/device/success");
+ } catch (e: any) {
+ const errorMessage = formatAxiosError(e);
+ setError(
+ errorMessage || t("deviceCodeVerifyFailed")
+ );
+ setVerifyingInitialCode(false);
+ return false;
+ }
+ return true;
+ } else {
+ setMetadata(res.data.data.metadata);
+ return true;
+ }
+ } else {
+ setError(t("deviceCodeInvalidOrExpired"));
+ return false;
+ }
+ } catch (e: any) {
+ const errorMessage = formatAxiosError(e);
+ setError(errorMessage || t("deviceCodeInvalidOrExpired"));
+ return false;
+ } finally {
+ setLoading(false);
}
- } catch (e: any) {
- const errorMessage = formatAxiosError(e);
- setError(errorMessage || t("deviceCodeInvalidOrExpired"));
- } finally {
- setLoading(false);
- }
+ },
+ [api, t, router]
+ );
+
+ async function onSubmit(data: z.infer) {
+ await validateCode(data.code);
}
+ // Auto-validate initial code if provided
+ useEffect(() => {
+ const cleanedInitialCode = initialCode.replace(/-/g, "").toUpperCase();
+ if (cleanedInitialCode && cleanedInitialCode.length === 8) {
+ setValidatingInitialCode(true);
+ validateCode(cleanedInitialCode, true).finally(() => {
+ setValidatingInitialCode(false);
+ });
+ }
+ }, [initialCode, validateCode]);
+
async function onConfirm() {
if (!code || !metadata) return;
@@ -149,9 +206,6 @@ export default function DeviceLoginForm({
}
const profileLabel = (userName || userEmail || "").trim();
- const profileInitial = profileLabel
- ? profileLabel.charAt(0).toUpperCase()
- : "?";
async function handleUseDifferentAccount() {
try {
@@ -172,6 +226,39 @@ export default function DeviceLoginForm({
}
}
+ // Show loading state while validating/verifying initial code
+ if (validatingInitialCode || verifyingInitialCode) {
+ return (
+
+
+
+ {t("deviceActivation")}
+
+ {validatingInitialCode
+ ? t("deviceCodeValidating")
+ : t("deviceCodeVerifying")}
+
+
+
+
+
+
+ {validatingInitialCode
+ ? t("deviceCodeValidating")
+ : t("deviceCodeVerifying")}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ );
+ }
+
if (metadata) {
return (
-