From c91c9e0441232109398798d4c42711b1c897b5ed Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 3 Jun 2026 16:09:49 +0300 Subject: [PATCH] fix(connection): Happ TV connect renders through the active block style (all 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TV quick-connect was rendered as standalone cards inserted between two sliced block lists, which broke the panel's chosen style (e.g. split the timeline rail) and dropped a step. Now it renders THROUGH the active renderer: - Blocks gain an optional customNode; every renderer (cards/timeline/accordion/ minimal) drops it into the block body, so it inherits that exact style. - InstallationGuide renders the FULL block list in the active style and injects the TV connect widget as customNode on the "add subscription" step (no more slicing / dropped steps) — only for the Happ Android TV app. - TvQuickConnect is now content-only (no one-off cards/titles); the block step is the wrapper. Buttons already use the shared blockButtonClass. --- .../connection/InstallationGuide.tsx | 52 +++--- src/components/connection/TvQuickConnect.tsx | 149 ++++++------------ .../connection/blocks/AccordionBlock.tsx | 7 +- .../connection/blocks/CardsBlock.tsx | 7 +- .../connection/blocks/MinimalBlock.tsx | 7 +- .../connection/blocks/TimelineBlock.tsx | 7 +- src/components/connection/blocks/index.ts | 2 +- src/components/connection/blocks/types.ts | 11 +- 8 files changed, 104 insertions(+), 138 deletions(-) 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} - /> - -
-
-
+
+ {/* 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} + /> +
- {/* QR Scanner */} -
-
-
+ {/* QR scan */} +
+

+ {t('subscription.tvQuickConnect.scanTitle')} +

+ {!scanning && ( +
-
-

- {t('subscription.tvQuickConnect.scanTitle')} -

-

- {t('subscription.tvQuickConnect.scanDescription')} -

- - {!scanning && ( - - )} -
-
- {scanning && ( - - )} -
-
+ {t('subscription.tvQuickConnect.scanBtn')} + + )} +
+
+ {scanning && ( + + )}
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;