diff --git a/apps/login/package.json b/apps/login/package.json index c200079530..1017c69310 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -46,6 +46,7 @@ "copy-to-clipboard": "^3.3.3", "deepmerge": "^4.3.1", "jose": "^5.3.0", + "lucide-react": "0.469.0", "moment": "^2.29.4", "next": "15.2.0-canary.33", "next-intl": "^3.25.1", diff --git a/apps/login/src/components/idps/base-button.tsx b/apps/login/src/components/idps/base-button.tsx index 4f24dd17bc..0185c57996 100644 --- a/apps/login/src/components/idps/base-button.tsx +++ b/apps/login/src/components/idps/base-button.tsx @@ -1,7 +1,9 @@ "use client"; import { clsx } from "clsx"; +import { Loader2Icon } from "lucide-react"; import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react"; +import { useFormStatus } from "react-dom"; export type SignInWithIdentityProviderProps = DetailedHTMLProps< ButtonHTMLAttributes, @@ -15,15 +17,25 @@ export const BaseButton = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps >(function BaseButton(props, ref) { + const formStatus = useFormStatus(); + return ( ); }); diff --git a/apps/login/src/components/idps/sign-in-with-github.tsx b/apps/login/src/components/idps/sign-in-with-github.tsx index 49b4ce7b26..45108d17f7 100644 --- a/apps/login/src/components/idps/sign-in-with-github.tsx +++ b/apps/login/src/components/idps/sign-in-with-github.tsx @@ -4,6 +4,39 @@ import { useTranslations } from "next-intl"; import { forwardRef } from "react"; import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; +function GitHubLogo() { + return ( + <> + + + + + + + + ); +} + export const SignInWithGithub = forwardRef< HTMLButtonElement, SignInWithIdentityProviderProps @@ -14,32 +47,7 @@ export const SignInWithGithub = forwardRef< return (
- - - - - - +
{children ? ( children diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 5af5878759..c2fca07cc6 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -1,13 +1,12 @@ "use client"; import { idpTypeToSlug } from "@/lib/idp"; -import { startIDPFlow } from "@/lib/server/idp"; +import { redirectToIdp } from "@/lib/server/idp"; import { IdentityProvider, IdentityProviderType, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; -import { useRouter } from "next/navigation"; -import { ReactNode, useCallback, useState } from "react"; +import { ReactNode, useActionState } from "react"; import { Alert } from "./alert"; import { SignInWithIdentityProviderProps } from "./idps/base-button"; import { SignInWithApple } from "./idps/sign-in-with-apple"; @@ -31,45 +30,10 @@ export function SignInWithIdp({ organization, linkOnly, }: Readonly) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); + const [state, action, _isPending] = useActionState(redirectToIdp, {}); - const startFlow = useCallback( - async (idpId: string, provider: string) => { - setLoading(true); - const params = new URLSearchParams(); - if (linkOnly) params.set("link", "true"); - if (requestId) params.set("requestId", requestId); - if (organization) params.set("organization", organization); - - try { - const response = await startIDPFlow({ - idpId, - successUrl: `/idp/${provider}/success?` + params.toString(), - failureUrl: `/idp/${provider}/failure?` + params.toString(), - }); - - if (response && "error" in response && response?.error) { - setError(response.error); - return; - } - - if (response && "redirect" in response && response?.redirect) { - return router.push(response.redirect); - } - } catch { - setError("Could not start IDP flow"); - } finally { - setLoading(false); - } - }, - [requestId, organization, linkOnly, router], - ); - - const renderIDPButton = (idp: IdentityProvider) => { + const renderIDPButton = (idp: IdentityProvider, index: number) => { const { id, name, type } = idp; - const onClick = () => startFlow(id, idpTypeToSlug(type)); const components: Partial< Record< @@ -92,16 +56,27 @@ export function SignInWithIdp({ const Component = components[type]; return Component ? ( - +
+ + + + + + + ) : null; }; return (
{identityProviders?.map(renderIDPButton)} - {error && ( + {state?.error && (
- {error} + {state?.error}
)}
diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index aa38a63f27..e6861a60c4 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -6,11 +6,45 @@ import { startIdentityProviderFlow, } from "@/lib/zitadel"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; import { getNextUrl } from "../client"; import { getServiceUrlFromHeaders } from "../service"; import { checkEmailVerification } from "../verify-helper"; import { createSessionForIdpAndUpdateCookie } from "./cookie"; +export type RedirectToIdpState = { error?: string | null } | undefined; + +export async function redirectToIdp( + prevState: RedirectToIdpState, + formData: FormData, +): Promise { + const params = new URLSearchParams(); + + const linkOnly = formData.get("linkOnly") === "true"; + const requestId = formData.get("requestId") as string; + const organization = formData.get("organization") as string; + const idpId = formData.get("id") as string; + const provider = formData.get("provider") as string; + + if (linkOnly) params.set("link", "true"); + if (requestId) params.set("requestId", requestId); + if (organization) params.set("organization", organization); + + const response = await startIDPFlow({ + idpId, + successUrl: `/idp/${provider}/success?` + params.toString(), + failureUrl: `/idp/${provider}/failure?` + params.toString(), + }); + + if (response && "error" in response && response?.error) { + return { error: response.error }; + } + + if (response && "redirect" in response && response?.redirect) { + redirect(response.redirect); + } +} + export type StartIDPFlowCommand = { idpId: string; successUrl: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b28520f9e2..c3bcfab38f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: jose: specifier: ^5.3.0 version: 5.8.0 + lucide-react: + specifier: 0.469.0 + version: 0.469.0(react@19.0.0) moment: specifier: ^2.29.4 version: 2.30.1 @@ -1479,9 +1482,6 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3339,6 +3339,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -5836,7 +5841,7 @@ snapshots: '@react-aria/ssr@3.9.6(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 19.0.0 '@react-aria/utils@3.25.3(react@19.0.0)': @@ -5844,13 +5849,13 @@ snapshots: '@react-aria/ssr': 3.9.6(react@19.0.0) '@react-stately/utils': 3.10.4(react@19.0.0) '@react-types/shared': 3.25.0(react@19.0.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 clsx: 2.1.1 react: 19.0.0 '@react-stately/utils@3.10.4(react@19.0.0)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.15 react: 19.0.0 '@react-types/shared@3.25.0(react@19.0.0)': @@ -5925,10 +5930,6 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.13': - dependencies: - tslib: 2.8.1 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -7114,7 +7115,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 @@ -7127,7 +7128,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -7148,7 +7149,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8135,6 +8136,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.469.0(react@19.0.0): + dependencies: + react: 19.0.0 + lz-string@1.5.0: {} magic-string@0.30.12: