From 8da19c977eefbfae280e912df507427083c28b0a Mon Sep 17 00:00:00 2001 From: Alex Foster <122472971+alexf37@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:37:14 -0400 Subject: [PATCH] feat: add 'last used' badge to login page (#387) --- packages/lib/auth-context.tsx | 34 ++++ packages/ui/pages/login.tsx | 361 +++++++++++++++++++--------------- 2 files changed, 241 insertions(+), 154 deletions(-) diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 5b2d58bc..66ff84bc 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -33,6 +33,40 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [session?.session.activeOrganizationId]) + // When a session exists and there is a pending login method recorded, + // promote it to the last-used method (successful login) and clear pending. + useEffect(() => { + if (typeof window === "undefined") return + if (!session?.session) return + + try { + const pendingMethod = localStorage.getItem( + "supermemory-pending-login-method", + ) + const pendingTsRaw = localStorage.getItem( + "supermemory-pending-login-timestamp", + ) + + if (pendingMethod) { + const now = Date.now() + const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : NaN + const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL + + if (isFresh) { + localStorage.setItem( + "supermemory-last-login-method", + pendingMethod, + ) + } + } + } catch { } + // Always clear pending markers once a session is present + try { + localStorage.removeItem("supermemory-pending-login-method") + localStorage.removeItem("supermemory-pending-login-timestamp") + } catch { } + }, [session?.session]) + const setActiveOrg = async (slug: string) => { if (!slug) return diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx index c14ba4ea..fcd48eae 100644 --- a/packages/ui/pages/login.tsx +++ b/packages/ui/pages/login.tsx @@ -1,25 +1,26 @@ -"use client"; +"use client" -import { signIn } from "@lib/auth"; -import { usePostHog } from "@lib/posthog"; -import { LogoFull } from "@repo/ui/assets/Logo"; -import { TextSeparator } from "@repo/ui/components/text-separator"; -import { ExternalAuthButton } from "@ui/button/external-auth"; -import { Button } from "@ui/components/button"; +import { signIn } from "@lib/auth" +import { usePostHog } from "@lib/posthog" +import { LogoFull } from "@repo/ui/assets/Logo" +import { TextSeparator } from "@repo/ui/components/text-separator" +import { ExternalAuthButton } from "@ui/button/external-auth" +import { Button } from "@ui/components/button" +import { Badge } from "@ui/components/badge" import { Carousel, CarouselContent, CarouselItem, -} from "@ui/components/carousel"; -import { LabeledInput } from "@ui/input/labeled-input"; -import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium"; -import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium"; -import { Label1Regular } from "@ui/text/label/label-1-regular"; -import { Title1Bold } from "@ui/text/title/title-1-bold"; -import Autoplay from "embla-carousel-autoplay"; -import Image from "next/image"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; +} from "@ui/components/carousel" +import { LabeledInput } from "@ui/input/labeled-input" +import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium" +import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium" +import { Label1Regular } from "@ui/text/label/label-1-regular" +import { Title1Bold } from "@ui/text/title/title-1-bold" +import Autoplay from "embla-carousel-autoplay" +import Image from "next/image" +import { useRouter, useSearchParams } from "next/navigation" +import { useState, useEffect } from "react" export function LoginPage({ heroText = "The unified memory API for the AI era.", @@ -28,74 +29,101 @@ export function LoginPage({ "Trusted by Open Source, enterprise and developers.", ], }) { - const [email, setEmail] = useState(""); - const [submittedEmail, setSubmittedEmail] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isLoadingEmail, setIsLoadingEmail] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); + const [email, setEmail] = useState("") + const [submittedEmail, setSubmittedEmail] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isLoadingEmail, setIsLoadingEmail] = useState(false) + const [error, setError] = useState(null) + const [lastUsedMethod, setLastUsedMethod] = useState(null) + const router = useRouter() + + const posthog = usePostHog() + + const params = useSearchParams() + + // Load last used method from localStorage on mount + useEffect(() => { + const savedMethod = localStorage.getItem('supermemory-last-login-method') + setLastUsedMethod(savedMethod) + }, []) + + // Record the pending login method (will be committed after successful auth) + function setPendingLoginMethod(method: string) { + try { + localStorage.setItem('supermemory-pending-login-method', method) + localStorage.setItem('supermemory-pending-login-timestamp', String(Date.now())) + } catch { } + } + + // If we land back on this page with an error, clear any pending marker + useEffect(() => { + if (params.get("error")) { + try { + localStorage.removeItem('supermemory-pending-login-method') + localStorage.removeItem('supermemory-pending-login-timestamp') + } catch { } + } + }, [params]) - const posthog = usePostHog(); - const params = useSearchParams(); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setIsLoadingEmail(true); - setError(null); + e.preventDefault() + setIsLoading(true) + setIsLoadingEmail(true) + setError(null) // Track login attempt posthog.capture("login_attempt", { method: "magic_link", email_domain: email.split("@")[1] || "unknown", - }); + }) try { await signIn.magicLink({ callbackURL: window.location.origin, email, - }); - setSubmittedEmail(email); - + }) + setSubmittedEmail(email) + setPendingLoginMethod('magic_link') // Track successful magic link send posthog.capture("login_magic_link_sent", { email_domain: email.split("@")[1] || "unknown", - }); + }) } catch (error) { - console.error(error); + console.error(error) // Track login failure posthog.capture("login_failed", { method: "magic_link", error: error instanceof Error ? error.message : "Unknown error", email_domain: email.split("@")[1] || "unknown", - }); + }) setError( error instanceof Error ? error.message : "Failed to send login link. Please try again.", - ); - setIsLoading(false); - setIsLoadingEmail(false); - return; + ) + setIsLoading(false) + setIsLoadingEmail(false) + return } - setIsLoading(false); - setIsLoadingEmail(false); - }; + setIsLoading(false) + setIsLoadingEmail(false) + } const handleSubmitToken = async (event: React.FormEvent) => { - event.preventDefault(); - setIsLoading(true); + event.preventDefault() + setIsLoading(true) - const formData = new FormData(event.currentTarget); - const token = formData.get("token") as string; + const formData = new FormData(event.currentTarget) + const token = formData.get("token") as string router.push( `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(window.location.host)}`, - ); - }; + ) + } return (
@@ -197,8 +225,8 @@ export function LoginPage({ disabled: isLoading, id: "email", onChange: (e) => { - setEmail(e.target.value); - error && setError(null); + setEmail(e.target.value) + error && setError(null) }, required: true, value: email, @@ -207,124 +235,149 @@ export function LoginPage({ label="Email" /> - +
+ + {lastUsedMethod === 'magic_link' && ( +
+ Last used +
+ )} +
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED || - !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( ) : null}
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( - - Google - - - - - - } - authProvider="Google" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "google", - }); - signIn - .social({ - callbackURL: window.location.origin, + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( +
+ + Google + + + + + + } + authProvider="Google" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", provider: "google", }) - .finally(() => { - setIsLoading(false); - }); - }} - /> + setPendingLoginMethod('google') + signIn + .social({ + callbackURL: window.location.origin, + provider: "google", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === 'google' && ( +
+ Last used +
+ )} +
) : null} {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( - - Github - - - - - - + + Github + + - - - - } - authProvider="Github" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "github", - }); - signIn - .social({ - callbackURL: window.location.origin, + + + + + + + + } + authProvider="Github" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", provider: "github", }) - .finally(() => { - setIsLoading(false); - }); - }} - /> + setPendingLoginMethod('github') + signIn + .social({ + callbackURL: window.location.origin, + provider: "github", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === 'github' && ( +
+ Last used +
+ )} +
) : null} @@ -350,5 +403,5 @@ export function LoginPage({ )}
- ); -} + ) +} \ No newline at end of file