From 6fa4afd5c1b67331a02127dc395476df1fbbd7e5 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 16 May 2026 02:15:17 +0300 Subject: [PATCH] feat(antilopay): inject apay-tag meta on cabinet boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with the bedolaga-bot commit that exposes GET /cabinet/public/site-verification (JSON: { apay_tag: string | null }). New useSiteVerification hook fires once on App mount, fetches the configured tag value, and upserts into document.head. When the bot returns null we proactively remove any previously-rendered tag so toggling the env var off cleans up the page. Failure modes are silent — verification is best-effort and must never block the cabinet from rendering. No admin UI field is needed: the value lives in the bot's .env (ANTILOPAY_APAY_VERIFICATION_TAG), matching how all other Antilopay credentials are configured. --- src/App.tsx | 4 +++ src/api/siteVerification.ts | 16 +++++++++ src/hooks/useSiteVerification.ts | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/api/siteVerification.ts create mode 100644 src/hooks/useSiteVerification.ts diff --git a/src/App.tsx b/src/App.tsx index 33f220f..fce6599 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { ErrorBoundary } from './components/ErrorBoundary'; import { PermissionRoute } from '@/components/auth/PermissionRoute'; import { saveReturnUrl } from './utils/token'; import { useAnalyticsCounters } from './hooks/useAnalyticsCounters'; +import { useSiteVerification } from './hooks/useSiteVerification'; // Auth pages - load immediately (small) import Login from './pages/Login'; import TelegramCallback from './pages/TelegramCallback'; @@ -229,6 +230,9 @@ function LegacySubscriptionRedirect() { function App() { useAnalyticsCounters(); + // Pulls site-verification tokens (Antilopay apay-tag etc.) from the bot + // backend and injects matching tags into document.head. + useSiteVerification(); return ( <> diff --git a/src/api/siteVerification.ts b/src/api/siteVerification.ts new file mode 100644 index 0000000..c11686b --- /dev/null +++ b/src/api/siteVerification.ts @@ -0,0 +1,16 @@ +import { apiClient } from './client'; + +export interface SiteVerification { + apay_tag: string | null; +} + +/** + * Public, unauthenticated endpoint — payment-provider crawlers (Antilopay) + * need to be able to read these values to validate site ownership. + */ +export const siteVerificationApi = { + async get(): Promise { + const { data } = await apiClient.get('/cabinet/public/site-verification'); + return data; + }, +}; diff --git a/src/hooks/useSiteVerification.ts b/src/hooks/useSiteVerification.ts new file mode 100644 index 0000000..b7b68db --- /dev/null +++ b/src/hooks/useSiteVerification.ts @@ -0,0 +1,56 @@ +import { useEffect } from 'react'; +import { siteVerificationApi } from '../api/siteVerification'; + +/** + * Fetches the configured site-verification tokens from the bot backend + * and injects the matching `` tags into `document.head`. + * + * Used by Antilopay's verification flow (lk.antilopay.com → Проект → + * Верификация → Способ 1: мета-тег). Once the merchant copies the + * Antilopay-provided value into `ANTILOPAY_APAY_VERIFICATION_TAG`, + * the cabinet will start rendering `` + * automatically — no rebuild required. + * + * The hook is no-op on backend errors (verification is a best-effort + * concern and must never block the cabinet from rendering). + */ +export function useSiteVerification(): void { + useEffect(() => { + let cancelled = false; + + siteVerificationApi + .get() + .then((data) => { + if (cancelled) return; + if (data.apay_tag) { + upsertMetaTag('apay-tag', data.apay_tag); + } else { + removeMetaTag('apay-tag'); + } + }) + .catch(() => { + // Silent fail — verification meta is best-effort, doesn't block UI. + }); + + return () => { + cancelled = true; + }; + }, []); +} + +function upsertMetaTag(name: string, content: string): void { + let tag = document.head.querySelector(`meta[name="${name}"]`); + if (!tag) { + tag = document.createElement('meta'); + tag.setAttribute('name', name); + document.head.appendChild(tag); + } + tag.setAttribute('content', content); +} + +function removeMetaTag(name: string): void { + const tag = document.head.querySelector(`meta[name="${name}"]`); + if (tag) { + tag.remove(); + } +}