Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
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>
125 lines
3.3 KiB
TypeScript
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";
|