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/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}