Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added last_used in login page #718

Merged
merged 7 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 86 additions & 59 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ import Passkey from "@/components/shared/icons/passkey";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { LastUsed, useLastUsed } from "@/components/hooks/useLastUsed";
import { Loader } from "lucide-react";

export default function Login() {
const { next } = useParams as { next?: string };

const [isLoginWithEmail, setIsLoginWithEmail] = useState<boolean>(false);
const [isLoginWithGoogle, setIsLoginWithGoogle] = useState<boolean>(false);
const [isLoginWithLinkedIn, setIsLoginWithLinkedIn] =
useState<boolean>(false);
const [lastUsed, setLastUsed] = useLastUsed();
const authMethods = ["google", "email", "linkedin", "passkey"] as const;
type AuthMethod = (typeof authMethods)[number];
const [clickedMethod, setClickedMethod] = useState<AuthMethod | undefined>(
Expand All @@ -28,6 +36,7 @@ export default function Login() {
"Continue with Email",
);


return (
<div className="flex h-screen w-full flex-wrap">
{/* Left part */}
Expand Down Expand Up @@ -59,6 +68,7 @@ export default function Login() {
}).then((res) => {
if (res?.ok && !res?.error) {
setEmail("");
setLastUsed("credentials")
setEmailButtonText("Email sent - check your inbox!");
toast.success("Email sent - check your inbox!");
} else {
Expand Down Expand Up @@ -97,69 +107,86 @@ export default function Login() {
)}
Continue with Email
</Button> */}
<Button
type="submit"
loading={clickedMethod === "email"}
className={`${
clickedMethod === "email"
<div className="relative">
<Button
type="submit"
loading={clickedMethod === "email"}
className={`${clickedMethod === "email"
? "bg-black"
: "bg-gray-800 hover:bg-gray-900"
} focus:shadow-outline transform rounded px-4 py-2 text-white transition-colors duration-300 ease-in-out focus:outline-none`}
>
{emailButtonText}
</Button>
} w-full focus:shadow-outline transform rounded px-4 py-2 text-white transition-colors duration-300 ease-in-out focus:outline-none`}
>
{emailButtonText}
{lastUsed === "credentials" && <LastUsed />}
</Button>
</div>
</form>
<p className="py-4 text-center">or</p>
<div className="flex flex-col space-y-2 px-4 sm:px-16">
<Button
onClick={() => {
setClickedMethod("google");
signIn("google", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
if (res?.status) {
setClickedMethod(undefined);
}
});
}}
loading={clickedMethod === "google"}
disabled={clickedMethod && clickedMethod !== "google"}
className="flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
>
<Google className="h-5 w-5" />
<span>Continue with Google</span>
</Button>
<Button
onClick={() => {
setClickedMethod("linkedin");
signIn("linkedin", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
if (res?.status) {
setClickedMethod(undefined);
}
});
}}
loading={clickedMethod === "linkedin"}
disabled={clickedMethod && clickedMethod !== "linkedin"}
className="flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
>
<LinkedIn />
<span>Continue with LinkedIn</span>
</Button>
<Button
onClick={() => {
signInWithPasskey({
tenantId: process.env.NEXT_PUBLIC_HANKO_TENANT_ID as string,
});
}}
disabled={clickedMethod && clickedMethod !== "passkey"}
variant="outline"
className="flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200 hover:text-gray-900"
>
<Passkey className="h-4 w-4" />
<span>Continue with a passkey</span>
</Button>
<div className="relative">
<Button
onClick={() => {
setLastUsed("google")
setIsLoginWithGoogle(true);
signIn("google", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
if (res?.status) {
setIsLoginWithGoogle(false);
}
});
}}
disabled={isLoginWithGoogle}
className="w-full flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200 "
>
{isLoginWithGoogle ? (
<Loader className="mr-2 h-5 w-5 animate-spin" />
) : (
<Google className="h-5 w-5" />
)}
<span>Continue with Google</span>
{lastUsed === "google" && <LastUsed />}
</Button>
</div>
<div className="relative">
<Button
onClick={() => {
setClickedMethod("linkedin");
signIn("linkedin", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
if (res?.status) {
setClickedMethod(undefined);
}
});
}}
loading={clickedMethod === "linkedin"}
disabled={clickedMethod && clickedMethod !== "linkedin"}
className="w-full flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
>
<LinkedIn />
<span>Continue with LinkedIn</span>
{lastUsed === "linkedin" && <LastUsed />}
</Button>
</div>
<div className="relative">
<Button
onClick={() => {
setLastUsed("saml")
signInWithPasskey({
tenantId: process.env.NEXT_PUBLIC_HANKO_TENANT_ID as string,
})
}
}
variant="outline"
disabled={clickedMethod && clickedMethod !== "passkey"}
className="w-full flex items-center justify-center space-x-2 border border-gray-200 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200 hover:text-gray-900"
>
<Passkey className="h-4 w-4" />
<span>Continue with a passkey</span>
{lastUsed === "saml" && <LastUsed />}
</Button>
</div>
</div>
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-16">
By clicking continue, you acknowledge that you have read and agree
Expand Down Expand Up @@ -212,6 +239,6 @@ export default function Login() {
</div>
</div>
</div>
</div>
</div >
);
}
42 changes: 42 additions & 0 deletions components/hooks/useLastUsed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { classNames } from "@/lib/utils";
import { localStorage } from "@/lib/webstorage";
import { useState, useEffect } from "react";




type LoginType = "saml" | "google" | "credentials" | "linkedin";

export function useLastUsed() {
const [lastUsed, setLastUsed] = useState<LoginType>();

useEffect(() => {
const storedValue = localStorage.getItem("last_papermark_login");
if (storedValue) {
setLastUsed(storedValue as LoginType);
}
}, []);

useEffect(() => {
if (lastUsed) {
localStorage.setItem("last_papermark_login", lastUsed);
} else {
localStorage.removeItem("last_papermark_login");
}
}, [lastUsed]);

return [lastUsed, setLastUsed] as const;
}

export const LastUsed = ({ className }: { className?: string | undefined }) => {
return (
<>
<div className="absolute left-11 sm:left-1 top-1/2 -translate-y-1/2 -translate-x-full">
<div className={`bg-[#333333] text-white text-xs py-1 px-2 rounded-md relative ${className} z-[999]`}>
Last used
<div className={`absolute -right-1 top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-l-4 border-r-0 border-transparent border-l-[#333333] `}></div>
</div>
</div>
</>
);
};
36 changes: 36 additions & 0 deletions lib/webstorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Provides a wrapper around localStorage(and sessionStorage(TODO when needed)) to avoid errors in case of restricted storage access.
*
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
*/
export const localStorage = {
getItem(key: string) {
try {
// eslint-disable-next-line
return window.localStorage.getItem(key);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Third Party Context in Chrome Incognito mode.
return null;
}
},
setItem(key: string, value: string) {
try {
// eslint-disable-next-line
window.localStorage.setItem(key, value);
} catch (e) {
// In case storage is restricted. Possible reasons
// 1. Third Party Context in Chrome Incognito mode.
// 2. Storage limit reached
return;
}
},
removeItem: (key: string) => {
try {
// eslint-disable-next-line
window.localStorage.removeItem(key);
} catch (e) {
return;
}
},
};