From fdcf668e06d36e1b60da69c006b87b8c9a5beee4 Mon Sep 17 00:00:00 2001 From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:59:19 +0530 Subject: [PATCH 1/2] fix: name of route in history --- frontend/src/components/layouts/AppLayout.tsx | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx index beced9ff3..801e43417 100644 --- a/frontend/src/components/layouts/AppLayout.tsx +++ b/frontend/src/components/layouts/AppLayout.tsx @@ -1,4 +1,5 @@ -import { Outlet } from "react-router-dom"; +import React from "react"; +import { Outlet, useMatches } from "react-router-dom"; import { AppSidebar } from "src/components/AppSidebar"; import { Banner } from "src/components/Banner"; @@ -22,6 +23,65 @@ function AppLayoutInner() { useRemoveSuccessfulChannelOrder(); useNotifyReceivedPayments(); + // Update document.title and the history entry when the route changes. + // This ensures the browser's history entries include a proper title + // (fixes: back gesture / long-press back showing empty/incorrect titles). + const matches = useMatches(); + React.useEffect(() => { + try { + // Attempt to derive a title from route handles (crumb) or matched route + // fallback to app name. Use a small typed helper instead of `any` to + // satisfy lint rules. + const getCrumbFromHandle = ( + handle: unknown + ): string | string[] | null => { + if (handle && typeof handle === "object") { + const h = handle as { crumb?: unknown }; + if (typeof h.crumb === "function") { + try { + return (h.crumb as () => string | string[])(); + } catch (err) { + return null; + } + } + } + return null; + }; + + const crumbTitle = + matches + .map((m) => getCrumbFromHandle(m.handle)) + .filter(Boolean) + .pop() || "Alby Hub"; + + const title = Array.isArray(crumbTitle) + ? crumbTitle.join(" - ") + : crumbTitle; + + // Set document title + document.title = title as string; + + // Replace current history state to include title in state (some browsers show + // history entry title from state). We keep the existing state but add _title. + try { + const state = + history.state && typeof history.state === "object" + ? { ...history.state } + : {}; + if (state && state._title !== title) { + state._title = title; + history.replaceState(state, title as string, window.location.href); + } + } catch (err) { + // ignore replaceState errors in weird environments + // eslint-disable-next-line no-console + console.debug("history.replaceState failed", err); + } + } catch (err) { + console.error("Failed to compute page title", err); + } + }, [matches]); + if (!info) { return null; } From 29c010d22163c8a9f3f8035c339c86bca914eba3 Mon Sep 17 00:00:00 2001 From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:48:33 +0530 Subject: [PATCH 2/2] fix: add titles to route handles and use them instead of bread crumbs --- frontend/src/components/layouts/AppLayout.tsx | 64 +------------- frontend/src/hooks/useDocumentTitle.ts | 49 +++++++++++ frontend/src/routes.tsx | 88 ++++++++++++------- 3 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 frontend/src/hooks/useDocumentTitle.ts diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx index 801e43417..a6b06cac1 100644 --- a/frontend/src/components/layouts/AppLayout.tsx +++ b/frontend/src/components/layouts/AppLayout.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { Outlet, useMatches } from "react-router-dom"; +import { Outlet } from "react-router-dom"; import { AppSidebar } from "src/components/AppSidebar"; import { Banner } from "src/components/Banner"; @@ -10,6 +9,7 @@ import { useCommandPaletteContext, } from "src/contexts/CommandPaletteContext"; import { useBanner } from "src/hooks/useBanner"; +import { useDocumentTitle } from "src/hooks/useDocumentTitle"; import { useInfo } from "src/hooks/useInfo"; import { useNotifyReceivedPayments } from "src/hooks/useNotifyReceivedPayments"; import { useRemoveSuccessfulChannelOrder } from "src/hooks/useRemoveSuccessfulChannelOrder"; @@ -22,65 +22,7 @@ function AppLayoutInner() { useRemoveSuccessfulChannelOrder(); useNotifyReceivedPayments(); - - // Update document.title and the history entry when the route changes. - // This ensures the browser's history entries include a proper title - // (fixes: back gesture / long-press back showing empty/incorrect titles). - const matches = useMatches(); - React.useEffect(() => { - try { - // Attempt to derive a title from route handles (crumb) or matched route - // fallback to app name. Use a small typed helper instead of `any` to - // satisfy lint rules. - const getCrumbFromHandle = ( - handle: unknown - ): string | string[] | null => { - if (handle && typeof handle === "object") { - const h = handle as { crumb?: unknown }; - if (typeof h.crumb === "function") { - try { - return (h.crumb as () => string | string[])(); - } catch (err) { - return null; - } - } - } - return null; - }; - - const crumbTitle = - matches - .map((m) => getCrumbFromHandle(m.handle)) - .filter(Boolean) - .pop() || "Alby Hub"; - - const title = Array.isArray(crumbTitle) - ? crumbTitle.join(" - ") - : crumbTitle; - - // Set document title - document.title = title as string; - - // Replace current history state to include title in state (some browsers show - // history entry title from state). We keep the existing state but add _title. - try { - const state = - history.state && typeof history.state === "object" - ? { ...history.state } - : {}; - if (state && state._title !== title) { - state._title = title; - history.replaceState(state, title as string, window.location.href); - } - } catch (err) { - // ignore replaceState errors in weird environments - // eslint-disable-next-line no-console - console.debug("history.replaceState failed", err); - } - } catch (err) { - console.error("Failed to compute page title", err); - } - }, [matches]); + useDocumentTitle(); if (!info) { return null; diff --git a/frontend/src/hooks/useDocumentTitle.ts b/frontend/src/hooks/useDocumentTitle.ts new file mode 100644 index 000000000..7349cd340 --- /dev/null +++ b/frontend/src/hooks/useDocumentTitle.ts @@ -0,0 +1,49 @@ +import React from "react"; +import { useMatches } from "react-router-dom"; + +/** + * Custom hook to manage document.title based on route handles. + * This ensures the browser's history entries include a proper title + * (fixes: back gesture / long-press back showing empty/incorrect titles). + * + * Looks for a `title` property in route handles (string or function), + * falling back to "Alby Hub" if not present. + */ +export function useDocumentTitle() { + const matches = useMatches(); + + React.useEffect(() => { + try { + // Extract title from route handles. Use a typed helper to get the title + // from the handle object (if present). + const getTitleFromHandle = (handle: unknown): string | null => { + if (handle && typeof handle === "object") { + const h = handle as { title?: unknown }; + if (typeof h.title === "string") { + return h.title; + } + if (typeof h.title === "function") { + try { + return (h.title as () => string)(); + } catch (err) { + return null; + } + } + } + return null; + }; + + // Find the last (most specific) route with a title, or default to "Alby Hub" + const routeTitle = + matches + .map((m) => getTitleFromHandle(m.handle)) + .filter(Boolean) + .pop() || "Alby Hub"; + + // Set document title + document.title = routeTitle; + } catch (err) { + console.error("Failed to compute page title", err); + } + }, [matches]); +} diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 5ed1436a1..57e2faef9 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -102,7 +102,7 @@ const routes = [ { path: "home", element: , - handle: { crumb: () => "Dashboard" }, + handle: { crumb: () => "Dashboard", title: "Dashboard" }, children: [ { index: true, @@ -113,7 +113,7 @@ const routes = [ { path: "wallet", element: , - handle: { crumb: () => "Wallet" }, + handle: { crumb: () => "Wallet", title: "Wallet" }, children: [ { index: true, @@ -121,7 +121,7 @@ const routes = [ }, { path: "swap", - handle: { crumb: () => "Swap" }, + handle: { crumb: () => "Swap", title: "Swap" }, children: [ { index: true, @@ -143,24 +143,30 @@ const routes = [ }, { path: "receive", - handle: { crumb: () => "Receive" }, + handle: { crumb: () => "Receive", title: "Receive" }, children: [ { index: true, element: , }, { - handle: { crumb: () => "Receive On-chain" }, + handle: { + crumb: () => "Receive On-chain", + title: "Receive On-chain", + }, path: "onchain", element: , }, { - handle: { crumb: () => "Invoice" }, + handle: { crumb: () => "Invoice", title: "Invoice" }, path: "invoice", element: , }, { - handle: { crumb: () => "BOLT-12 Offer" }, + handle: { + crumb: () => "BOLT-12 Offer", + title: "BOLT-12 Offer", + }, path: "offer", element: , }, @@ -168,7 +174,7 @@ const routes = [ }, { path: "send", - handle: { crumb: () => "Send" }, + handle: { crumb: () => "Send", title: "Send" }, children: [ { index: true, @@ -203,24 +209,27 @@ const routes = [ { path: "sign-message", element: , - handle: { crumb: () => "Sign Message" }, + handle: { crumb: () => "Sign Message", title: "Sign Message" }, }, { path: "node-alias", element: , - handle: { crumb: () => "Node Alias" }, + handle: { crumb: () => "Node Alias", title: "Node Alias" }, }, { path: "withdraw", element: , - handle: { crumb: () => "Withdraw On-Chain Balance" }, + handle: { + crumb: () => "Withdraw On-Chain Balance", + title: "Withdraw On-Chain Balance", + }, }, ], }, { path: "settings", element: , - handle: { crumb: () => "Settings" }, + handle: { crumb: () => "Settings", title: "Settings" }, children: [ { path: "", @@ -233,22 +242,25 @@ const routes = [ { path: "about", element: , - handle: { crumb: () => "About" }, + handle: { crumb: () => "About", title: "About" }, }, { path: "auto-unlock", element: , - handle: { crumb: () => "Auto Unlock" }, + handle: { crumb: () => "Auto Unlock", title: "Auto Unlock" }, }, { path: "change-unlock-password", element: , - handle: { crumb: () => "Unlock Password" }, + handle: { + crumb: () => "Unlock Password", + title: "Change Unlock Password", + }, }, { path: "backup", element: , - handle: { crumb: () => "Backup" }, + handle: { crumb: () => "Backup", title: "Backup" }, }, { path: "node-migrate", @@ -273,7 +285,7 @@ const routes = [ { path: "apps", element: , - handle: { crumb: () => "Connections" }, + handle: { crumb: () => "Connections", title: "Connections" }, children: [ { index: true, @@ -286,7 +298,7 @@ const routes = [ { path: "new", element: , - handle: { crumb: () => "New App" }, + handle: { crumb: () => "New App", title: "New Connection" }, }, { path: "cleanup", @@ -297,7 +309,7 @@ const routes = [ { path: "sub-wallets", element: , - handle: { crumb: () => "Sub-wallets" }, + handle: { crumb: () => "Sub-wallets", title: "Sub-wallets" }, children: [ { @@ -317,7 +329,7 @@ const routes = [ { path: "internal-apps", element: , - handle: { crumb: () => "Connections" }, + handle: { crumb: () => "Connections", title: "Internal Apps" }, children: [ { path: "buzzpay", @@ -356,7 +368,7 @@ const routes = [ { path: "appstore", element: , - handle: { crumb: () => "App Store" }, + handle: { crumb: () => "App Store", title: "App Store" }, children: [ { path: ":appStoreId", @@ -367,7 +379,7 @@ const routes = [ { path: "channels", element: , - handle: { crumb: () => "Node" }, + handle: { crumb: () => "Node", title: "Channels" }, children: [ { index: true, @@ -375,7 +387,10 @@ const routes = [ }, { path: "first", - handle: { crumb: () => "Your First Channel" }, + handle: { + crumb: () => "Your First Channel", + title: "Your First Channel", + }, children: [ { index: true, @@ -393,7 +408,7 @@ const routes = [ }, { path: "auto", - handle: { crumb: () => "New Channel" }, + handle: { crumb: () => "New Channel", title: "New Channel" }, children: [ { index: true, @@ -412,34 +427,43 @@ const routes = [ { path: "outgoing", element: , - handle: { crumb: () => "Open Channel with On-Chain" }, + handle: { + crumb: () => "Open Channel with On-Chain", + title: "Open Channel with On-Chain", + }, }, { path: "incoming", element: , - handle: { crumb: () => "Open Channel with Lightning" }, + handle: { + crumb: () => "Open Channel with Lightning", + title: "Open Channel with Lightning", + }, }, { path: "order", element: , - handle: { crumb: () => "Current Order" }, + handle: { crumb: () => "Current Order", title: "Current Order" }, }, { path: "onchain/buy-bitcoin", element: , - handle: { crumb: () => "Buy Bitcoin" }, + handle: { crumb: () => "Buy Bitcoin", title: "Buy Bitcoin" }, }, { path: "onchain/deposit-bitcoin", element: , - handle: { crumb: () => "Deposit Bitcoin" }, + handle: { + crumb: () => "Deposit Bitcoin", + title: "Deposit Bitcoin", + }, }, ], }, { path: "peers", element: , - handle: { crumb: () => "Peers" }, + handle: { crumb: () => "Peers", title: "Peers" }, children: [ { index: true, @@ -448,7 +472,7 @@ const routes = [ { path: "new", element: , - handle: { crumb: () => "Connect Peer" }, + handle: { crumb: () => "Connect Peer", title: "Connect Peer" }, }, ], }, @@ -459,7 +483,7 @@ const routes = [ { path: "review-earn", element: , - handle: { crumb: () => "Review & Earn" }, + handle: { crumb: () => "Review & Earn", title: "Review & Earn" }, }, ], },