From ed197143d612a124817ccd2b1bb0d99ec274ad06 Mon Sep 17 00:00:00 2001 From: Vlad Chikalkin Date: Sat, 2 Aug 2025 15:42:06 +0300 Subject: [PATCH] Feature/tma back button (#70) * feat(layout): integrate TelegramProvider and BackButton into main layout for enhanced navigation * refactor(layout): remove BackButton from main layout and update navigation imports * use ui back button for non tma mode --- apps/web/app/(main)/layout.tsx | 15 ++++++-- .../components/navigation/header/index.tsx | 14 ++++++-- apps/web/hooks/telegram/index.ts | 2 ++ apps/web/hooks/telegram/use-back-button.ts | 36 +++++++++++++++++++ apps/web/hooks/telegram/use-viewport.ts | 12 +++++++ apps/web/providers/empty.tsx | 5 +++ apps/web/providers/telegram.tsx | 5 ++- apps/web/utils/telegram/init.ts | 14 ++++++-- 8 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 apps/web/hooks/telegram/use-back-button.ts create mode 100644 apps/web/hooks/telegram/use-viewport.ts create mode 100644 apps/web/providers/empty.tsx diff --git a/apps/web/app/(main)/layout.tsx b/apps/web/app/(main)/layout.tsx index 16e17c0..700358a 100644 --- a/apps/web/app/(main)/layout.tsx +++ b/apps/web/app/(main)/layout.tsx @@ -1,13 +1,22 @@ +'use client'; + import { UpdateProfile } from '@/components/auth'; import { BottomNav } from '@/components/navigation'; +import { EmptyProvider } from '@/providers/empty'; +import { TelegramProvider } from '@/providers/telegram'; +import { isTMA } from '@telegram-apps/sdk-react'; import { type PropsWithChildren } from 'react'; -export default async function Layout({ children }: Readonly) { +export default function Layout({ children }: Readonly) { + const isTG = isTMA('simple'); + + const Provider = isTG ? TelegramProvider : EmptyProvider; + return ( - <> +
{children}
- +
); } diff --git a/apps/web/components/navigation/header/index.tsx b/apps/web/components/navigation/header/index.tsx index 59f83d0..d7b6731 100644 --- a/apps/web/components/navigation/header/index.tsx +++ b/apps/web/components/navigation/header/index.tsx @@ -1,13 +1,21 @@ 'use client'; - import { BackButton } from './back-button'; +import { cn } from '@repo/ui/lib/utils'; +import { isTMA } from '@telegram-apps/sdk-react'; type Props = { title: string | undefined }; export function PageHeader(props: Readonly) { + const isTG = isTMA('simple'); + return ( -
- +
+ {!isTG && } {props.title}
); diff --git a/apps/web/hooks/telegram/index.ts b/apps/web/hooks/telegram/index.ts index 019eb25..2f1cbc1 100644 --- a/apps/web/hooks/telegram/index.ts +++ b/apps/web/hooks/telegram/index.ts @@ -1,2 +1,4 @@ +export * from './use-back-button'; export * from './use-client-once'; export * from './use-did-mount'; +export * from './use-viewport'; diff --git a/apps/web/hooks/telegram/use-back-button.ts b/apps/web/hooks/telegram/use-back-button.ts new file mode 100644 index 0000000..58238db --- /dev/null +++ b/apps/web/hooks/telegram/use-back-button.ts @@ -0,0 +1,36 @@ +'use client'; + +import { backButton } from '@telegram-apps/sdk-react'; +import { usePathname, useRouter } from 'next/navigation'; +import { useCallback, useEffect } from 'react'; + +export function useBackButton() { + const { back } = useRouter(); + const pathname = usePathname(); + + const onBackClick = useCallback(() => { + if (pathname !== '/') { + back(); + } + }, [pathname, back]); + + useEffect(() => { + const off = backButton.onClick(onBackClick); + + return off; + }, [onBackClick]); + + useEffect(() => { + if (backButton.isMounted()) { + if (isRootLevelPage(pathname)) { + backButton.hide(); + } else { + backButton.show(); + } + } + }, [pathname]); +} + +function isRootLevelPage(path: string) { + return path.split('/').filter(Boolean).length === 1; +} diff --git a/apps/web/hooks/telegram/use-viewport.ts b/apps/web/hooks/telegram/use-viewport.ts new file mode 100644 index 0000000..65b2b5e --- /dev/null +++ b/apps/web/hooks/telegram/use-viewport.ts @@ -0,0 +1,12 @@ +import { useClientOnce } from './use-client-once'; +import { swipeBehavior } from '@telegram-apps/sdk-react'; + +export function useViewport() { + useClientOnce(() => { + if (swipeBehavior.disableVertical.isAvailable()) { + swipeBehavior.disableVertical(); + } + }); + + return null; +} diff --git a/apps/web/providers/empty.tsx b/apps/web/providers/empty.tsx new file mode 100644 index 0000000..077528c --- /dev/null +++ b/apps/web/providers/empty.tsx @@ -0,0 +1,5 @@ +import { type PropsWithChildren } from 'react'; + +export function EmptyProvider({ children }: Readonly) { + return <>{children}; +} diff --git a/apps/web/providers/telegram.tsx b/apps/web/providers/telegram.tsx index 2be20d6..26aefba 100644 --- a/apps/web/providers/telegram.tsx +++ b/apps/web/providers/telegram.tsx @@ -1,7 +1,7 @@ /* eslint-disable sonarjs/function-return-type */ 'use client'; -import { useClientOnce, useDidMount } from '@/hooks/telegram'; +import { useBackButton, useClientOnce, useDidMount, useViewport } from '@/hooks/telegram'; import { setLocale } from '@/utils/i18n/locale'; import { init } from '@/utils/telegram/init'; import { initData, useSignal } from '@telegram-apps/sdk-react'; @@ -28,6 +28,9 @@ function RootInner({ children }: PropsWithChildren) { init(debug); }); + useViewport(); + useBackButton(); + const initDataUser = useSignal(initData.user); // Set the user locale. diff --git a/apps/web/utils/telegram/init.ts b/apps/web/utils/telegram/init.ts index 00e4a81..44d4b71 100644 --- a/apps/web/utils/telegram/init.ts +++ b/apps/web/utils/telegram/init.ts @@ -1,17 +1,19 @@ /* eslint-disable no-console */ /* eslint-disable promise/prefer-await-to-then */ + import { backButton, $debug as debugSDK, initData, init as initSDK, miniApp, + swipeBehavior, } from '@telegram-apps/sdk-react'; /** * Initializes the application and configures its dependencies. */ -export function init(debug: boolean): void { +export async function init(debug: boolean): Promise { // Set @telegram-apps/sdk-react debug mode. if (debug) debugSDK.set(debug); @@ -20,10 +22,18 @@ export function init(debug: boolean): void { initSDK(); // Mount all components used in the project. - if (backButton.isSupported()) backButton.mount(); + if (backButton.isSupported()) { + backButton.mount(); + } + miniApp.mount(); + initData.restore(); + if (swipeBehavior.mount.isAvailable()) { + swipeBehavior.mount(); + } + // Add Eruda if needed. if (debug) import('eruda').then((library) => library.default.init()).catch(console.error); }