diff --git a/README.md b/README.md index f5cb751..cc89889 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ docker pull ghcr.io/bedolaga-dev/bedolaga-cabinet:latest ```bash # Создать временный контейнер и скопировать статику docker create --name tmp_cabinet ghcr.io/bedolaga-dev/bedolaga-cabinet:latest -docker cp tmp_cabinet:/usr/share/nginx/html ./cabinet-dist +mkdir -p ./cabinet-dist +docker cp tmp_cabinet:/usr/share/nginx/html/. ./cabinet-dist/ docker rm tmp_cabinet ``` @@ -77,10 +78,11 @@ VITE_APP_LOGO=V Соберите и извлеките: ```bash -docker compose build -docker create --name tmp_cabinet cabinet_frontend -docker cp tmp_cabinet:/usr/share/nginx/html ./cabinet-dist -docker rm tmp_cabinet +docker compose build cabinet-frontend +docker compose create cabinet-frontend +mkdir -p ./cabinet-dist +docker cp cabinet_frontend:/usr/share/nginx/html/. ./cabinet-dist/ +docker compose rm -sf cabinet-frontend ``` ### Шаг 3. Размещение файлов на сервере diff --git a/src/api/subscription.ts b/src/api/subscription.ts index 529e934..2af14bf 100644 --- a/src/api/subscription.ts +++ b/src/api/subscription.ts @@ -449,6 +449,9 @@ export const subscriptionApi = { display_link: string | null; happ_redirect_link: string | null; happ_scheme_link: string | null; + happ_cryptolink?: string | null; + happ_crypto_link?: string | null; + happ_link?: string | null; connect_mode: string; hide_link: boolean; instructions: { diff --git a/src/pages/AdminPromoGroupCreate.tsx b/src/pages/AdminPromoGroupCreate.tsx index a9ee620..11d7416 100644 --- a/src/pages/AdminPromoGroupCreate.tsx +++ b/src/pages/AdminPromoGroupCreate.tsx @@ -135,7 +135,7 @@ export default function AdminPromoGroupCreate() { periodDiscounts.forEach((pd) => { const days = pd.days === '' ? 0 : pd.days; const percent = pd.percent === '' ? 0 : pd.percent; - if (days > 0 && percent > 0) { + if (days > 0 && percent >= 0) { periodDiscountsRecord[days] = percent; } }); @@ -150,8 +150,7 @@ export default function AdminPromoGroupCreate() { server_discount_percent: serverVal, traffic_discount_percent: trafficVal, device_discount_percent: deviceVal, - period_discounts: - Object.keys(periodDiscountsRecord).length > 0 ? periodDiscountsRecord : undefined, + period_discounts: periodDiscountsRecord, apply_discounts_to_addons: applyToAddons, auto_assign_total_spent_kopeks: autoAssignVal > 0 ? Math.round(autoAssignVal * 100) : 0, is_default: isDefault, diff --git a/src/pages/Connection.tsx b/src/pages/Connection.tsx index d22e5f4..a36c049 100644 --- a/src/pages/Connection.tsx +++ b/src/pages/Connection.tsx @@ -7,6 +7,7 @@ import { subscriptionApi } from '../api/subscription'; import { useTelegramSDK } from '../hooks/useTelegramSDK'; import { useHaptic } from '@/platform'; import { resolveTemplate, hasTemplates } from '../utils/templateEngine'; +import { isHappCryptolinkMode, resolveConnectionUrlForUi } from '../utils/connectionLink'; import { useAuthStore } from '../store/auth'; import type { AppConfig, RemnawavePlatformData } from '../types'; import InstallationGuide from '../components/connection/InstallationGuide'; @@ -32,21 +33,59 @@ export default function Connection() { queryKey: ['appConfig', subId], queryFn: () => subscriptionApi.getAppConfig(subId), }); + const { data: connectionLink, isLoading: isConnectionLinkLoading } = useQuery({ + queryKey: ['connectionLink', subId], + queryFn: () => subscriptionApi.getConnectionLink(subId), + retry: false, + staleTime: 0, + }); + + const qrConnectionUrl = useMemo( + () => + resolveConnectionUrlForUi({ + mode: connectionLink?.connect_mode, + happSchemeLink: connectionLink?.happ_scheme_link, + displayLink: connectionLink?.display_link, + subscriptionUrl: connectionLink?.subscription_url, + happCryptLink: connectionLink?.happ_cryptolink, + happCryptoLink: connectionLink?.happ_crypto_link, + happLink: connectionLink?.happ_link, + fallbackUrl: appConfig?.subscriptionUrl, + }), + [ + appConfig?.subscriptionUrl, + connectionLink?.connect_mode, + connectionLink?.display_link, + connectionLink?.happ_cryptolink, + connectionLink?.happ_crypto_link, + connectionLink?.happ_link, + connectionLink?.happ_scheme_link, + connectionLink?.subscription_url, + ], + ); const handleGoBack = useCallback(() => { navigate(-1); }, [navigate]); const handleOpenQR = useCallback(() => { + if (!qrConnectionUrl) return; navigate('/connection/qr', { replace: !isTelegramWebApp, state: { - url: appConfig?.subscriptionUrl, - hideLink: appConfig?.hideLink ?? false, + url: qrConnectionUrl, + hideLink: connectionLink?.hide_link ?? appConfig?.hideLink ?? false, subscriptionId: subId, }, }); - }, [navigate, appConfig?.subscriptionUrl, appConfig?.hideLink, isTelegramWebApp, subId]); + }, [ + navigate, + qrConnectionUrl, + connectionLink?.hide_link, + appConfig?.hideLink, + isTelegramWebApp, + subId, + ]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -73,24 +112,30 @@ export default function Connection() { const openDeepLink = useCallback( (deepLink: string) => { let resolved = deepLink; - if (hasTemplates(resolved)) { + if (isHappCryptolinkMode(connectionLink?.connect_mode) && qrConnectionUrl) { + // In HAPP cryptolink mode always open the resolved happ://crypt... URL. + resolved = qrConnectionUrl; + } else if (hasTemplates(resolved)) { resolved = resolveUrl(resolved); } - - const finalUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(resolved)}&lang=${i18n.language || 'en'}`; + const isHttpUrl = /^https?:\/\//i.test(resolved); + const finalUrlForTelegram = isHttpUrl + ? resolved + : `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(resolved)}&lang=${i18n.language || 'en'}`; if (isTelegramWebApp) { try { - sdkOpenLink(finalUrl, { tryInstantView: false }); + sdkOpenLink(finalUrlForTelegram, { tryInstantView: false }); return; } catch { // SDK not available, fallback } } - window.location.href = finalUrl; + // In regular browsers open deeplink directly (without intermediate redirect page). + window.location.href = resolved; }, - [isTelegramWebApp, i18n.language, resolveUrl], + [isTelegramWebApp, i18n.language, resolveUrl, connectionLink?.connect_mode, qrConnectionUrl], ); // Check if any platform has configured apps @@ -101,7 +146,7 @@ export default function Connection() { ); }, [appConfig?.platforms]); - if (isLoading) { + if (isLoading || isConnectionLinkLoading) { return (
diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 9f3a14e..e8ffbeb 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback, memo } from 'react'; +import { useState, useEffect, useRef, useCallback, useMemo, memo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Navigate, useNavigate, useParams } from 'react-router'; @@ -18,6 +18,7 @@ import { useCloseOnSuccessNotification } from '../store/successNotification'; import PurchaseCTAButton from '../components/subscription/PurchaseCTAButton'; import { CopyIcon, CheckIcon } from '../components/icons'; import { useHaptic } from '../platform'; +import { resolveConnectionUrlForUi } from '../utils/connectionLink'; import { getErrorMessage, getInsufficientBalanceError, @@ -221,9 +222,41 @@ export default function Subscription() { staleTime: 0, refetchOnMount: 'always', }); + const { data: connectionLink, isLoading: isConnectionLinkLoading } = useQuery({ + queryKey: ['connection-link', subscriptionId], + queryFn: () => subscriptionApi.getConnectionLink(subscriptionId), + retry: false, + staleTime: 0, + }); // Extract subscription from response (null if no subscription) const subscription = subscriptionResponse?.subscription ?? null; + const displayedConnectionUrl = useMemo( + () => + resolveConnectionUrlForUi({ + mode: connectionLink?.connect_mode, + happSchemeLink: connectionLink?.happ_scheme_link, + displayLink: connectionLink?.display_link, + subscriptionUrl: connectionLink?.subscription_url, + happCryptLink: connectionLink?.happ_cryptolink, + happCryptoLink: connectionLink?.happ_crypto_link, + happLink: connectionLink?.happ_link, + fallbackUrl: isConnectionLinkLoading ? null : (subscription?.subscription_url ?? null), + }), + [ + connectionLink?.connect_mode, + connectionLink?.display_link, + connectionLink?.happ_cryptolink, + connectionLink?.happ_crypto_link, + connectionLink?.happ_link, + connectionLink?.happ_scheme_link, + connectionLink?.subscription_url, + isConnectionLinkLoading, + subscription?.subscription_url, + ], + ); + const shouldHideConnectionLink = + subscription?.hide_subscription_link || connectionLink?.hide_link; // Traffic zone (theme-aware) — called unconditionally at top level const usedPercent = trafficData?.traffic_used_percent ?? subscription?.traffic_used_percent ?? 0; @@ -453,8 +486,8 @@ export default function Subscription() { }, [subscription, refreshTrafficMutation, subscriptionId]); const copyUrl = () => { - if (subscription?.subscription_url) { - navigator.clipboard.writeText(subscription.subscription_url); + if (displayedConnectionUrl) { + navigator.clipboard.writeText(displayedConnectionUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } @@ -890,16 +923,17 @@ export default function Subscription() { )} {/* ─── Subscription URL ─── */} - {subscription.subscription_url && !subscription.hide_subscription_link && ( + {displayedConnectionUrl && !shouldHideConnectionLink && (
- {subscription.subscription_url} + {displayedConnectionUrl}