fix(connection): keep the Happ add-subscription button working on Remnawave 2.8.0

Panel 2.8.0 removed the server-side happ-encrypt endpoint, so for users
without a stored crypto link the backend leaves {{HAPP_CRYPT4_LINK}}
unresolved and the subscriptionLink button silently disappeared
(isValidDeepLink requires '://').

- BlockButtons: resolve button templates client-side at render (with username,
  like the panel's own subpage) instead of dropping the button; collapse
  double-prefixed happ://crypt4/happ://cryptN/... resolvedUrl from old backends
- templateEngine: collapse a hardcoded happ://cryptN/ prefix before
  {{HAPP_CRYPT[34]_LINK}}; cache crypt-link generation (jsencrypt RSA-4096 is
  slow on weak devices and re-randomizes every call)
- Connection: in happ_cryptolink mode force the crypt link only when the button
  fell back to the plain subscription link or kept unresolved templates - an
  explicit Subpage link (e.g. happ://add/...) now wins, so Subpage edits apply
This commit is contained in:
Fringg
2026-07-02 16:12:18 +03:00
parent 984d1b0776
commit 8f31311112
4 changed files with 71 additions and 9 deletions

View File

@@ -48,6 +48,7 @@ interface Props {
isTelegramWebApp: boolean;
onGoBack: () => void;
onOpenQR?: () => void;
username?: string;
}
export default function InstallationGuide({
@@ -56,6 +57,7 @@ export default function InstallationGuide({
isTelegramWebApp,
onGoBack,
onOpenQR,
username,
}: Props) {
const { t, i18n } = useTranslation();
const { isLight } = useTheme();
@@ -131,6 +133,7 @@ export default function InstallationGuide({
subscriptionUrl={appConfig.subscriptionUrl}
hideLink={appConfig.hideLink}
deepLink={selectedApp?.deepLink}
username={username}
getLocalizedText={getLocalizedText}
getBaseTranslation={getBaseTranslation}
getSvgHtml={getSvgHtml}
@@ -141,6 +144,7 @@ export default function InstallationGuide({
appConfig.subscriptionUrl,
appConfig.hideLink,
selectedApp?.deepLink,
username,
isLight,
getLocalizedText,
getBaseTranslation,

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { CheckIcon, CopyIcon } from '@/components/icons';
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
import { copyToClipboard } from '@/utils/clipboard';
import { collapseDoubledCryptPrefix, hasTemplates, resolveTemplate } from '@/utils/templateEngine';
import { blockButtonClass } from './buttonStyles';
// eslint-disable-next-line no-script-url
@@ -29,6 +30,7 @@ interface BlockButtonsProps {
subscriptionUrl: string | null;
hideLink?: boolean;
deepLink?: string | null;
username?: string;
getLocalizedText: (text: LocalizedText | undefined) => string;
getBaseTranslation: (key: string, i18nKey: string) => string;
getSvgHtml: (key: string | undefined) => string;
@@ -42,6 +44,7 @@ export function BlockButtons({
subscriptionUrl,
hideLink,
deepLink,
username,
getLocalizedText,
getBaseTranslation,
getSvgHtml,
@@ -73,8 +76,17 @@ export function BlockButtons({
) : null;
if (btn.type === 'subscriptionLink') {
const url = btn.resolvedUrl || btn.url || btn.link || deepLink || subscriptionUrl;
if (!url || !isValidDeepLink(url)) return null;
const raw = btn.resolvedUrl || btn.url || btn.link || deepLink || subscriptionUrl;
// The backend resolves {{HAPP_CRYPT*_LINK}} only when a crypto link is
// stored in the DB; since Remnawave 2.8.0 removed the encrypt endpoint,
// new users can get the raw template here. Resolve it client-side (the
// panel's own subpage does the same) instead of dropping the button.
const resolved =
raw && subscriptionUrl && hasTemplates(raw)
? resolveTemplate(raw, { subscriptionUrl, username })
: raw;
const url = resolved ? collapseDoubledCryptPrefix(resolved) : resolved;
if (!url || hasTemplates(url) || !isValidDeepLink(url)) return null;
return (
<button
key={idx}