* Make SBP default and first top-up option (#389)

* fix(connection): happ cryptolink flow + ui fixes (#385)

* fix(connection): respect happ_cryptolink mode

- read connect_mode from connection-link endpoint
- auto-redirect to happ cryptolink flow when mode is HAPP_CRYPTOLINK
- keep installation guide behavior for other modes

* docs(readme): fix source build step 2

- use compose service name for build/create commands
- copy static files from created compose container
- remove compose container via docker compose rm

* fix(connection): handle mode and ws path

- treat any non-guide connect mode as direct happ redirect
- wait for connection-link response before rendering installation guide
- build websocket URL from VITE_API_URL (supports /api and absolute URLs)

* docs(readme): fix docker cp static path

- copy /usr/share/nginx/html contents via html/.
- prevent nested /srv/cabinet/html deployment and 404 on root

* fix(connection): keep guide, use happ links

- restore guide page behavior for /connection (no auto redirect)
- use happ cryptolink URL in /connection/qr when happ crypt mode is active
- replace subscription page connection URL with happ cryptolink from connection-link API

* fix(connection): avoid wrong redirect flow

- force happ scheme deeplink in HAPP_CRYPTOLINK mode for guide connect buttons
- use cabinet redirect page only inside Telegram Mini App
- open deeplink directly in regular browsers

* fix(connection): enforce happ cryptolink

- prioritize backend crypt deeplink fields in HAPP_CRYPTOLINK mode
- fallback to local crypt4/crypt3 generation from subscription URL when backend returns plain happ://sub...
- apply same resolution for guide open action, qr page source and subscription page link display/copy

* fix(subscription): truncate long connect link

- render connection URL in a single line with ellipsis on /subscriptions
- preserve full URL in tooltip for easier manual copy
- keep copy button behavior unchanged

* fix(connection): build cryptolink from happ sub

- allow cryptolink fallback generation from happ://sub... URLs in addition to http(s)
- prevent plain happ://sub links from leaking into UI in HAPP_CRYPTOLINK mode

* fix: allow saving 0% period discount in promo groups

Changed condition from percent > 0 to percent >= 0 so that
admins can explicitly set 0% discount for a period. Previously
0% entries were silently dropped and not sent to the backend.

* fix: always send period_discounts in promo group updates

When all period discounts were removed, the frontend sent undefined
(field absent from JSON), so the backend never cleared them.
Now always sends the field - empty object {} to clear, or populated
object to update.

---------

Co-authored-by: zavul0nn <34007368+zavul0nn@users.noreply.github.com>
Co-authored-by: Dxnil <62987903+D4nilKO@users.noreply.github.com>
This commit is contained in:
Egor
2026-04-15 14:04:39 +03:00
committed by GitHub
parent 1f833dbdd5
commit ac89343cc1
8 changed files with 257 additions and 43 deletions

View File

@@ -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 && (
<div className="mb-5 flex gap-2">
<code
className="scrollbar-hide flex-1 overflow-x-auto break-all rounded-[10px] px-3 py-2 font-mono text-[11px] text-dark-50/30"
className="block min-w-0 flex-1 truncate whitespace-nowrap rounded-[10px] px-3 py-2 font-mono text-[11px] text-dark-50/30"
style={{
background: g.codeBg,
border: `1px solid ${g.codeBorder}`,
}}
title={displayedConnectionUrl}
>
{subscription.subscription_url}
{displayedConnectionUrl}
</code>
<button
onClick={copyUrl}