diff --git a/miniapp/index.html b/miniapp/index.html index 322f5f83..3688cbb6 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -254,6 +254,18 @@ 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; @@ -3853,6 +3865,13 @@ 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)); @@ -3865,6 +3884,19 @@ 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; } @@ -5494,7 +5526,15 @@ // All original constants and functions const LANG_STORAGE_KEY = 'remnawave-miniapp-language'; - const SUPPORTED_LANGUAGES = ['en', 'ru']; + 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 translations = { en: { @@ -5739,8 +5779,11 @@ '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.', @@ -5862,6 +5905,11 @@ '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', @@ -6134,8 +6182,11 @@ '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': 'Ответ будет добавлен позже.', @@ -6257,6 +6308,11 @@ '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': 'Недоступно', @@ -6308,6 +6364,103 @@ 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 HAPP_APP_IDS = new Set(['happ', 'happ-app']); + + 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: '📜', @@ -6332,26 +6485,44 @@ 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]) => { - if (typeof value !== 'string') { + const normalizedValue = normalizeLocalizedValue(value); + if (!normalizedValue) { return; } - const trimmed = value.trim(); - if (!trimmed) { - return; - } - normalized[lang.toLowerCase()] = trimmed; + normalized[lang.toLowerCase()] = normalizedValue; }); + 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) { @@ -6388,14 +6559,50 @@ }); } - applyKey('app.name', rawServiceName); - applyKey('app.title', rawServiceName); - applyKey('app.subtitle', rawServiceDescription); + 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); + } + } } let userData = null; let appsConfig = {}; let currentPlatform = 'android'; + let detectedDevicePlatform = 'android'; + let cachedHappCryptolinkRedirectTemplate; let configPurchaseUrl = null; let subscriptionPurchaseUrl = null; let preferredLanguage = 'en'; @@ -7019,11 +7226,11 @@ return null; } const normalized = String(lang).toLowerCase(); - if (SUPPORTED_LANGUAGES.includes(normalized)) { + if (supportedLanguages.has(normalized)) { return normalized; } const short = normalized.split('-')[0]; - if (SUPPORTED_LANGUAGES.includes(short)) { + if (supportedLanguages.has(short)) { return short; } return null; @@ -7238,6 +7445,7 @@ } } + renderLanguageSelectOptions(); applyTranslations(); updateConnectButtonLabel(); @@ -7484,10 +7692,55 @@ } userData = payload; + cachedHappCryptolinkRedirectTemplate = undefined; userData.subscriptionUrl = userData.subscription_url || null; userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; userData.referral = userData.referral || null; + const happData = payload?.happ; + if (happData && typeof happData === 'object') { + userData.happ = { ...happData }; + + const happCryptoLinkCandidate = happData.cryptoLink + ?? happData.crypto_link + ?? null; + if (happCryptoLinkCandidate) { + if (!userData.happ_crypto_link) { + userData.happ_crypto_link = happCryptoLinkCandidate; + } + if (!userData.happCryptoLink) { + userData.happCryptoLink = happCryptoLinkCandidate; + } + } + + const happRedirectCandidate = happData.cryptolinkRedirectLink + ?? happData.cryptolink_redirect_link + ?? happData.redirectLink + ?? happData.redirect_link + ?? null; + if (happRedirectCandidate) { + if (!userData.happ_cryptolink_redirect_link) { + userData.happ_cryptolink_redirect_link = happRedirectCandidate; + } + if (!userData.happCryptolinkRedirectLink) { + userData.happCryptolinkRedirectLink = happRedirectCandidate; + } + } + + const happLinkCandidate = happData.link + ?? happData.appLink + ?? happData.url + ?? null; + if (happLinkCandidate) { + if (!userData.happ_link) { + userData.happ_link = happLinkCandidate; + } + if (!userData.happLink) { + userData.happLink = happLinkCandidate; + } + } + } + if (hasPaidSubscription()) { subscriptionAutopayState.loading = true; subscriptionAutopayState.saving = false; @@ -7632,9 +7885,13 @@ } const data = await response.json(); - appsConfig = data?.platforms || {}; + appsConfig = sanitizeAppsConfig(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 @@ -7648,6 +7905,45 @@ 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 = {}; @@ -8764,12 +9060,14 @@ function detectPlatform() { const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes('iphone') || userAgent.includes('ipad')) { - currentPlatform = 'ios'; + detectedDevicePlatform = 'ios'; } else if (userAgent.includes('android')) { - currentPlatform = 'android'; + detectedDevicePlatform = 'android'; } else { - currentPlatform = 'pc'; + detectedDevicePlatform = 'pc'; } + + currentPlatform = detectedDevicePlatform; } function setActivePlatformButton() { @@ -8778,20 +9076,25 @@ }); } - function getPlatformKey(platform) { - const mapping = { - ios: 'ios', - android: 'android', - pc: 'windows', - tv: 'androidTV', - mac: 'macos' - }; - return mapping[platform] || platform; + function getPlatformLabel(platformKey) { + const labelKey = PLATFORM_LABEL_KEYS[platformKey]; + if (!labelKey) { + return null; + } + const label = t(labelKey); + return label === labelKey ? platformKey : label; } function getAppsForCurrentPlatform() { - const platformKey = getPlatformKey(currentPlatform); - return appsConfig?.[platformKey] || []; + 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; } function resolveConnectButtonLabel() { @@ -8803,7 +9106,10 @@ function renderConnectActionButton(appId = '') { const label = escapeHtml(resolveConnectButtonLabel()); - const appAttribute = appId ? ` data-app-id="${escapeHtml(appId)}"` : ''; + const normalizedId = typeof appId === 'string' ? appId.trim() : ''; + const appAttribute = normalizedId + ? ` data-app-id="${escapeHtml(normalizedId)}"` + : ''; return `