230 lines
6.5 KiB
TypeScript
230 lines
6.5 KiB
TypeScript
'use client';
|
||
|
||
import type { JSX } from "react";
|
||
import { useEffect, useMemo, useRef, useState } from "react";
|
||
|
||
import { assertClientEnv, env } from "@/lib/env";
|
||
|
||
type ButtonStatus = "initial" | "ready" | "fallback" | "error";
|
||
|
||
declare global {
|
||
interface Window {
|
||
YaAuthSuggest?: {
|
||
init: (
|
||
config: Record<string, unknown>,
|
||
origin: string,
|
||
options: Record<string, unknown>,
|
||
) => Promise<{
|
||
handler: {
|
||
on: (event: string, callback: () => void) => void;
|
||
redirect: () => void;
|
||
};
|
||
}>;
|
||
};
|
||
}
|
||
}
|
||
|
||
const YANDEX_SDK_URL =
|
||
"https://yastatic.net/s3/passport-sdk/autofill/v1/sdk-suggest.js";
|
||
const YANDEX_SDK_ORIGIN =
|
||
"https://yastatic.net/s3/passport-sdk/autofill/v1/";
|
||
|
||
async function requestBackendRedirect(): Promise<string | null> {
|
||
if (!env.backendBaseUrl) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${env.backendBaseUrl.replace(
|
||
/\/$/,
|
||
"",
|
||
)}/api/v1/auth/authorize/yandex`,
|
||
{
|
||
method: "GET",
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
credentials: "include",
|
||
},
|
||
);
|
||
|
||
if (!response.ok) {
|
||
console.error(
|
||
"Не удалось получить redirect URL с бэкенда",
|
||
response.status,
|
||
);
|
||
return null;
|
||
}
|
||
|
||
const payload = await response.json();
|
||
return payload?.redirect_uri ?? null;
|
||
} catch (error) {
|
||
console.error("Ошибка при запросе redirect URL", error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function YandexLoginButton(): JSX.Element {
|
||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||
const [status, setStatus] = useState<ButtonStatus>("initial");
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||
|
||
const missingEnv = useMemo(() => assertClientEnv(), []);
|
||
|
||
useEffect(() => {
|
||
if (missingEnv.length > 0) {
|
||
setErrorMessage(
|
||
`Не заданы переменные окружения: ${missingEnv.join(
|
||
", ",
|
||
)}. Кнопка Яндекс ID будет скрыта.`,
|
||
);
|
||
setStatus("error");
|
||
return;
|
||
}
|
||
|
||
if (!containerRef.current) {
|
||
return;
|
||
}
|
||
|
||
const initializeButton = () => {
|
||
if (!window.YaAuthSuggest) {
|
||
setErrorMessage("Yandex ID SDK недоступен.");
|
||
setStatus("fallback");
|
||
return;
|
||
}
|
||
|
||
window.YaAuthSuggest.init(
|
||
{
|
||
client_id: env.yandexClientId,
|
||
response_type: "code",
|
||
redirect_uri: env.yandexRedirectUri,
|
||
},
|
||
YANDEX_SDK_ORIGIN,
|
||
{
|
||
view: "button",
|
||
parentId: containerRef.current?.id ?? "",
|
||
buttonView: "main",
|
||
buttonTheme: "dark",
|
||
buttonSize: "l",
|
||
buttonBorderRadius: "16",
|
||
buttonIcon: "ya",
|
||
},
|
||
)
|
||
.then((result) => {
|
||
setStatus("ready");
|
||
const { handler } = result;
|
||
const host = containerRef.current;
|
||
|
||
if (handler && typeof handler.on === "function") {
|
||
handler.on("click", () => {
|
||
try {
|
||
handler.redirect();
|
||
} catch (error) {
|
||
console.error(
|
||
"Ошибка redirect через handler, fallback к бэкенду",
|
||
error,
|
||
);
|
||
void handleFallback();
|
||
}
|
||
});
|
||
} else if (host) {
|
||
host.addEventListener("click", () => {
|
||
if (typeof handler?.redirect === "function") {
|
||
handler.redirect();
|
||
} else {
|
||
void handleFallback();
|
||
}
|
||
});
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Ошибка инициализации Yandex ID", error);
|
||
setErrorMessage("Не удалось инициализировать кнопку Yandex ID.");
|
||
setStatus("fallback");
|
||
});
|
||
};
|
||
|
||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||
`script[src="${YANDEX_SDK_URL}"]`,
|
||
);
|
||
|
||
if (existingScript?.dataset.loaded === "true") {
|
||
initializeButton();
|
||
return;
|
||
}
|
||
|
||
const script = existingScript ?? document.createElement("script");
|
||
|
||
if (!existingScript) {
|
||
script.src = YANDEX_SDK_URL;
|
||
script.async = true;
|
||
script.dataset.loaded = "false";
|
||
script.onload = () => {
|
||
script.dataset.loaded = "true";
|
||
initializeButton();
|
||
};
|
||
script.onerror = () => {
|
||
setErrorMessage("Не удалось загрузить Yandex ID SDK.");
|
||
setStatus("fallback");
|
||
};
|
||
document.body.appendChild(script);
|
||
} else if (existingScript.dataset.loaded === "true") {
|
||
initializeButton();
|
||
} else {
|
||
existingScript.addEventListener("load", initializeButton, { once: true });
|
||
existingScript.addEventListener(
|
||
"error",
|
||
() => {
|
||
setErrorMessage("Не удалось загрузить Yandex ID SDK.");
|
||
setStatus("fallback");
|
||
},
|
||
{ once: true },
|
||
);
|
||
}
|
||
|
||
return () => {
|
||
existingScript?.removeEventListener("load", initializeButton);
|
||
};
|
||
}, [missingEnv]);
|
||
|
||
const handleFallback = async () => {
|
||
setStatus("fallback");
|
||
const target = await requestBackendRedirect();
|
||
|
||
if (target) {
|
||
window.location.href = target;
|
||
} else {
|
||
setErrorMessage(
|
||
"Не удалось автоматически перенаправить на авторизацию Яндекс. Попробуйте позже.",
|
||
);
|
||
setStatus("error");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col items-center gap-4">
|
||
<div
|
||
id="ya-oauth-button"
|
||
ref={containerRef}
|
||
className={
|
||
status === "fallback" || status === "error" ? "hidden" : "flex"
|
||
}
|
||
/>
|
||
{(status === "fallback" || status === "error") && (
|
||
<button
|
||
type="button"
|
||
onClick={handleFallback}
|
||
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-yellow-300 px-5 py-3 text-sm font-semibold text-stone-950 shadow transition hover:bg-yellow-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-400"
|
||
>
|
||
Продолжить через Яндекс
|
||
</button>
|
||
)}
|
||
{errorMessage && (
|
||
<p className="text-center text-sm text-red-300" role="alert">
|
||
{errorMessage}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|