taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b885a68037c35040c3c409201cdbb6815c97a7bf
parent 4a5e6849db14e2b772c97820655c85c91aee4ae5
Author: Florian Dold <dold@taler.net>
Date:   Mon, 24 Aug 2026 02:28:48 +0200

web-util: parse hash routes and query parameters together

Diffstat:
Apackages/web-util/src/context/navigation.test.ts | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/context/navigation.ts | 40++++++++++++++++++++++++++++++++--------
2 files changed, 81 insertions(+), 8 deletions(-)

diff --git a/packages/web-util/src/context/navigation.test.ts b/packages/web-util/src/context/navigation.test.ts @@ -0,0 +1,49 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +import assert from "node:assert"; +import { test } from "node:test"; +import { parseHashLocation } from "./navigation.js"; +import { findMatch, urlPattern } from "../utils/route.js"; + +test("bank transfer route round-trips its path and hash query", () => { + const transfer = urlPattern<{ + account?: string; + amount?: string; + subject?: string; + }>( + /^\/wire-transfer\/(?<account>[a-zA-Z0-9._~-]+)$/, + ({ account, amount, subject }) => { + const query = new URLSearchParams(); + if (amount) query.set("amount", amount); + if (subject) query.set("subject", subject); + return `#/wire-transfer/${encodeURIComponent(account ?? "")}?${query}`; + }, + ); + const href = transfer.url({ + account: "alice.test~", + amount: "EUR:12", + subject: "rent August", + }); + const parsed = parseHashLocation(href, "?outer=one&outer=two"); + const matched = findMatch( + { transfer }, + ["transfer"], + parsed.path, + parsed.params, + ); + + assert.equal(matched.name, "transfer"); + assert.deepEqual(matched.values, { account: "alice.test~" }); + assert.deepEqual(matched.params, { + outer: ["one", "two"], + amount: ["EUR:12"], + subject: ["rent August"], + }); +}); diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts @@ -51,15 +51,27 @@ export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>( return findMatch(pagesMap, pageList, path, params); } -function getPathAndParamsFromWindow(): { +export function parseHashLocation( + hash: string, + search: string, +): { path: string; params: Record<string, string[]>; } { - const path = - typeof window !== "undefined" ? window.location.hash.substring(1) : "/"; + const rawHash = hash.startsWith("#") ? hash.substring(1) : hash; + const queryIndex = rawHash.indexOf("?"); + const path = queryIndex < 0 ? rawHash : rawHash.substring(0, queryIndex); const params: Record<string, string[]> = {}; - if (typeof window !== "undefined") { - for (const [key, value] of new URLSearchParams(window.location.search)) { + for (const [key, value] of new URLSearchParams(search)) { + if (!params[key]) { + params[key] = []; + } + params[key].push(value); + } + if (queryIndex >= 0) { + for (const [key, value] of new URLSearchParams( + rawHash.substring(queryIndex + 1), + )) { if (!params[key]) { params[key] = []; } @@ -69,6 +81,16 @@ function getPathAndParamsFromWindow(): { return { path, params }; } +function getPathAndParamsFromWindow(): { + path: string; + params: Record<string, string[]>; +} { + if (typeof window === "undefined") { + return parseHashLocation("/", ""); + } + return parseHashLocation(window.location.hash, window.location.search); +} + const { path: initialPath, params: initialParams } = getPathAndParamsFromWindow(); @@ -93,9 +115,9 @@ export const BrowserHashNavigationProvider = ({ ); } function navigateTo(path: string): void { - const { params } = getPathAndParamsFromWindow(); - setState({ path, params }); - window.location.href = path; + const target = new URL(path, window.location.href); + setState(parseHashLocation(target.hash, target.search)); + window.location.href = target.href; } useEffect(() => { @@ -103,8 +125,10 @@ export const BrowserHashNavigationProvider = ({ setState(getPathAndParamsFromWindow()); } window.addEventListener(PopStateEventType, eventListener); + window.addEventListener("hashchange", eventListener); return () => { window.removeEventListener(PopStateEventType, eventListener); + window.removeEventListener("hashchange", eventListener); }; }, []); return h(Context.Provider, {