taler-typescript-core

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

commit 8e4bb79c96f4077b60dadd7f5dcb92b48f57d4a9
parent ecd2aee1818eb76240608ed3d92f64f8bb5598ec
Author: Florian Dold <dold@taler.net>
Date:   Tue,  3 Nov 2026 23:18:03 +0100

merchant web UI: use signals for settings editor and deletion state

Diffstat:
Mpackages/taler-merchant-webui/src/screens/BusinessSettingsScreen.state.test.tsx | 32++++++++++++++++++++++++++++++++
Mpackages/taler-merchant-webui/src/screens/BusinessSettingsScreen.tsx | 49++++++++++++++++++++++---------------------------
2 files changed, 54 insertions(+), 27 deletions(-)

diff --git a/packages/taler-merchant-webui/src/screens/BusinessSettingsScreen.state.test.tsx b/packages/taler-merchant-webui/src/screens/BusinessSettingsScreen.state.test.tsx @@ -171,3 +171,35 @@ test("password changes share the settings save lock and release it after a chall await cleanup(root); } }); + +test("settings refresh preserves open drafts while closed sections use the latest values", async () => { + const root = mount(); + try { + await act(() => + render( + <BusinessSettingsScreen + settings={{ name: "Old", email: "old@example.com" }} + />, + root, + ), + ); + await click(button("Identity and logo")); + input(field("business-name"), "Unsaved draft"); + await act(() => + render( + <BusinessSettingsScreen + settings={{ name: "Server name", email: "server@example.com" }} + />, + root, + ), + ); + assert.equal(field("business-name").value, "Unsaved draft"); + await click(button("Customer contact")); + assert.equal(field("business-email").value, "server@example.com"); + await click(button("Cancel", field("business-name").closest("form")!)); + await click(button("Identity and logo")); + assert.equal(field("business-name").value, "Server name"); + } finally { + await cleanup(root); + } +}); diff --git a/packages/taler-merchant-webui/src/screens/BusinessSettingsScreen.tsx b/packages/taler-merchant-webui/src/screens/BusinessSettingsScreen.tsx @@ -268,12 +268,10 @@ export function BusinessSettingsScreen({ const committed = useSignal<BusinessSettings>( incomingSettings || DEFAULT_SETTINGS, ); - const [editors, setEditors] = useState<Record<EditorSection, boolean>>(() => + const editors = useSignal<Record<EditorSection, boolean>>( editorState(initialSection), ); - const editorsRef = useRef(editors); const hasReceivedSettingsRef = useRef(Boolean(incomingSettings)); - editorsRef.current = editors; const [name, setName] = useState(committed.value.name); const [email, setEmail] = useState(committed.value.email || ""); @@ -328,9 +326,8 @@ export function BusinessSettingsScreen({ null, ); const [deletionAccountId, setDeletionAccountId] = useState(""); - const [deletionPending, setDeletionPending] = useState(false); + const deletionPending = useSignal(false); const [deletionError, setDeletionError] = useState<unknown>(); - const deletionLock = useRef(false); const committedLogoStatus = useImageDataUrlStatus( committed.value.logoUrl || "", ); @@ -372,7 +369,7 @@ export function BusinessSettingsScreen({ const preserveOpenDrafts = hasReceivedSettingsRef.current; committed.value = incomingSettings; for (const section of Object.keys(CLOSED_EDITORS) as EditorSection[]) { - if (!preserveOpenDrafts || !editorsRef.current[section]) + if (!preserveOpenDrafts || !editors.value[section]) seedSection(section, incomingSettings); } hasReceivedSettingsRef.current = true; @@ -389,14 +386,14 @@ export function BusinessSettingsScreen({ const openSection = (section: EditorSection) => { seedSection(section); setSectionErrors((current) => ({ ...current, [section]: undefined })); - setEditors((current) => ({ ...current, [section]: true })); + editors.value = { ...editors.value, [section]: true }; }; const cancelSection = (section: EditorSection) => { if (section === "security") setSecurityValidation(""); seedSection(section); setSectionErrors((current) => ({ ...current, [section]: undefined })); - setEditors((current) => ({ ...current, [section]: false })); + editors.value = { ...editors.value, [section]: false }; }; const saveSection = async ( @@ -415,7 +412,7 @@ export function BusinessSettingsScreen({ const result = onSave ? await onSave(next) : "saved"; if (result === "challenge") return; committed.value = next; - setEditors((current) => ({ ...current, [section]: false })); + editors.value = { ...editors.value, [section]: false }; setSavedSection(section); } catch (error) { setSectionErrors((current) => ({ ...current, [section]: error })); @@ -464,7 +461,7 @@ export function BusinessSettingsScreen({ if (!result) return; if (result.status === "error") throw result.error; if (result.status === "challenge") return; - setEditors((current) => ({ ...current, security: false })); + editors.value = { ...editors.value, security: false }; setSavedSection("security"); seedSection("security"); } catch (error) { @@ -481,17 +478,16 @@ export function BusinessSettingsScreen({ }; const closeDeletion = () => { - if (deletionPending) return; + if (deletionPending.value) return; setDeletionMode(null); setDeletionAccountId(""); setDeletionError(undefined); }; const deleteAccount = async () => { - if (!deletionMode || !onDeleteAccount || deletionLock.current) return; + if (!deletionMode || !onDeleteAccount || deletionPending.value) return; if (deletionMode === "purge" && deletionAccountId !== accountId) return; - deletionLock.current = true; - setDeletionPending(true); + deletionPending.value = true; setDeletionError(undefined); try { await onDeleteAccount(deletionMode); @@ -500,8 +496,7 @@ export function BusinessSettingsScreen({ } catch (error) { setDeletionError(error); } finally { - deletionLock.current = false; - setDeletionPending(false); + deletionPending.value = false; } }; @@ -613,7 +608,7 @@ export function BusinessSettingsScreen({ title={t`Identity and logo`} subtitle={t`Your public business name and uploaded logo.`} summary={profileSummary} - isOpen={editors.profile} + isOpen={editors.value.profile} saved={savedSection === "profile"} onOpen={() => openSection("profile")} > @@ -677,7 +672,7 @@ export function BusinessSettingsScreen({ title={t`Customer contact`} subtitle={t`Public email address and business website.`} summary={contactSummary} - isOpen={editors.contact} + isOpen={editors.value.contact} saved={savedSection === "contact"} onOpen={() => openSection("contact")} > @@ -736,7 +731,7 @@ export function BusinessSettingsScreen({ title={t`Business locations`} subtitle={t`Physical business address and legal jurisdiction.`} summary={addressSummary} - isOpen={editors.address} + isOpen={editors.value.address} saved={savedSection === "address"} onOpen={() => openSection("address")} > @@ -806,7 +801,7 @@ export function BusinessSettingsScreen({ ? t`Business covers transaction fees` : t`Transaction fees are added to the customer’s payment` } - isOpen={editors.fees} + isOpen={editors.value.fees} saved={savedSection === "fees"} onOpen={() => openSection("fees")} > @@ -854,7 +849,7 @@ export function BusinessSettingsScreen({ title={t`Payment, refund, and payout timing`} subtitle={t`Default time limits for new orders and payouts.`} summary={delaySummary} - isOpen={editors.delays} + isOpen={editors.value.delays} saved={savedSection === "delays"} onOpen={() => openSection("delays")} > @@ -972,7 +967,7 @@ export function BusinessSettingsScreen({ summary={ committed.value.phone_number || t`No verification phone configured` } - isOpen={editors.phone} + isOpen={editors.value.phone} saved={savedSection === "phone"} onOpen={() => openSection("phone")} > @@ -1028,7 +1023,7 @@ export function BusinessSettingsScreen({ title={t`Account password`} subtitle={t`Change the password used to sign into this merchant account.`} summary={t`Password is hidden`} - isOpen={editors.security} + isOpen={editors.value.security} saved={savedSection === "security"} onOpen={() => openSection("security")} > @@ -1162,7 +1157,7 @@ export function BusinessSettingsScreen({ onInput={(event) => setDeletionAccountId(event.currentTarget.value) } - disabled={deletionPending} + disabled={deletionPending.value} autoComplete="off" class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-red-500" /> @@ -1175,14 +1170,14 @@ export function BusinessSettingsScreen({ <Button variant="secondary" onClick={closeDeletion} - disabled={deletionPending} + disabled={deletionPending.value} >{t`Cancel`}</Button> <Button variant="danger" onClick={() => void deleteAccount()} - isLoading={deletionPending} + isLoading={deletionPending.value} disabled={ - deletionPending || + deletionPending.value || (deletionMode === "purge" && deletionAccountId !== accountId) } >