diff --git a/miniapp/index.html b/miniapp/index.html
index 8ca5075c..322f5f83 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -254,18 +254,6 @@
z-index: 1;
}
- .logo-icon.has-image {
- font-size: 0;
- line-height: 0;
- }
-
- .logo-icon.has-image img {
- width: 60px;
- height: 60px;
- object-fit: contain;
- display: block;
- }
-
.logo {
font-size: 28px;
font-weight: 800;
@@ -3865,13 +3853,6 @@
margin-bottom: 4px;
}
- .app-meta {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 6px;
- }
-
.featured-badge {
display: inline-block;
background: linear-gradient(135deg, var(--primary), rgba(var(--primary-rgb), 0.8));
@@ -3884,19 +3865,6 @@
letter-spacing: 0.5px;
}
- .app-platform {
- display: inline-flex;
- align-items: center;
- padding: 4px 10px;
- border-radius: 20px;
- background: var(--bg-secondary);
- color: var(--text-secondary);
- font-size: 12px;
- font-weight: 600;
- text-transform: none;
- letter-spacing: 0;
- }
-
.app-steps {
margin-top: 16px;
}
@@ -5526,15 +5494,7 @@
// All original constants and functions
const LANG_STORAGE_KEY = 'remnawave-miniapp-language';
- const DEFAULT_SUPPORTED_LANGUAGES = ['en', 'ru'];
- const BASE_LANGUAGE_ORDER = ['en', 'ru'];
- const supportedLanguages = new Set(DEFAULT_SUPPORTED_LANGUAGES);
- const LANGUAGE_LABELS = {
- en: '🇬🇧 English',
- ru: '🇷🇺 Русский',
- zh: '🇨🇳 中文',
- fa: '🇮🇷 فارسی',
- };
+ const SUPPORTED_LANGUAGES = ['en', 'ru'];
const translations = {
en: {
@@ -5779,11 +5739,8 @@
'apps.no_data': 'No installation guide available for this platform yet.',
'apps.featured': 'Recommended',
'apps.step.download': 'Download & install',
- 'apps.step.before_add': 'Before adding subscription',
'apps.step.add': 'Add subscription',
- 'apps.step.after_add': 'If the subscription is not added',
'apps.step.connect': 'Connect & use',
- 'apps.button.open_link': 'Open link',
'faq.title': 'FAQ',
'faq.item_default_title': 'Question {index}',
'faq.item_empty': 'Answer will be added soon.',
@@ -5905,11 +5862,6 @@
'platform.android': 'Android',
'platform.pc': 'PC',
'platform.tv': 'TV',
- 'platform.windows': 'Windows',
- 'platform.macos': 'macOS',
- 'platform.linux': 'Linux',
- 'platform.android_tv': 'Android TV',
- 'platform.apple_tv': 'Apple TV',
'units.gb': 'GB',
'values.unlimited': 'Unlimited',
'values.not_available': 'Not available',
@@ -6182,11 +6134,8 @@
'apps.no_data': 'Для этой платформы инструкция пока недоступна.',
'apps.featured': 'Рекомендуем',
'apps.step.download': 'Скачать и установить',
- 'apps.step.before_add': 'Перед добавлением подписки',
'apps.step.add': 'Добавить подписку',
- 'apps.step.after_add': 'Если подписка не добавилась',
'apps.step.connect': 'Подключиться и пользоваться',
- 'apps.button.open_link': 'Открыть ссылку',
'faq.title': 'FAQ',
'faq.item_default_title': 'Вопрос {index}',
'faq.item_empty': 'Ответ будет добавлен позже.',
@@ -6308,11 +6257,6 @@
'platform.android': 'Android',
'platform.pc': 'ПК',
'platform.tv': 'ТВ',
- 'platform.windows': 'Windows',
- 'platform.macos': 'macOS',
- 'platform.linux': 'Linux',
- 'platform.android_tv': 'Android TV',
- 'platform.apple_tv': 'Apple TV',
'units.gb': 'ГБ',
'values.unlimited': 'Безлимит',
'values.not_available': 'Недоступно',
@@ -6364,101 +6308,6 @@
Object.assign(translations[lang], pcTranslations[lang]);
});
- function getLanguageLabel(code) {
- const normalized = String(code ?? '').toLowerCase();
- if (!normalized) {
- return '';
- }
- const base = normalized.split('-')[0];
- return LANGUAGE_LABELS[normalized] || LANGUAGE_LABELS[base] || normalized.toUpperCase();
- }
-
- function getSupportedLanguages() {
- return Array.from(supportedLanguages);
- }
-
- function addSupportedLanguages(locales = []) {
- let updated = false;
- locales.forEach(locale => {
- if (!locale) {
- return;
- }
- const normalized = String(locale).toLowerCase();
- if (!normalized) {
- return;
- }
- if (!supportedLanguages.has(normalized)) {
- supportedLanguages.add(normalized);
- updated = true;
- }
- });
- return updated;
- }
-
- function renderLanguageSelectOptions() {
- const select = document.getElementById('languageSelect');
- if (!select) {
- return;
- }
-
- const normalizedPreferred = resolveLanguage(preferredLanguage) || 'en';
- if (!supportedLanguages.has(normalizedPreferred)) {
- supportedLanguages.add(normalizedPreferred);
- }
-
- const baseOrder = BASE_LANGUAGE_ORDER.map(lang => lang.toLowerCase());
- const extras = getSupportedLanguages()
- .map(lang => lang.toLowerCase())
- .filter(lang => !baseOrder.includes(lang));
- extras.sort((a, b) => a.localeCompare(b));
-
- const order = [...baseOrder];
- extras.forEach(lang => {
- if (!order.includes(lang)) {
- order.push(lang);
- }
- });
- if (!order.includes(normalizedPreferred)) {
- order.push(normalizedPreferred);
- }
-
- const seen = new Set();
- const optionsHtml = order.map(lang => {
- const normalized = String(lang).toLowerCase();
- if (!normalized || seen.has(normalized)) {
- return '';
- }
- seen.add(normalized);
- const value = escapeHtml(normalized);
- const label = escapeHtml(getLanguageLabel(normalized));
- return ``;
- }).filter(Boolean).join('');
-
- select.innerHTML = optionsHtml;
- select.value = normalizedPreferred;
-
- if (preferredLanguage !== normalizedPreferred) {
- preferredLanguage = normalizedPreferred;
- }
- }
-
- const PLATFORM_GROUPS = {
- ios: ['ios'],
- android: ['android'],
- pc: ['windows', 'macos', 'linux'],
- tv: ['androidTV', 'appleTV'],
- };
-
- const PLATFORM_LABEL_KEYS = {
- ios: 'platform.ios',
- android: 'platform.android',
- windows: 'platform.windows',
- macos: 'platform.macos',
- linux: 'platform.linux',
- androidTV: 'platform.android_tv',
- appleTV: 'platform.apple_tv',
- };
-
const LEGAL_DOCUMENT_CONFIG = {
public_offer: {
icon: '📜',
@@ -6483,44 +6332,26 @@
return;
}
+ const {
+ service_name: rawServiceName = {},
+ service_description: rawServiceDescription = {}
+ } = branding;
+
function normalizeMap(map) {
const normalized = {};
- if (!map) {
- return normalized;
- }
-
- if (typeof map === 'string') {
- const normalizedValue = normalizeLocalizedValue(map);
- if (normalizedValue) {
- normalized.default = normalizedValue;
- }
- return normalized;
- }
-
Object.entries(map || {}).forEach(([lang, value]) => {
- const normalizedValue = normalizeLocalizedValue(value);
- if (!normalizedValue) {
+ if (typeof value !== 'string') {
return;
}
- normalized[lang.toLowerCase()] = normalizedValue;
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return;
+ }
+ normalized[lang.toLowerCase()] = trimmed;
});
-
return normalized;
}
- function mergeLocaleMaps(...sources) {
- const result = {};
- sources.forEach(source => {
- const normalized = normalizeMap(source);
- Object.entries(normalized).forEach(([lang, value]) => {
- if (value) {
- result[lang] = value;
- }
- });
- });
- return result;
- }
-
function applyKey(key, map) {
const normalized = normalizeMap(map);
if (!Object.keys(normalized).length) {
@@ -6557,43 +6388,9 @@
});
}
- const serviceNameMap = mergeLocaleMaps(
- branding.service_name,
- branding.name,
- branding.title
- );
- const serviceDescriptionMap = mergeLocaleMaps(
- branding.service_description,
- branding.description,
- branding.subtitle
- );
-
- applyKey('app.name', serviceNameMap);
- applyKey('app.title', serviceNameMap);
- applyKey('app.subtitle', serviceDescriptionMap);
-
- const logoUrl = normalizeUrl(
- branding.logoUrl
- || branding.logo_url
- || branding.logo
- || null
- );
-
- if (logoUrl) {
- const logoIcon = document.querySelector('.logo-icon');
- if (logoIcon) {
- logoIcon.classList.add('has-image');
- logoIcon.innerHTML = '';
- const img = document.createElement('img');
- img.src = logoUrl;
- const altText = serviceNameMap.default
- || serviceNameMap.en
- || serviceNameMap.ru
- || 'Logo';
- img.alt = altText;
- logoIcon.appendChild(img);
- }
- }
+ applyKey('app.name', rawServiceName);
+ applyKey('app.title', rawServiceName);
+ applyKey('app.subtitle', rawServiceDescription);
}
let userData = null;
@@ -7222,11 +7019,11 @@
return null;
}
const normalized = String(lang).toLowerCase();
- if (supportedLanguages.has(normalized)) {
+ if (SUPPORTED_LANGUAGES.includes(normalized)) {
return normalized;
}
const short = normalized.split('-')[0];
- if (supportedLanguages.has(short)) {
+ if (SUPPORTED_LANGUAGES.includes(short)) {
return short;
}
return null;
@@ -7400,16 +7197,7 @@
if (!label) {
return;
}
- const happData = userData?.happ || userData?.happData || null;
- const useHappLabel = Boolean(
- getHappCryptoLink()
- || userData?.happ_cryptolink_redirect_link
- || userData?.happCryptolinkRedirectLink
- || happData?.redirectLink
- || happData?.redirect_link
- || happData?.redirectUrl
- || happData?.redirect_url
- );
+ const useHappLabel = Boolean(getHappCryptoLink() || userData?.happ_cryptolink_redirect_link);
const key = useHappLabel ? 'button.connect.happ' : 'button.connect.default';
label.textContent = t(key);
}
@@ -7450,7 +7238,6 @@
}
}
- renderLanguageSelectOptions();
applyTranslations();
updateConnectButtonLabel();
@@ -7845,13 +7632,9 @@
}
const data = await response.json();
- appsConfig = sanitizeAppsConfig(data?.platforms || {});
+ appsConfig = data?.platforms || {};
const configData = data?.config || {};
- if (configData.branding || data?.branding) {
- applyBrandingOverrides(configData.branding || data.branding);
- }
-
const configUrl = normalizeUrl(
configData.subscriptionPurchaseUrl
|| configData.subscription_purchase_url
@@ -7865,45 +7648,6 @@
if (configUrl) {
configPurchaseUrl = configUrl;
}
-
- const additionalLocalesRaw = configData.additionalLocales
- || configData.additional_locales
- || data?.additionalLocales
- || data?.additional_locales
- || [];
- const additionalLocales = (Array.isArray(additionalLocalesRaw)
- ? additionalLocalesRaw
- : [additionalLocalesRaw])
- .map(locale => typeof locale === 'string' ? locale.trim() : '')
- .filter(Boolean);
-
- const previousLanguage = preferredLanguage;
- const languagesUpdated = addSupportedLanguages(additionalLocales);
-
- if (languagesUpdated && !languageLockedByUser) {
- const storedLanguageRaw = safeGetStoredLanguage();
- const storedResolved = resolveLanguage(storedLanguageRaw);
- if (storedResolved) {
- preferredLanguage = storedResolved;
- languageLockedByUser = true;
- } else {
- const telegramResolved = resolveLanguage(tg.initDataUnsafe?.user?.language_code);
- if (telegramResolved) {
- preferredLanguage = telegramResolved;
- }
- }
- }
-
- const normalizedPreferred = resolveLanguage(preferredLanguage) || 'en';
- if (preferredLanguage !== normalizedPreferred) {
- preferredLanguage = normalizedPreferred;
- }
-
- renderLanguageSelectOptions();
-
- if (languagesUpdated || preferredLanguage !== previousLanguage) {
- refreshAfterLanguageChange();
- }
} catch (error) {
console.warn('Unable to load apps configuration:', error);
appsConfig = {};
@@ -9034,25 +8778,20 @@
});
}
- function getPlatformLabel(platformKey) {
- const labelKey = PLATFORM_LABEL_KEYS[platformKey];
- if (!labelKey) {
- return null;
- }
- const label = t(labelKey);
- return label === labelKey ? platformKey : label;
+ function getPlatformKey(platform) {
+ const mapping = {
+ ios: 'ios',
+ android: 'android',
+ pc: 'windows',
+ tv: 'androidTV',
+ mac: 'macos'
+ };
+ return mapping[platform] || platform;
}
function getAppsForCurrentPlatform() {
- const platformKeys = PLATFORM_GROUPS[currentPlatform] || [currentPlatform];
- const aggregated = [];
- platformKeys.forEach(key => {
- const apps = Array.isArray(appsConfig?.[key]) ? appsConfig[key] : [];
- apps.forEach(app => {
- aggregated.push({ ...app, __platformKey: key });
- });
- });
- return aggregated;
+ const platformKey = getPlatformKey(currentPlatform);
+ return appsConfig?.[platformKey] || [];
}
function resolveConnectButtonLabel() {
@@ -9064,10 +8803,7 @@
function renderConnectActionButton(appId = '') {
const label = escapeHtml(resolveConnectButtonLabel());
- const normalizedId = typeof appId === 'string' ? appId.trim() : '';
- const appAttribute = normalizedId
- ? ` data-app-id="${escapeHtml(normalizedId)}"`
- : '';
+ const appAttribute = appId ? ` data-app-id="${escapeHtml(appId)}"` : '';
return `
@@ -9081,16 +8817,13 @@
`;
}
- function attachInstructionConnectHandlers(scope, apps = null) {
+ function attachInstructionConnectHandlers(scope) {
if (!scope || typeof scope.querySelectorAll !== 'function') {
return;
}
-
- const appList = Array.isArray(apps) ? apps : getAppsForCurrentPlatform();
scope.querySelectorAll('.step-final-button').forEach(button => {
button.addEventListener('click', () => {
- const appId = button.getAttribute('data-app-id') || null;
- const link = getConnectLink(appId, { apps: appList });
+ const link = getConnectLink();
if (!link) {
return;
}
@@ -9107,13 +8840,13 @@
}
const apps = getAppsForCurrentPlatform();
- const hasConnectLink = hasAnyConnectLink(apps);
+ const hasConnectLink = Boolean(getConnectLink());
if (!apps.length) {
const noDataMessage = `
${escapeHtml(t('apps.no_data'))}
`;
const fallbackButton = hasConnectLink ? renderConnectActionButton() : '';
container.innerHTML = `${noDataMessage}${fallbackButton}`;
- attachInstructionConnectHandlers(container, apps);
+ attachInstructionConnectHandlers(container);
updateInstructionConnectButtons(hasConnectLink);
return;
}
@@ -9124,19 +8857,13 @@
const featuredBadge = app.isFeatured
? `
${escapeHtml(t('apps.featured'))}`
: '';
- const platformLabel = app.__platformKey ? getPlatformLabel(app.__platformKey) : null;
- const platformBadge = platformLabel
- ? `
${escapeHtml(platformLabel)}`
- : '';
- const badges = [featuredBadge, platformBadge].filter(Boolean).join('');
- const metaSection = badges ? `
${badges}
` : '';
return `
@@ -9146,151 +8873,73 @@
`;
}).join('');
- attachInstructionConnectHandlers(container, apps);
+ attachInstructionConnectHandlers(container);
updateInstructionConnectButtons(hasConnectLink);
}
- function renderInstructionStep(step, options = {}) {
- if (!step) {
- return '';
- }
-
- const {
- number = null,
- defaultTitleKey = '',
- defaultTitle = '',
- } = options;
-
- let title = '';
- let explicitTitle = '';
- if (Object.prototype.hasOwnProperty.call(step, 'title')) {
- title = getLocalizedText(step.title);
- explicitTitle = title;
- }
-
- if (!title) {
- if (defaultTitleKey) {
- const translated = t(defaultTitleKey);
- title = translated && translated !== defaultTitleKey
- ? translated
- : (defaultTitle || '');
- } else {
- title = defaultTitle || '';
- }
- }
-
- const description = Object.prototype.hasOwnProperty.call(step, 'description')
- ? getLocalizedText(step.description)
- : '';
-
- const buttons = Array.isArray(step.buttons)
- ? step.buttons.filter(btn => btn && btn.buttonLink)
- : [];
-
- const buttonsHtml = buttons.length
- ? `
-
- `
- : '';
-
- const hasExplicitTitle = Boolean(explicitTitle);
- const hasContent = Boolean(description || buttonsHtml.trim().length || hasExplicitTitle);
- if (!hasContent) {
- return '';
- }
-
- const titleHtml = title ? `
${escapeHtml(title)}
` : '';
- const descriptionHtml = description
- ? `
${description}
`
- : '';
-
- const stepNumber = Number.isFinite(number) ? Number(number) : null;
- const numberHtml = stepNumber !== null
- ? `
${stepNumber}`
- : '
';
-
- return `
-
- ${numberHtml}
-
- ${titleHtml}
- ${descriptionHtml}
- ${buttonsHtml}
-
-
- `;
- }
-
function renderAppSteps(app) {
- const parts = [];
+ let html = '';
let stepNum = 1;
- function appendStep(step, defaults) {
- const stepHtml = renderInstructionStep(step, {
- number: stepNum,
- ...(defaults || {}),
- });
- if (stepHtml) {
- parts.push(stepHtml);
- stepNum += 1;
- }
+ if (app.installationStep) {
+ const descriptionHtml = app.installationStep.description
+ ? `
${getLocalizedText(app.installationStep.description)}
`
+ : '';
+ const buttonsHtml = Array.isArray(app.installationStep.buttons) && app.installationStep.buttons.length
+ ? `
+
+ `
+ : '';
+ html += `
+
+
${stepNum++}
+
+
${escapeHtml(t('apps.step.download'))}
+ ${descriptionHtml}
+ ${buttonsHtml}
+
+
+ `;
}
- appendStep(app.installationStep, {
- defaultTitleKey: 'apps.step.download',
- defaultTitle: 'Download & install',
- });
+ if (app.addSubscriptionStep) {
+ html += `
+
+
${stepNum++}
+
+
${escapeHtml(t('apps.step.add'))}
+
${getLocalizedText(app.addSubscriptionStep.description)}
+
+
+ `;
+ }
- appendStep(app.additionalBeforeAddSubscriptionStep, {
- defaultTitleKey: 'apps.step.before_add',
- defaultTitle: 'Before adding subscription',
- });
+ if (app.connectAndUseStep) {
+ html += `
+
+
${stepNum++}
+
+
${escapeHtml(t('apps.step.connect'))}
+
${getLocalizedText(app.connectAndUseStep.description)}
+
+
+ `;
+ }
- appendStep(app.addSubscriptionStep, {
- defaultTitleKey: 'apps.step.add',
- defaultTitle: 'Add subscription',
- });
+ html += renderConnectActionButton(app.id || '');
- appendStep(app.additionalAfterAddSubscriptionStep, {
- defaultTitleKey: 'apps.step.after_add',
- defaultTitle: 'If the subscription is not added',
- });
-
- appendStep(app.connectAndUseStep, {
- defaultTitleKey: 'apps.step.connect',
- defaultTitle: 'Connect & use',
- });
-
- parts.push(renderConnectActionButton(app.id || ''));
-
- return parts.join('');
+ return html;
}
- function updateInstructionConnectButtons(isEnabled = true) {
- const apps = getAppsForCurrentPlatform();
-
+ function updateInstructionConnectButtons(hasConnect = Boolean(getConnectLink())) {
document.querySelectorAll('.step-final-button').forEach(button => {
- if (!isEnabled) {
- button.disabled = true;
- return;
- }
-
- const appId = button.getAttribute('data-app-id') || null;
- const hasLink = Boolean(getConnectLink(appId, { apps }));
- button.disabled = !hasLink;
+ button.disabled = !hasConnect;
});
}
@@ -9302,7 +8951,7 @@
renderApps();
setActivePlatformButton();
- updateInstructionConnectButtons(hasAnyConnectLink());
+ updateInstructionConnectButtons(Boolean(getConnectLink()));
modal.classList.remove('hidden');
document.body.classList.add('modal-open');
@@ -9327,83 +8976,38 @@
document.body.classList.remove('modal-open');
}
- function normalizeLocalizedValue(value) {
- if (typeof value !== 'string') {
- return '';
- }
-
- const trimmed = value.trim();
- if (!trimmed || /^[-–—]+$/.test(trimmed)) {
- return '';
- }
-
- return trimmed;
- }
-
function getLocalizedText(textObj) {
if (!textObj) {
return '';
}
if (typeof textObj === 'string') {
- return normalizeLocalizedValue(textObj);
+ return textObj;
}
const telegramLang = tg.initDataUnsafe?.user?.language_code;
- const fallbackLanguages = getSupportedLanguages();
const preferenceOrder = [
preferredLanguage,
preferredLanguage?.split('-')[0],
userData?.user?.language,
telegramLang,
telegramLang?.split('-')[0],
- ...fallbackLanguages,
'en',
'ru'
].filter(Boolean).map(lang => lang.toLowerCase());
const seen = new Set();
for (const lang of preferenceOrder) {
- const normalizedLang = lang.toLowerCase();
- if (seen.has(normalizedLang)) {
+ if (seen.has(lang)) {
continue;
}
- seen.add(normalizedLang);
-
- const candidateKeys = [normalizedLang];
- if (lang !== normalizedLang) {
- candidateKeys.push(lang);
- }
-
- for (const key of candidateKeys) {
- if (!Object.prototype.hasOwnProperty.call(textObj, key)) {
- continue;
- }
- const value = normalizeLocalizedValue(textObj[key]);
- if (value) {
- return value;
- }
+ seen.add(lang);
+ if (textObj[lang]) {
+ return textObj[lang];
}
}
- const fallbackKeys = ['default', 'en', 'ru'];
- for (const key of fallbackKeys) {
- if (!Object.prototype.hasOwnProperty.call(textObj, key)) {
- continue;
- }
- const value = normalizeLocalizedValue(textObj[key]);
- if (value) {
- return value;
- }
- }
-
- for (const value of Object.values(textObj)) {
- const normalizedValue = normalizeLocalizedValue(value);
- if (normalizedValue) {
- return normalizedValue;
- }
- }
-
- return '';
+ const fallback = Object.values(textObj).find(value => typeof value === 'string' && value.trim().length);
+ return fallback || '';
}
function ensureArray(value) {
@@ -9456,186 +9060,6 @@
return fallback;
}
- function sanitizeAppsConfig(rawPlatforms) {
- const sanitized = {};
- if (!rawPlatforms || typeof rawPlatforms !== 'object') {
- return sanitized;
- }
-
- Object.entries(rawPlatforms).forEach(([platformKey, apps]) => {
- if (!Array.isArray(apps)) {
- sanitized[platformKey] = [];
- return;
- }
-
- const normalizedApps = apps
- .map(app => sanitizeAppDefinition(app))
- .filter(Boolean);
- sanitized[platformKey] = normalizedApps;
- });
-
- return sanitized;
- }
-
- function sanitizeAppDefinition(app) {
- if (!app || typeof app !== 'object') {
- return null;
- }
-
- const sanitized = {};
-
- const rawId = app.id ?? app.appId ?? app.slug ?? null;
- let normalizedId = typeof rawId === 'string' && rawId.trim().length
- ? rawId.trim()
- : null;
-
- let resolvedName = '';
- if (typeof app.name === 'string') {
- resolvedName = normalizeLocalizedValue(app.name) || app.name;
- } else if (app.name && typeof app.name === 'object') {
- const nameCandidates = [
- app.name.en,
- app.name.default,
- app.name.ru,
- ...Object.values(app.name)
- ];
- for (const candidate of nameCandidates) {
- const normalizedCandidate = normalizeLocalizedValue(candidate);
- if (normalizedCandidate) {
- resolvedName = normalizedCandidate;
- break;
- }
- }
- }
-
- if (!resolvedName && typeof rawId === 'string') {
- resolvedName = rawId.trim();
- }
-
- if (!normalizedId && resolvedName) {
- const slug = resolvedName
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, '-')
- .replace(/^-+|-+$/g, '');
- normalizedId = slug || null;
- }
-
- sanitized.id = normalizedId;
- sanitized.name = resolvedName || 'App';
-
- const rawScheme = app.urlScheme ?? app.url_scheme ?? '';
- sanitized.urlScheme = typeof rawScheme === 'string' && rawScheme.trim().length
- ? rawScheme.trim()
- : null;
-
- sanitized.isFeatured = coerceBoolean(app.isFeatured ?? app.is_featured, false);
- sanitized.isNeedBase64Encoding = coerceBoolean(
- app.isNeedBase64Encoding ?? app.is_need_base64_encoding,
- false
- );
-
- const installationStep = sanitizeStep(app.installationStep ?? app.installation_step);
- if (installationStep) {
- sanitized.installationStep = installationStep;
- }
-
- const beforeAddStep = sanitizeStep(
- app.additionalBeforeAddSubscriptionStep
- ?? app.additional_before_add_subscription_step
- );
- if (beforeAddStep) {
- sanitized.additionalBeforeAddSubscriptionStep = beforeAddStep;
- }
-
- const addStep = sanitizeStep(app.addSubscriptionStep ?? app.add_subscription_step);
- if (addStep) {
- sanitized.addSubscriptionStep = addStep;
- }
-
- const afterAddStep = sanitizeStep(
- app.additionalAfterAddSubscriptionStep
- ?? app.additional_after_add_subscription_step
- );
- if (afterAddStep) {
- sanitized.additionalAfterAddSubscriptionStep = afterAddStep;
- }
-
- const connectStep = sanitizeStep(app.connectAndUseStep ?? app.connect_and_use_step);
- if (connectStep) {
- sanitized.connectAndUseStep = connectStep;
- }
-
- return sanitized;
- }
-
- function sanitizeStep(step) {
- if (!step || typeof step !== 'object') {
- return null;
- }
-
- const sanitized = {};
-
- if (Object.prototype.hasOwnProperty.call(step, 'title')) {
- sanitized.title = step.title;
- }
-
- if (Object.prototype.hasOwnProperty.call(step, 'description')) {
- sanitized.description = step.description;
- }
-
- const rawButtons = ensureArray(step.buttons ?? step.buttonList ?? step.button_list);
- const buttons = rawButtons.map(sanitizeButton).filter(Boolean);
- if (buttons.length) {
- sanitized.buttons = buttons;
- }
-
- if (
- !Object.prototype.hasOwnProperty.call(sanitized, 'title')
- && !Object.prototype.hasOwnProperty.call(sanitized, 'description')
- && !sanitized.buttons
- ) {
- return null;
- }
-
- return sanitized;
- }
-
- function sanitizeButton(button) {
- if (!button || typeof button !== 'object') {
- return null;
- }
-
- const link = normalizeUrl(
- button.buttonLink
- ?? button.button_link
- ?? button.link
- ?? button.url
- ?? button.href
- );
-
- if (!link) {
- return null;
- }
-
- const rawText = button.buttonText
- ?? button.button_text
- ?? button.text
- ?? button.label
- ?? '';
-
- if (typeof rawText === 'string') {
- return {
- buttonLink: link,
- buttonText: normalizeLocalizedValue(rawText) || rawText,
- };
- }
-
- return {
- buttonLink: link,
- buttonText: rawText,
- };
- }
-
async function parseJsonSafe(response) {
try {
return await response.json();
@@ -17469,149 +16893,58 @@
return null;
}
- const happData = userData.happ || userData.happData || null;
-
- const candidates = [
- userData.happ_crypto_link,
- userData.subscriptionCryptoLink,
- userData.subscription_crypto_link,
- happData?.cryptoLink,
- happData?.crypto_link,
- happData?.link,
- happData?.url,
- ];
-
- for (const candidate of candidates) {
- const normalized = normalizeUrl(candidate);
- if (normalized) {
- return normalized;
- }
- }
-
- return null;
+ return (
+ userData.happ_crypto_link ||
+ userData.subscriptionCryptoLink ||
+ userData.subscription_crypto_link ||
+ null
+ );
}
- function encodeToBase64(value) {
- if (typeof value !== 'string') {
- return '';
- }
-
- try {
- return btoa(value);
- } catch (error) {
- try {
- return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
- } catch (nestedError) {
- console.warn('Failed to base64 encode value:', nestedError);
- return value;
- }
- }
- }
-
- function buildAppSchemeLink(app, subscriptionUrl) {
- if (!app || !subscriptionUrl) {
- return null;
- }
-
- const scheme = typeof app.urlScheme === 'string' ? app.urlScheme.trim() : '';
- if (!scheme) {
- return null;
- }
-
- let payload = subscriptionUrl;
- if (app.isNeedBase64Encoding) {
- const encoded = encodeToBase64(subscriptionUrl);
- payload = encoded || subscriptionUrl;
- }
-
- return `${scheme}${payload}`;
- }
-
- function getConnectLink(appId = null, options = {}) {
+ function getConnectLink() {
if (!userData) {
return null;
}
- const apps = Array.isArray(options.apps)
- ? options.apps
- : getAppsForCurrentPlatform();
- const normalizedAppId = typeof appId === 'string' && appId.trim().length
- ? appId.trim()
- : null;
- const requestedApp = normalizedAppId
- ? apps.find(app => (app.id || '').toString() === normalizedAppId)
- : null;
- const featuredApp = apps.find(app => coerceBoolean(app.isFeatured, false)) || apps[0] || null;
- const selectedApp = requestedApp || featuredApp || null;
-
- const subscriptionUrl = normalizeUrl(getCurrentSubscriptionUrl());
const happCryptoLink = getHappCryptoLink();
- const happData = userData?.happ || userData?.happData || null;
- const redirectLink = normalizeUrl(
- userData?.happ_cryptolink_redirect_link
- || userData?.happCryptolinkRedirectLink
- || happData?.redirectLink
- || happData?.redirect_link
- || happData?.redirectUrl
- || happData?.redirect_url
- || null
- );
- const happLink = normalizeUrl(
- userData?.happ_link
- || userData?.happLink
- || happData?.link
- || happData?.url
- || happData?.subscriptionLink
- || happData?.subscription_link
- || null
- );
-
- const schemeLink = selectedApp && subscriptionUrl
- ? buildAppSchemeLink(selectedApp, subscriptionUrl)
- : null;
-
- if (selectedApp && (selectedApp.id === 'happ' || selectedApp.id === 'happ-app')) {
- if (redirectLink) {
- return redirectLink;
+ const isPC = currentPlatform === 'pc' || (!['ios', 'android', 'tv'].includes(currentPlatform));
+
+ // For PC, prefer direct subscription URL if no Happ redirect link
+ if (isPC) {
+ if (userData.happ_cryptolink_redirect_link) {
+ return userData.happ_cryptolink_redirect_link;
}
if (happCryptoLink) {
return happCryptoLink;
}
- if (happLink) {
- return happLink;
- }
- return subscriptionUrl || null;
+ // Return plain subscription URL for PC
+ return getCurrentSubscriptionUrl();
}
-
- if (schemeLink) {
- return schemeLink;
- }
-
- if (redirectLink) {
- return redirectLink;
- }
-
+
+ // Original logic for mobile platforms
if (happCryptoLink) {
return happCryptoLink;
}
- if (selectedApp && selectedApp.id === 'happ' && happLink) {
- return happLink;
+ if (userData.happ_cryptolink_redirect_link) {
+ return userData.happ_cryptolink_redirect_link;
}
- return subscriptionUrl || null;
- }
-
- function hasAnyConnectLink(precomputedApps = null) {
- const apps = Array.isArray(precomputedApps)
- ? precomputedApps
- : getAppsForCurrentPlatform();
-
- if (Boolean(getConnectLink(null, { apps }))) {
- return true;
+ const subscriptionUrl = getCurrentSubscriptionUrl();
+ if (!subscriptionUrl) {
+ return null;
}
- return apps.some(app => Boolean(getConnectLink(app.id || null, { apps })));
+ const apps = getAppsForCurrentPlatform();
+ const featuredApp = apps.find(app => app.isFeatured) || apps[0];
+
+ if (featuredApp?.urlScheme) {
+ return `${featuredApp.urlScheme}${subscriptionUrl}`;
+ }
+ if (userData?.happ_link && featuredApp?.id === 'happ') {
+ return userData.happ_link;
+ }
+ return subscriptionUrl;
}
function openExternalLink(link, options = {}) {
@@ -17855,7 +17188,6 @@
const guideBtn = document.getElementById('openGuideBtn');
const connectLink = getConnectLink();
- const anyConnectLink = hasAnyConnectLink();
const showConnectButton = hasActiveSubscription();
if (guideBtn) {
@@ -17867,7 +17199,7 @@
closeInstallationModal();
}
- updateInstructionConnectButtons(showConnectButton && anyConnectLink);
+ updateInstructionConnectButtons(showConnectButton && Boolean(connectLink));
const subscriptionUrl = getCurrentSubscriptionUrl();
if (copyBtn) {