diff --git a/src/locales/en.json b/src/locales/en.json index b4eabe4..a12b571 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -346,7 +346,8 @@ "changeApp": "Change app", "selectPlatform": "Select platform", "app": "app", - "apps": "apps" + "apps": "apps", + "tutorial": "Tutorial" }, "myDevices": "My Devices", "noDevices": "No connected devices", diff --git a/src/locales/fa.json b/src/locales/fa.json index 4eb2d39..b4fa28b 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -298,7 +298,8 @@ "changeApp": "تغییر برنامه", "selectPlatform": "انتخاب پلتفرم", "app": "برنامه", - "apps": "برنامه" + "apps": "برنامه", + "tutorial": "آموزش" }, "additionalOptions": { "title": "گزینه‌های اضافی", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4a48ae4..97007ec 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -368,7 +368,8 @@ "changeApp": "Сменить приложение", "selectPlatform": "Выберите платформу", "app": "приложение", - "apps": "приложений" + "apps": "приложений", + "tutorial": "Инструкция" }, "myDevices": "Мои устройства", "noDevices": "Нет подключенных устройств", diff --git a/src/locales/zh.json b/src/locales/zh.json index adbae2a..fbd16d3 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -298,7 +298,8 @@ "changeApp": "更换应用", "selectPlatform": "选择平台", "app": "个应用", - "apps": "个应用" + "apps": "个应用", + "tutorial": "教程" }, "additionalOptions": { "title": "附加选项", diff --git a/src/pages/AdminApps.tsx b/src/pages/AdminApps.tsx index e965c1a..4efb2fb 100644 --- a/src/pages/AdminApps.tsx +++ b/src/pages/AdminApps.tsx @@ -116,13 +116,13 @@ function getLocalizedText(text: LocalizedText | undefined, lang: string): string } function renderSvgIcon(svgLibrary: Record, key?: string, color?: string) { - if (!key || !svgLibrary[key]) return null; + if (!key || !svgLibrary[key]?.svgString) return null; const sanitized = DOMPurify.sanitize(svgLibrary[key].svgString, { USE_PROFILES: { svg: true, svgFilters: true }, }); return (
@@ -230,7 +230,17 @@ function AppCard({ className="flex w-full items-center gap-4 p-4 text-left" > {/* Icon from first block */} -
+
{renderSvgIcon(svgLibrary, firstBlock?.svgIconKey, firstBlock?.svgIconColor) || ( )} @@ -287,10 +297,18 @@ function BlockCard({ const title = getLocalizedText(block.title, lang); const description = getLocalizedText(block.description, lang); + const iconColor = block.svgIconColor || '#8B5CF6'; + return (
-
+
{renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || ( {index + 1} )} diff --git a/src/pages/Connection.tsx b/src/pages/Connection.tsx index e23991e..6fabed7 100644 --- a/src/pages/Connection.tsx +++ b/src/pages/Connection.tsx @@ -230,12 +230,16 @@ export default function Connection() { }, [navigate]); const handleBack = useCallback(() => { - if (selectedPlatform) { - setSelectedPlatform(null); + if (showAppSelector) { + if (selectedPlatform) { + setSelectedPlatform(null); + } else { + setShowAppSelector(false); + } } else { - setShowAppSelector(false); + navigate(-1); } - }, [selectedPlatform]); + }, [showAppSelector, selectedPlatform, navigate]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -268,6 +272,20 @@ export default function Connection() { return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || ''; }; + // Get translation from RemnaWave baseTranslations with i18n fallback + const getBaseTranslation = ( + key: string, + i18nKey: string, + interpolation?: Record, + ): string => { + const bt = appConfig?.baseTranslations; + if (bt && key in bt) { + const text = getLocalizedText(bt[key as keyof typeof bt] as LocalizedText); + if (text) return text; + } + return t(i18nKey, interpolation); + }; + const availablePlatforms = useMemo(() => { if (!appConfig?.platforms) return []; const available = platformOrder.filter( @@ -339,11 +357,6 @@ export default function Connection() { openDeepLink(app.deepLink); }; - const handleConnectRemnawave = (app: RemnawaveAppClient) => { - if (!app.deepLink || !isValidDeepLink(app.deepLink)) return; - openDeepLink(app.deepLink); - }; - // Resolve button links for step buttons const resolveButtonLink = (link: string): string => { return resolveUrl(link); @@ -372,7 +385,7 @@ export default function Connection() {

{t('common.error')}

); @@ -387,7 +400,7 @@ export default function Connection() {

{t('subscription.connection.noSubscription')}

); @@ -416,269 +429,106 @@ export default function Connection() { return fallback[key] || key; }; - // App selector for RemnaWave - if (showAppSelector) { - if (!selectedPlatform) { - // Platform selection - const platformIcons: Record = { - ios: ( - - - - ), - android: ( - - - - ), - windows: ( - - - - ), - macos: ( - - - - ), - linux: ( - - - - ), - androidTV: ( - - - - ), - appleTV: ( - - - - ), - }; + // Current platform apps for chips + const currentPlatformKey = activePlatformKey || availablePlatforms[0]; + const currentPlatformData = currentPlatformKey + ? appConfig.platforms[currentPlatformKey] + : undefined; + const currentPlatformApps = currentPlatformData ? getRemnawaveApps(currentPlatformData) : []; - return ( -
-
- {!isTelegramWebApp && ( - - )} -

- {t('subscription.connection.selectPlatform')} -

-
-
- {availablePlatforms.map((platform) => { - const platformData = appConfig.platforms[platform]; - if (!platformData) return null; - const appCount = getPlatformAppsCount(platformData); - if (!appCount) return null; - const isCurrentPlatform = platform === detectedPlatform; - - return ( - - ); - })} -
-
- ); - } - - // App selection for chosen platform (RemnaWave) - const platformData = appConfig.platforms[selectedPlatform]; - const apps = platformData ? getRemnawaveApps(platformData) : []; - const isCurrentPlatform = selectedPlatform === detectedPlatform; - - return ( -
-
- {!isTelegramWebApp && ( - - )} -
-

- {getPlatformDisplayName(selectedPlatform)} -

- {isCurrentPlatform && ( - - {t('subscription.connection.yourDevice')} - - )} -
-
-
-
- {apps.map((app, idx) => { - const isSelected = selectedRemnawaveApp?.name === app.name; - return ( - - ); - })} -
-
-
- ); - } - - // Main RemnaWave view — blocks rendering + // Main RemnaWave view — inline app chips + blocks return (
- {/* Header */} -
-
- {!isTelegramWebApp && ( - - )} -

- {t('subscription.connection.title')} -

- {/* Platform dropdown */} - {availablePlatforms.length > 1 && ( - - )} -
- - {/* App selector button */} - + {/* Header: title + platform dropdown */} +
+ {!isTelegramWebApp && ( + + )} +

+ {t('subscription.connection.title')} +

+ {/* Platform dropdown */} + {availablePlatforms.length > 1 && ( + + )}
+ {/* App chips row */} + {currentPlatformApps.length > 0 && ( +
+ {currentPlatformApps.map((app, idx) => { + const isSelected = currentApp?.name === app.name; + return ( + + ); + })} +
+ )} + + {/* Tutorial button */} + {appConfig.baseSettings?.isShowTutorialButton && appConfig.baseSettings?.tutorialUrl && ( + + + + + {getBaseTranslation('tutorial', 'subscription.connection.tutorial')} + + )} + {/* Blocks */} {currentApp && (
@@ -686,6 +536,22 @@ export default function Connection() { const svgHtml = getSvgHtml(block.svgIconKey); const iconColor = block.svgIconColor || '#8B5CF6'; + // Fallback block title from baseTranslations by block index + const blockTitleFallbackKeys = ['installApp', 'addSubscription', 'connectAndUse']; + const blockTitleFallbackI18n = [ + 'subscription.connection.installApp', + 'subscription.connection.addSubscription', + 'subscription.connection.connectVpn', + ]; + const blockTitle = + getLocalizedText(block.title) || + (blockIdx < blockTitleFallbackKeys.length + ? getBaseTranslation( + blockTitleFallbackKeys[blockIdx], + blockTitleFallbackI18n[blockIdx], + ) + : ''); + return (
@@ -705,7 +571,7 @@ export default function Connection() {
)}
-

{getLocalizedText(block.title)}

+

{blockTitle}

{getLocalizedText(block.description)}

@@ -713,24 +579,36 @@ export default function Connection() { {block.buttons && block.buttons.length > 0 && (
{block.buttons.map((btn, btnIdx) => { - const btnText = getLocalizedText(btn.text); + const btnText = + getLocalizedText(btn.text) || + getBaseTranslation('openApp', 'subscription.connection.openLink'); if (btn.type === 'subscriptionLink') { + // Use app deepLink first, fallback to resolved button URL + const deepLink = currentApp?.deepLink || btn.resolvedUrl; + if (!deepLink || !isValidDeepLink(deepLink)) return null; + const subBtnSvg = getSvgHtml(btn.svgIconKey); return ( ); } if (btn.type === 'copyButton') { + if (appConfig?.hideLink) return null; return ( ); }