Files
claudemesh/apps/web/src/modules/auth/form/social-providers.tsx
Alejandro Gutiérrez e8ad7a5b19
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
fix(web): auth UX polish batch — guest button, oauth labels
Three launch-visible friction fixes:

#3: "Continuar como invitado" (anonymous sign-in) removed. claudemesh
    requires an account — mesh membership, invite issuance, and audit
    trails are all tied to a user.id. Flipping the toggle is enough:
    the AnonymousLogin component is gated by
    `authConfig.providers.anonymous` in login.tsx, so disabling the
    flag makes the button disappear from both /login and /register.

#4: OAuth buttons now show proper brand labels. Was rendering lowercase
    "github" / "google" / "apple" via capitalize CSS (which users read
    as "is this broken?"). Now renders "Continue with GitHub" /
    "Continue with Google" / "Continue with Apple" next to the existing
    brand icons. Also swapped layout: was `grow basis-28` (side-by-side
    chips), now `w-full justify-center` (stacked full-width buttons) —
    matches claude.com login styling more closely.

#6: Session hydration race on /dashboard — NON-ISSUE verified. The
    0-mesh redirect runs in a Server Component AFTER
    /dashboard/layout.tsx's getSession() gate. Server api.ts forwards
    cookies to the Hono backend, so no client-side auth state is in
    play. No fix needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:55:09 +01:00

125 lines
3.3 KiB
TypeScript

"use client";
import { useMutation } from "@tanstack/react-query";
import { memo } from "react";
import { SocialProvider as SocialProviderType } from "@turbostarter/auth";
import { useTranslation } from "@turbostarter/i18n";
import { Badge } from "@turbostarter/ui-web/badge";
import { Button } from "@turbostarter/ui-web/button";
import { Icons } from "@turbostarter/ui-web/icons";
import { pathsConfig } from "~/config/paths";
import { authClient } from "~/lib/auth/client";
import { useAuthFormStore } from "~/modules/auth/form/store";
import { auth } from "../lib/api";
import type { AuthProvider } from "@turbostarter/auth";
import type { Icon } from "@turbostarter/ui-web/icons";
interface SocialProvidersProps {
readonly providers: SocialProviderType[];
readonly redirectTo?: string;
}
export const SocialIcons: Record<SocialProviderType, Icon> = {
[SocialProviderType.GITHUB]: Icons.Github,
[SocialProviderType.GOOGLE]: Icons.Google,
[SocialProviderType.APPLE]: Icons.Apple,
};
const PROVIDER_LABELS: Record<SocialProviderType, string> = {
[SocialProviderType.GITHUB]: "GitHub",
[SocialProviderType.GOOGLE]: "Google",
[SocialProviderType.APPLE]: "Apple",
};
const SocialProvider = ({
provider,
isSubmitting,
onClick,
actualProvider,
}: {
provider: SocialProviderType;
isSubmitting: boolean;
onClick: () => void;
actualProvider: AuthProvider;
}) => {
const { t } = useTranslation("common");
const Icon = SocialIcons[provider];
return (
<Button
key={provider}
variant="outline"
type="button"
size="lg"
className="relative w-full justify-center gap-2"
disabled={isSubmitting}
onClick={onClick}
>
{isSubmitting && actualProvider === provider ? (
<Icons.Loader2 className="animate-spin" />
) : (
<>
<Icon className="size-5 dark:brightness-125" />
<span className="leading-none">
Continue with {PROVIDER_LABELS[provider]}
</span>
</>
)}
{authClient.isLastUsedLoginMethod(provider) && (
<Badge className="absolute top-0 -right-4 z-10 -translate-y-1/2 shadow-sm">
{t("lastUsed")}
</Badge>
)}
</Button>
);
};
export const SocialProviders = memo<SocialProvidersProps>(
({ providers, redirectTo = pathsConfig.dashboard.user.index }) => {
const {
provider: actualProvider,
setProvider,
isSubmitting,
setIsSubmitting,
} = useAuthFormStore();
const signIn = useMutation({
...auth.mutations.signIn.social,
onMutate: ({ provider }) => {
setProvider(provider as SocialProviderType);
setIsSubmitting(true);
},
onSettled: () => {
setIsSubmitting(false);
},
});
return (
<div className="flex flex-wrap gap-2">
{Object.values(providers).map((provider) => (
<SocialProvider
key={provider}
provider={provider}
isSubmitting={isSubmitting}
onClick={() =>
signIn.mutate({
provider,
callbackURL: redirectTo,
errorCallbackURL: pathsConfig.auth.error,
})
}
actualProvider={actualProvider}
/>
))}
</div>
);
},
);
SocialProviders.displayName = "SocialProviders";