diff --git a/src/components/connection/InstallationGuide.tsx b/src/components/connection/InstallationGuide.tsx
index 49dff9b..a6038a5 100644
--- a/src/components/connection/InstallationGuide.tsx
+++ b/src/components/connection/InstallationGuide.tsx
@@ -10,7 +10,7 @@ import type {
} from '@/types';
import { useTheme } from '@/hooks/useTheme';
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
-import type { BlockRendererProps } from './blocks';
+import type { BlockRendererProps, RenderBlock } from './blocks';
import TvQuickConnect from './TvQuickConnect';
import { BackIcon, BookOpenIcon, ChevronIcon } from '@/components/icons';
@@ -194,6 +194,21 @@ export default function InstallationGuide({
const blockType = appConfig.uiConfig?.installationGuidesBlockType || 'cards';
const Renderer = RENDERERS[blockType] || CardsBlock;
+ // For the Happ Android TV app, inject the TV connect widget into a step as
+ // customNode so it renders THROUGH the active block style (cards/timeline/
+ // accordion/minimal) instead of as separate clashing cards that break it.
+ const showTvConnect = Boolean(
+ selectedApp && isAndroidTvLayout && isHappApp(selectedApp) && appConfig.subscriptionUrl,
+ );
+ let renderBlocks: RenderBlock[] = selectedApp?.blocks ?? [];
+ if (selectedApp && showTvConnect && appConfig.subscriptionUrl) {
+ // install → add-subscription → connect: attach to the add step (index 1);
+ // fall back to the last block for shorter configs.
+ const idx = selectedApp.blocks.length >= 3 ? 1 : Math.max(0, selectedApp.blocks.length - 1);
+ const widget = ;
+ renderBlocks = selectedApp.blocks.map((b, i) => (i === idx ? { ...b, customNode: widget } : b));
+ }
+
return (
{/* Header + platform dropdown */}
@@ -321,42 +336,19 @@ export default function InstallationGuide({
)}
- {/* Blocks — for the Happ TV app: first block, Quick Connect, last block.
- Other apps (or non-TV) render their blocks normally. */}
- {selectedApp && isAndroidTvLayout && isHappApp(selectedApp) && appConfig.subscriptionUrl ? (
- <>
- {selectedApp.blocks.length > 0 && (
-
- )}
-
- {selectedApp.blocks.length > 1 && (
-
- )}
- >
- ) : selectedApp ? (
+ {/* Blocks rendered in the panel's active style. For the Happ Android TV
+ app the TV connect widget is injected into a step (customNode), so it
+ adapts to that style instead of breaking it. */}
+ {selectedApp && (
- ) : null}
+ )}
);
}
diff --git a/src/components/connection/TvQuickConnect.tsx b/src/components/connection/TvQuickConnect.tsx
index 43fc103..8400d40 100644
--- a/src/components/connection/TvQuickConnect.tsx
+++ b/src/components/connection/TvQuickConnect.tsx
@@ -191,10 +191,6 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
}
}, [tgNative, sendToTV, showToast, onScanDecoded, t]);
- const cardClass = isLight
- ? 'rounded-2xl border border-dark-700/60 bg-white/80 shadow-sm p-4 sm:p-5'
- : 'rounded-2xl border border-dark-700/50 bg-dark-800/50 p-4 sm:p-5';
-
const inputClass = isLight
? 'w-full rounded-xl border border-dark-700/60 bg-white px-4 py-3 text-center text-2xl font-bold tracking-[0.3em] uppercase text-dark-100 outline-none focus:border-accent-500 focus:ring-1 focus:ring-accent-500'
: 'w-full rounded-xl border border-dark-700 bg-dark-900/50 px-4 py-3 text-center text-2xl font-bold tracking-[0.3em] uppercase text-dark-100 outline-none focus:border-accent-500 focus:ring-1 focus:ring-accent-500';
@@ -204,66 +200,46 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
const actionBtnClass = `${blockButtonClass('light', isLight)} flex w-full items-center justify-center`;
return (
-
- {/* Code input */}
-
-
-
-
-
- {t('subscription.tvQuickConnect.title')}
-
-
- {t('subscription.tvQuickConnect.description')}
-
-
-
-
setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))}
- placeholder="12345"
- autoComplete="one-time-code"
- inputMode="numeric"
- className={inputClass}
- />
-
sendToTV(code)}
- disabled={sending || code.length !== 5}
- className={`${actionBtnClass} disabled:opacity-50`}
- >
- {sending ? (
-
- ) : (
- t('subscription.tvQuickConnect.sendBtn')
- )}
-
-
-
-
+
+ {/* Code import — the block step (timeline/cards/accordion/minimal) is the
+ wrapper; here we only render the interactive controls so the widget
+ inherits whatever style the panel config produced. */}
+
+
+ {t('subscription.tvQuickConnect.title')}
+
+
setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))}
+ placeholder="12345"
+ autoComplete="one-time-code"
+ inputMode="numeric"
+ className={inputClass}
+ />
+
sendToTV(code)}
+ disabled={sending || code.length !== 5}
+ className={`${actionBtnClass} disabled:opacity-50`}
+ >
+ {sending ? (
+
+ ) : (
+ t('subscription.tvQuickConnect.sendBtn')
+ )}
+
- {/* QR Scanner */}
-
-
-
+ {/* QR scan */}
+
+
+ {t('subscription.tvQuickConnect.scanTitle')}
+
+ {!scanning && (
+
-
-
-
- {t('subscription.tvQuickConnect.scanTitle')}
-
-
- {t('subscription.tvQuickConnect.scanDescription')}
-
-
- {!scanning && (
-
-
-
-
-
- {t('subscription.tvQuickConnect.scanBtn')}
-
- )}
-
-
- {scanning && (
-
- {t('subscription.tvQuickConnect.stopScan')}
-
- )}
-
-
+ {t('subscription.tvQuickConnect.scanBtn')}
+
+ )}
+
+
+ {scanning && (
+
+ {t('subscription.tvQuickConnect.stopScan')}
+
+ )}
diff --git a/src/components/connection/blocks/AccordionBlock.tsx b/src/components/connection/blocks/AccordionBlock.tsx
index d07bcad..e4da74d 100644
--- a/src/components/connection/blocks/AccordionBlock.tsx
+++ b/src/components/connection/blocks/AccordionBlock.tsx
@@ -15,7 +15,11 @@ export function AccordionBlock({
const [openIndex, setOpenIndex] = useState
(0);
const visibleBlocks = blocks.filter(
- (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length,
+ (b) =>
+ getLocalizedText(b.title) ||
+ getLocalizedText(b.description) ||
+ b.buttons?.length ||
+ b.customNode,
);
if (!visibleBlocks.length) return null;
@@ -68,6 +72,7 @@ export function AccordionBlock({
{getLocalizedText(block.description)}
{renderBlockButtons(block.buttons, 'light')}
+ {block.customNode}
diff --git a/src/components/connection/blocks/CardsBlock.tsx b/src/components/connection/blocks/CardsBlock.tsx
index 5ef48c5..a589fd8 100644
--- a/src/components/connection/blocks/CardsBlock.tsx
+++ b/src/components/connection/blocks/CardsBlock.tsx
@@ -11,7 +11,11 @@ export function CardsBlock({
renderBlockButtons,
}: BlockRendererProps) {
const visibleBlocks = blocks.filter(
- (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length,
+ (b) =>
+ getLocalizedText(b.title) ||
+ getLocalizedText(b.description) ||
+ b.buttons?.length ||
+ b.customNode,
);
if (!visibleBlocks.length) return null;
@@ -43,6 +47,7 @@ export function CardsBlock({
{getLocalizedText(block.description)}
{renderBlockButtons(block.buttons, 'light')}
+ {block.customNode}
diff --git a/src/components/connection/blocks/MinimalBlock.tsx b/src/components/connection/blocks/MinimalBlock.tsx
index e5abb99..4185d0b 100644
--- a/src/components/connection/blocks/MinimalBlock.tsx
+++ b/src/components/connection/blocks/MinimalBlock.tsx
@@ -11,7 +11,11 @@ export function MinimalBlock({
renderBlockButtons,
}: BlockRendererProps) {
const visibleBlocks = blocks.filter(
- (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length,
+ (b) =>
+ getLocalizedText(b.title) ||
+ getLocalizedText(b.description) ||
+ b.buttons?.length ||
+ b.customNode,
);
if (!visibleBlocks.length) return null;
@@ -44,6 +48,7 @@ export function MinimalBlock({
{getLocalizedText(block.description)}
{renderBlockButtons(block.buttons, 'subtle')}
+ {block.customNode}
);
})}
diff --git a/src/components/connection/blocks/TimelineBlock.tsx b/src/components/connection/blocks/TimelineBlock.tsx
index 400469f..e7d23ad 100644
--- a/src/components/connection/blocks/TimelineBlock.tsx
+++ b/src/components/connection/blocks/TimelineBlock.tsx
@@ -11,7 +11,11 @@ export function TimelineBlock({
renderBlockButtons,
}: BlockRendererProps) {
const visibleBlocks = blocks.filter(
- (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length,
+ (b) =>
+ getLocalizedText(b.title) ||
+ getLocalizedText(b.description) ||
+ b.buttons?.length ||
+ b.customNode,
);
if (!visibleBlocks.length) return null;
@@ -43,6 +47,7 @@ export function TimelineBlock({
{getLocalizedText(block.description)}
{renderBlockButtons(block.buttons, 'light')}
+ {block.customNode}
);
diff --git a/src/components/connection/blocks/index.ts b/src/components/connection/blocks/index.ts
index 633e0e9..b96bbab 100644
--- a/src/components/connection/blocks/index.ts
+++ b/src/components/connection/blocks/index.ts
@@ -3,4 +3,4 @@ export { TimelineBlock } from './TimelineBlock';
export { AccordionBlock } from './AccordionBlock';
export { MinimalBlock } from './MinimalBlock';
export { BlockButtons } from './BlockButtons';
-export type { BlockRendererProps } from './types';
+export type { BlockRendererProps, RenderBlock } from './types';
diff --git a/src/components/connection/blocks/types.ts b/src/components/connection/blocks/types.ts
index 61580b2..1047d0e 100644
--- a/src/components/connection/blocks/types.ts
+++ b/src/components/connection/blocks/types.ts
@@ -1,7 +1,16 @@
+import type { ReactNode } from 'react';
import type { RemnawaveBlockClient, RemnawaveButtonClient, LocalizedText } from '@/types';
+/**
+ * A block to render. Beyond the panel's data (title/description/buttons), it may
+ * carry `customNode` — extra interactive content (e.g. the Happ TV connect
+ * widget) that every renderer drops into the block body, so it inherits the
+ * active style (cards/timeline/accordion/minimal) instead of clashing with it.
+ */
+export type RenderBlock = RemnawaveBlockClient & { customNode?: ReactNode };
+
export interface BlockRendererProps {
- blocks: RemnawaveBlockClient[];
+ blocks: RenderBlock[];
isMobile: boolean;
isLight: boolean;
getLocalizedText: (text: LocalizedText | undefined) => string;