mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(connection): Happ TV quick-connect is Happ-only + matches config-block styles
The TV quick-connect block (Подключить TV / Сканировать QR) is a Happ-only
feature (check.happ.su/sendtv), but it rendered for ANY app on a TV platform and
used one-off button styles (solid btn-primary + grey btn-secondary) that clashed
with the outlined-accent buttons that come from the subscription-page config.
- Show TvQuickConnect only for the Happ app (detected by its happ:// deep-link
scheme, name as fallback).
- Extract the config-block button style into a shared blockButtonClass and use it
for the TV buttons too, so they adapt to exactly the same visual language as the
panel-config blocks (no divergent one-off styling).
- Align the code input with the Happ Android TV API doc (5-digit numeric):
placeholder 12345 + inputMode numeric. The send call already matches the spec
(POST check.happ.su/sendtv/{code}, body {"data": base64(subscriptionUrl)}).
This commit is contained in:
@@ -34,6 +34,14 @@ const RENDERERS: Record<string, React.ComponentType<BlockRendererProps>> = {
|
|||||||
minimal: MinimalBlock,
|
minimal: MinimalBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** TV quick-connect is a Happ-only feature (check.happ.su/sendtv) — show it only
|
||||||
|
* for the Happ app, detected by its happ:// deep-link scheme (name as fallback). */
|
||||||
|
function isHappApp(app: RemnawaveAppClient | null): boolean {
|
||||||
|
if (!app) return false;
|
||||||
|
if ((app.deepLink ?? '').toLowerCase().startsWith('happ://')) return true;
|
||||||
|
return app.name.toLowerCase().includes('happ');
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
onOpenDeepLink: (url: string) => void;
|
onOpenDeepLink: (url: string) => void;
|
||||||
@@ -312,8 +320,9 @@ export default function InstallationGuide({
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Blocks — for TV: first block, Quick Connect, last block */}
|
{/* Blocks — for the Happ TV app: first block, Quick Connect, last block.
|
||||||
{selectedApp && isTvPlatform && appConfig.subscriptionUrl ? (
|
Other apps (or non-TV) render their blocks normally. */}
|
||||||
|
{selectedApp && isTvPlatform && isHappApp(selectedApp) && appConfig.subscriptionUrl ? (
|
||||||
<>
|
<>
|
||||||
{selectedApp.blocks.length > 0 && (
|
{selectedApp.blocks.length > 0 && (
|
||||||
<Renderer
|
<Renderer
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isQrScannerSupported,
|
isQrScannerSupported,
|
||||||
retrieveLaunchParams,
|
retrieveLaunchParams,
|
||||||
} from '@telegram-apps/sdk-react';
|
} from '@telegram-apps/sdk-react';
|
||||||
|
import { blockButtonClass } from './blocks/buttonStyles';
|
||||||
|
|
||||||
const TG_MOBILE_PLATFORMS = new Set(['ios', 'android', 'android_x', 'ios_x']);
|
const TG_MOBILE_PLATFORMS = new Set(['ios', 'android', 'android_x', 'ios_x']);
|
||||||
|
|
||||||
@@ -198,6 +199,10 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
|
|||||||
? '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/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';
|
: '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';
|
||||||
|
|
||||||
|
// Full-width buttons in the same outlined-accent language as the config blocks
|
||||||
|
// (so the Happ TV block adapts to the subscription-page styles, not a one-off).
|
||||||
|
const actionBtnClass = `${blockButtonClass('light', isLight)} flex w-full items-center justify-center`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Code input */}
|
{/* Code input */}
|
||||||
@@ -232,15 +237,15 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
|
|||||||
maxLength={5}
|
maxLength={5}
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))}
|
onChange={(e) => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))}
|
||||||
placeholder="A1B2C"
|
placeholder="12345"
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
inputMode="text"
|
inputMode="numeric"
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => sendToTV(code)}
|
onClick={() => sendToTV(code)}
|
||||||
disabled={sending || code.length !== 5}
|
disabled={sending || code.length !== 5}
|
||||||
className="btn-primary w-full justify-center py-3 disabled:opacity-50"
|
className={`${actionBtnClass} disabled:opacity-50`}
|
||||||
>
|
>
|
||||||
{sending ? (
|
{sending ? (
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||||
@@ -285,7 +290,7 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{!scanning && (
|
{!scanning && (
|
||||||
<button onClick={startScan} className="btn-secondary mt-3 w-full justify-center py-3">
|
<button onClick={startScan} className={`${actionBtnClass} mt-3`}>
|
||||||
<svg
|
<svg
|
||||||
className="mr-2 h-5 w-5"
|
className="mr-2 h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -310,7 +315,7 @@ export default function TvQuickConnect({ subscriptionUrl, isLight }: Props) {
|
|||||||
<div className={scanning ? 'mt-3 space-y-2' : 'hidden'}>
|
<div className={scanning ? 'mt-3 space-y-2' : 'hidden'}>
|
||||||
<div id="tv-qr-reader" className="overflow-hidden rounded-xl" />
|
<div id="tv-qr-reader" className="overflow-hidden rounded-xl" />
|
||||||
{scanning && (
|
{scanning && (
|
||||||
<button onClick={stopScan} className="btn-secondary w-full justify-center py-2.5">
|
<button onClick={stopScan} className={actionBtnClass}>
|
||||||
{t('subscription.tvQuickConnect.stopScan')}
|
{t('subscription.tvQuickConnect.stopScan')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { CheckIcon, CopyIcon } from '@/components/icons';
|
import { CheckIcon, CopyIcon } from '@/components/icons';
|
||||||
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
||||||
import { copyToClipboard } from '@/utils/clipboard';
|
import { copyToClipboard } from '@/utils/clipboard';
|
||||||
|
import { blockButtonClass } from './buttonStyles';
|
||||||
|
|
||||||
// eslint-disable-next-line no-script-url
|
// eslint-disable-next-line no-script-url
|
||||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'];
|
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'];
|
||||||
@@ -57,14 +58,7 @@ export function BlockButtons({
|
|||||||
|
|
||||||
if (!buttons || buttons.length === 0) return null;
|
if (!buttons || buttons.length === 0) return null;
|
||||||
|
|
||||||
const baseClass =
|
const baseClass = blockButtonClass(variant, isLight);
|
||||||
variant === 'light'
|
|
||||||
? isLight
|
|
||||||
? 'rounded-xl border border-accent-500/50 px-4 py-2 text-sm font-medium text-accent-600 shadow-sm transition-all hover:bg-accent-500/10'
|
|
||||||
: 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10'
|
|
||||||
: isLight
|
|
||||||
? 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/30'
|
|
||||||
: 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
|||||||
16
src/components/connection/blocks/buttonStyles.ts
Normal file
16
src/components/connection/blocks/buttonStyles.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Shared button styling for connection blocks. The config-driven blocks
|
||||||
|
* (BlockButtons) and the Happ TV quick-connect both render through this so the
|
||||||
|
* latter adapts to exactly the same visual language as the styles coming from
|
||||||
|
* the subscription-page config — no divergent one-off button styles.
|
||||||
|
*/
|
||||||
|
export function blockButtonClass(variant: 'light' | 'subtle', isLight?: boolean): string {
|
||||||
|
if (variant === 'light') {
|
||||||
|
return isLight
|
||||||
|
? 'rounded-xl border border-accent-500/50 px-4 py-2 text-sm font-medium text-accent-600 shadow-sm transition-all hover:bg-accent-500/10'
|
||||||
|
: 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10';
|
||||||
|
}
|
||||||
|
return isLight
|
||||||
|
? 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/30'
|
||||||
|
: 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user