feat: connect RemnaWave baseTranslations and fix SVG icons

- Wire baseTranslations from RemnaWave config to connection page UI
  (copyLink, openApp, installApp, addSubscription, connectAndUse,
  tutorial, close) with locale fallback: current lang → en → ru → first
- Add tutorial button when baseSettings.isShowTutorialButton is enabled
- Fix SVG icon rendering on admin apps settings page with proper scaling
- Add colored circle backgrounds for block and app icons in admin panel
- Add tutorial translation key to all locale files
This commit is contained in:
c0mrade
2026-02-05 09:44:42 +03:00
parent 79afe3a733
commit a50dea9a3d
6 changed files with 189 additions and 284 deletions

View File

@@ -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",

View File

@@ -298,7 +298,8 @@
"changeApp": "تغییر برنامه",
"selectPlatform": "انتخاب پلتفرم",
"app": "برنامه",
"apps": "برنامه"
"apps": "برنامه",
"tutorial": "آموزش"
},
"additionalOptions": {
"title": "گزینه‌های اضافی",

View File

@@ -368,7 +368,8 @@
"changeApp": "Сменить приложение",
"selectPlatform": "Выберите платформу",
"app": "приложение",
"apps": "приложений"
"apps": "приложений",
"tutorial": "Инструкция"
},
"myDevices": "Мои устройства",
"noDevices": "Нет подключенных устройств",

View File

@@ -298,7 +298,8 @@
"changeApp": "更换应用",
"selectPlatform": "选择平台",
"app": "个应用",
"apps": "个应用"
"apps": "个应用",
"tutorial": "教程"
},
"additionalOptions": {
"title": "附加选项",

View File

@@ -116,13 +116,13 @@ function getLocalizedText(text: LocalizedText | undefined, lang: string): string
}
function renderSvgIcon(svgLibrary: Record<string, RemnawaveSvgItem>, 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 (
<div
className="h-6 w-6 flex-shrink-0"
className="h-6 w-6 flex-shrink-0 [&>svg]:h-full [&>svg]:w-full"
style={{ color: color || 'currentColor' }}
dangerouslySetInnerHTML={{ __html: sanitized }}
/>
@@ -230,7 +230,17 @@ function AppCard({
className="flex w-full items-center gap-4 p-4 text-left"
>
{/* Icon from first block */}
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-dark-700/80 text-dark-300">
<div
className="flex h-10 w-10 items-center justify-center rounded-full"
style={
firstBlock?.svgIconColor
? {
backgroundColor: firstBlock.svgIconColor + '20',
color: firstBlock.svgIconColor,
}
: undefined
}
>
{renderSvgIcon(svgLibrary, firstBlock?.svgIconKey, firstBlock?.svgIconColor) || (
<AppsIcon />
)}
@@ -287,10 +297,18 @@ function BlockCard({
const title = getLocalizedText(block.title, lang);
const description = getLocalizedText(block.description, lang);
const iconColor = block.svgIconColor || '#8B5CF6';
return (
<div className="rounded-lg bg-dark-800/50 p-3">
<div className="flex items-start gap-3">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-dark-700">
<div
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full"
style={{
backgroundColor: iconColor + '20',
color: iconColor,
}}
>
{renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || (
<span className="text-xs font-bold text-dark-400">{index + 1}</span>
)}

View File

@@ -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, string>,
): 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() {
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
<p className="mb-4 text-lg text-dark-300">{t('common.error')}</p>
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
{t('common.close')}
{getBaseTranslation('close', 'common.close')}
</button>
</div>
);
@@ -387,7 +400,7 @@ export default function Connection() {
</h3>
<p className="mb-4 text-dark-400">{t('subscription.connection.noSubscription')}</p>
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
{t('common.close')}
{getBaseTranslation('close', 'common.close')}
</button>
</div>
);
@@ -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<string, React.ReactNode> = {
ios: (
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
),
android: (
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85-.29-.15-.65-.06-.83.22l-1.88 3.24c-1.39-.59-2.94-.92-4.58-.92s-3.19.33-4.58.92L5.5 5.67c-.16-.28-.54-.37-.83-.22-.31.16-.43.54-.26.85l1.84 3.18C3.38 11.11 1.5 14.12 1.5 17.5h21c0-3.38-1.88-6.39-4.9-8.02zM7 15.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25zm10 0c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z" />
</svg>
),
windows: (
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 12V6.75l6-1.32v6.48L3 12zm17-9v8.75l-10 .15V5.21L20 3zM3 13l6 .09v6.81l-6-1.15V13zm17 .25V22l-10-1.91V13.1l10 .15z" />
</svg>
),
macos: (
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
),
linux: (
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489.117.779.456 1.462 1.047 1.993.545.487 1.183.805 1.894.983.718.178 1.463.201 2.215.201.752 0 1.498-.023 2.215-.201.711-.178 1.349-.496 1.894-.983.591-.531.93-1.214 1.047-1.993.123-.805-.009-1.657-.287-2.489-.589-1.77-1.831-3.47-2.716-4.521-.75-1.067-.974-1.928-1.05-3.02-.065-1.491 1.056-5.965-3.17-6.298-.165-.013-.325-.021-.48-.021zm-.126 1.09c.99.063 1.783.569 2.172 1.443.376.844.413 1.903.199 2.999-.215 1.096-.687 2.23-1.357 3.18-.669.95-1.405 1.705-2.017 2.379-.611.674-1.074 1.252-1.316 1.857-.242.605-.262 1.233-.006 1.873.256.64.755 1.198 1.36 1.547.606.35 1.247.536 1.88.536.634 0 1.275-.186 1.88-.536.606-.349 1.105-.907 1.36-1.547.256-.64.236-1.268-.006-1.873-.242-.605-.705-1.183-1.316-1.857-.612-.674-1.348-1.429-2.017-2.379-.67-.95-1.142-2.084-1.357-3.18-.214-1.096-.177-2.155.199-2.999.389-.874 1.182-1.38 2.172-1.443z" />
</svg>
),
androidTV: (
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z" />
</svg>
),
appleTV: (
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z" />
</svg>
),
};
// Current platform apps for chips
const currentPlatformKey = activePlatformKey || availablePlatforms[0];
const currentPlatformData = currentPlatformKey
? appConfig.platforms[currentPlatformKey]
: undefined;
const currentPlatformApps = currentPlatformData ? getRemnawaveApps(currentPlatformData) : [];
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
{!isTelegramWebApp && (
<button
onClick={handleBack}
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
>
<BackIcon />
</button>
)}
<h2 className="text-lg font-bold text-dark-100">
{t('subscription.connection.selectPlatform')}
</h2>
</div>
<div className="space-y-2">
{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 (
<button
key={platform}
onClick={() => setSelectedPlatform(platform)}
className={`flex w-full items-center gap-4 rounded-xl p-4 transition-all active:scale-[0.98] ${
isCurrentPlatform
? 'bg-accent-500/10 ring-1 ring-accent-500/30'
: 'bg-dark-800/50 hover:bg-dark-800'
}`}
>
<div
className={`flex h-12 w-12 items-center justify-center rounded-xl ${
isCurrentPlatform
? 'bg-accent-500/20 text-accent-400'
: 'bg-dark-700 text-dark-300'
}`}
>
{platformIcons[platform] || <span className="text-xl">📱</span>}
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span
className={`font-medium ${isCurrentPlatform ? 'text-accent-400' : 'text-dark-100'}`}
>
{getPlatformDisplayName(platform)}
</span>
{isCurrentPlatform && (
<span className="rounded-full bg-accent-500/10 px-2 py-0.5 text-xs text-accent-500">
{t('subscription.connection.yourDevice')}
</span>
)}
</div>
<span className="text-sm text-dark-400">
{appCount}{' '}
{appCount === 1
? t('subscription.connection.app')
: t('subscription.connection.apps')}
</span>
</div>
<ChevronIcon />
</button>
);
})}
</div>
</div>
);
}
// App selection for chosen platform (RemnaWave)
const platformData = appConfig.platforms[selectedPlatform];
const apps = platformData ? getRemnawaveApps(platformData) : [];
const isCurrentPlatform = selectedPlatform === detectedPlatform;
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
{!isTelegramWebApp && (
<button
onClick={handleBack}
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
>
<BackIcon />
</button>
)}
<div className="flex-1">
<h2 className="text-lg font-bold text-dark-100">
{getPlatformDisplayName(selectedPlatform)}
</h2>
{isCurrentPlatform && (
<span className="text-xs text-accent-500">
{t('subscription.connection.yourDevice')}
</span>
)}
</div>
</div>
<div ref={scrollContainerRef}>
<div className="grid grid-cols-3 gap-3">
{apps.map((app, idx) => {
const isSelected = selectedRemnawaveApp?.name === app.name;
return (
<button
key={app.name + idx}
onClick={() => {
setSelectedRemnawaveApp(app);
setActivePlatformKey(selectedPlatform);
setShowAppSelector(false);
setSelectedPlatform(null);
}}
className={`relative flex flex-col items-center rounded-xl p-4 transition-all active:scale-95 ${
isSelected
? 'bg-accent-500/15 ring-2 ring-accent-500/50'
: 'bg-dark-800/50 hover:bg-dark-800'
}`}
>
{app.featured && (
<div className="absolute -right-1.5 -top-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-accent-500 shadow-lg">
<svg
className="h-3.5 w-3.5 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</div>
)}
<div
className={`mb-3 flex h-14 w-14 items-center justify-center rounded-2xl transition-colors ${
isSelected
? 'bg-accent-500/20 text-accent-400'
: 'bg-dark-700/80 text-dark-300'
}`}
>
{getAppIcon(app.name)}
</div>
<span
className={`line-clamp-2 text-center text-sm font-medium leading-tight ${
isSelected ? 'text-accent-400' : 'text-dark-200'
}`}
>
{app.name}
</span>
{isSelected && (
<div className="absolute bottom-2 left-1/2 h-2 w-2 -translate-x-1/2 rounded-full bg-accent-500" />
)}
</button>
);
})}
</div>
</div>
</div>
);
}
// Main RemnaWave view — blocks rendering
// Main RemnaWave view — inline app chips + blocks
return (
<div className="space-y-6">
{/* Header */}
<div>
<div className="mb-3 flex items-center gap-3">
{!isTelegramWebApp && (
<button
onClick={handleGoBack}
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
>
<BackIcon />
</button>
)}
<h2 className="flex-1 text-lg font-bold text-dark-100">
{t('subscription.connection.title')}
</h2>
{/* Platform dropdown */}
{availablePlatforms.length > 1 && (
<select
value={activePlatformKey || ''}
onChange={(e) => {
const newPlatform = e.target.value;
setActivePlatformKey(newPlatform);
const platformData = appConfig.platforms[newPlatform];
if (platformData) {
const apps = getRemnawaveApps(platformData);
const app = apps.find((a) => a.featured) || apps[0];
if (app) setSelectedRemnawaveApp(app);
}
}}
className="rounded-lg border border-dark-600 bg-dark-800 px-3 py-1.5 text-sm text-dark-200 outline-none"
>
{availablePlatforms.map((p) => (
<option key={p} value={p}>
{getPlatformDisplayName(p)}
</option>
))}
</select>
)}
</div>
{/* App selector button */}
<button
onClick={() => setShowAppSelector(true)}
className="flex w-full items-center gap-3 rounded-xl bg-dark-800/50 p-3 transition-colors hover:bg-dark-800"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/10 text-accent-400">
{currentApp && getAppIcon(currentApp.name)}
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className="font-medium text-dark-100">{currentApp?.name}</span>
{currentApp?.featured && (
<span className="rounded-full bg-accent-500/10 px-2 py-0.5 text-xs text-accent-500">
featured
</span>
)}
</div>
<div className="text-sm text-dark-400">{t('subscription.connection.changeApp')}</div>
</div>
<ChevronIcon />
</button>
{/* Header: title + platform dropdown */}
<div className="flex items-center gap-3">
{!isTelegramWebApp && (
<button
onClick={handleGoBack}
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
>
<BackIcon />
</button>
)}
<h2 className="flex-1 text-lg font-bold text-dark-100">
{t('subscription.connection.title')}
</h2>
{/* Platform dropdown */}
{availablePlatforms.length > 1 && (
<select
value={currentPlatformKey || ''}
onChange={(e) => {
const newPlatform = e.target.value;
setActivePlatformKey(newPlatform);
const platformData = appConfig.platforms[newPlatform];
if (platformData) {
const apps = getRemnawaveApps(platformData);
const app = apps.find((a) => a.featured) || apps[0];
if (app) setSelectedRemnawaveApp(app);
}
}}
className="rounded-lg border border-dark-600 bg-dark-800 px-3 py-1.5 text-sm text-dark-200 outline-none"
>
{availablePlatforms.map((p) => (
<option key={p} value={p}>
{getPlatformDisplayName(p)}
</option>
))}
</select>
)}
</div>
{/* App chips row */}
{currentPlatformApps.length > 0 && (
<div className="-mx-1 flex flex-wrap gap-2 px-1">
{currentPlatformApps.map((app, idx) => {
const isSelected = currentApp?.name === app.name;
return (
<button
key={app.name + idx}
onClick={() => setSelectedRemnawaveApp(app)}
className={`inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-sm font-medium transition-all active:scale-95 ${
isSelected
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/40'
: 'bg-dark-800/60 text-dark-300 hover:bg-dark-800'
}`}
>
{app.featured && (
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
)}
{app.name}
</button>
);
})}
</div>
)}
{/* Tutorial button */}
{appConfig.baseSettings?.isShowTutorialButton && appConfig.baseSettings?.tutorialUrl && (
<a
href={appConfig.baseSettings.tutorialUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 rounded-xl border border-dark-600 px-4 py-2.5 text-sm font-medium text-dark-200 transition-all hover:bg-dark-800"
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
</a>
)}
{/* Blocks */}
{currentApp && (
<div className="space-y-4">
@@ -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 (
<div key={blockIdx} className="rounded-xl border border-dark-700 p-4">
<div className="flex items-start gap-4">
@@ -705,7 +571,7 @@ export default function Connection() {
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="font-bold text-dark-100">{getLocalizedText(block.title)}</h3>
<h3 className="font-bold text-dark-100">{blockTitle}</h3>
<p className="mt-1 text-sm text-dark-400">
{getLocalizedText(block.description)}
</p>
@@ -713,24 +579,36 @@ export default function Connection() {
{block.buttons && block.buttons.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{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 (
<button
key={btnIdx}
onClick={() => {
if (currentApp) handleConnectRemnawave(currentApp);
}}
onClick={() => openDeepLink(deepLink)}
className="btn-primary flex items-center gap-2 px-4 py-2 text-sm font-semibold"
>
<LinkIcon />
{subBtnSvg ? (
<div
className="h-5 w-5 [&>svg]:h-full [&>svg]:w-full"
dangerouslySetInnerHTML={{ __html: subBtnSvg }}
/>
) : (
<LinkIcon />
)}
{btnText}
</button>
);
}
if (btn.type === 'copyButton') {
if (appConfig?.hideLink) return null;
return (
<button
key={btnIdx}
@@ -742,7 +620,12 @@ export default function Connection() {
}`}
>
{copied ? <CheckIcon /> : <CopyIcon />}
{copied ? t('subscription.connection.copied') : btnText}
{copied
? t('subscription.connection.copied')
: getBaseTranslation(
'copyLink',
'subscription.connection.copyLink',
)}
</button>
);
}