From b2c21c5f018da718cba4bcd582ae005daf05b29f Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Fri, 9 Jan 2026 08:23:14 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20create=20provider=20callbac?= =?UTF-8?q?k=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/providers/[name]/callback/page.tsx | 19 ++----- bun.lock | 3 ++ features/authCallback/index.tsx | 40 ++++++++++++++ package.json | 1 + shared/libs/shadcn/ui/sonner.tsx | 49 +++++++++++++++++ .../signin/actions/submitProviderCallback.ts | 54 +++++++++++++++++++ 6 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 features/authCallback/index.tsx create mode 100644 shared/libs/shadcn/ui/sonner.tsx create mode 100644 shared/widgets/signin/actions/submitProviderCallback.ts diff --git a/app/(clean)/auth/providers/[name]/callback/page.tsx b/app/(clean)/auth/providers/[name]/callback/page.tsx index 9b9cf13..b9dc0d8 100644 --- a/app/(clean)/auth/providers/[name]/callback/page.tsx +++ b/app/(clean)/auth/providers/[name]/callback/page.tsx @@ -1,20 +1,7 @@ -import React from "react"; +import AuthCallbackIndex from "@/features/authCallback"; -const page = async ({ - params, - searchParams, -}: { - params: { name: string }; - searchParams: { [key: string]: string | string[] | undefined }; -}) => { - const resolvedParams = await params; - const resolvedSearchParams = await searchParams; - - return ( -
-

Loading....

-
- ); +const page = async () => { + return ; }; export default page; diff --git a/bun.lock b/bun.lock index 74d0f3c..f6ebf33 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "shadcn": "^3.6.3", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", }, @@ -1276,6 +1277,8 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], diff --git a/features/authCallback/index.tsx b/features/authCallback/index.tsx new file mode 100644 index 0000000..012b0b7 --- /dev/null +++ b/features/authCallback/index.tsx @@ -0,0 +1,40 @@ +"use client"; +import { Spinner } from "@/shared/libs/shadcn/ui/spinner"; +import { submitProviderCallback } from "@/shared/widgets/signin/actions/submitProviderCallback"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +const AuthCallbackIndex = () => { + const { name } = useParams(); + const queries = useSearchParams().toString(); + const router = useRouter(); + const [textDescription, setTextDescription] = useState( + "We are processing your authentication." + ); + + useEffect(() => { + (async () => { + const response = await submitProviderCallback(name as string, queries); + if (response.success) { + setTextDescription("Authentication successful! Redirecting..."); + router.push("/"); + } else { + setTextDescription("Authentication failed. Please try again."); + } + })(); + }, []); + + return ( +
+ +
+

Please wait...

+

+ {textDescription} +

+
+
+ ); +}; + +export default AuthCallbackIndex; diff --git a/package.json b/package.json index 11eab16..188d0c4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "shadcn": "^3.6.3", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0" }, diff --git a/shared/libs/shadcn/ui/sonner.tsx b/shared/libs/shadcn/ui/sonner.tsx new file mode 100644 index 0000000..9280ee5 --- /dev/null +++ b/shared/libs/shadcn/ui/sonner.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" +import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ), + info: ( + + ), + warning: ( + + ), + error: ( + + ), + loading: ( + + ), + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + toastOptions={{ + classNames: { + toast: "cn-toast", + }, + }} + {...props} + /> + ) +} + +export { Toaster } diff --git a/shared/widgets/signin/actions/submitProviderCallback.ts b/shared/widgets/signin/actions/submitProviderCallback.ts new file mode 100644 index 0000000..f75d849 --- /dev/null +++ b/shared/widgets/signin/actions/submitProviderCallback.ts @@ -0,0 +1,54 @@ +"use server"; + +import { backendFetch, BackendResponse } from "@/shared/helper/backendFetch"; +import { cookies } from "next/headers"; + +export const submitProviderCallback = async ( + providerName: string, + queries?: unknown +): Promise< + BackendResponse<{ + authToken: string; + }> +> => { + try { + const envKey = providerName.toUpperCase() + "_CALLBACK_URL"; + + const authClientCallbackUrl = (await backendFetch( + "auth/providers/" + providerName + "/callback" + )) as BackendResponse<{ + callback_url: string; + }>; + + if (!authClientCallbackUrl.success) + throw new Error("Failed to get auth client callback URL"); + + const responseProvision = (await backendFetch( + `${authClientCallbackUrl.data?.callback_url!}?callbackURI=${ + process.env.APP_URL + }${process.env[envKey]}&${queries}` + )) as BackendResponse<{ + authToken: string; + }>; + + if (!responseProvision.success) + throw new Error("Failed to submit provider callback"); + + (await cookies()).set({ + name: "auth_token", + value: responseProvision.data?.authToken!, + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", + maxAge: Number(process.env.SESSION_EXPIRE), + }); + + return responseProvision; + } catch (error) { + return { + success: false, + message: "Error submitting provider callback", + error: error, + }; + } +};