taler-typescript-core

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

commit 5addd9e394a4f818f88687d3552817ab9de2ce9a
parent 3f0c08da0046b252f2da7e1b0891c58f5c8b04f5
Author: Florian Dold <dold@taler.net>
Date:   Sun, 13 Sep 2026 17:14:31 +0200

wallet web UI: clarify payment directions and financial labels

Use Payment request for peer-pull links and distinguish wallet costs
from transfer amounts. Describe bank confirmations, payment references,
renewal, recovery and browser storage protection consistently.

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts | 8++++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts | 4+++-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts | 8+++++---
Mpackages/wallet-webui/src/routes/App.tsx | 7+++----
Mpackages/wallet-webui/src/routes/transaction-model.ts | 42++++++++++++++++++++++++------------------
Mpackages/wallet-webui/src/routes/withdrawal-model.ts | 2+-
Mpackages/wallet-webui/src/screens/ActionScreen.tsx | 4++--
Mpackages/wallet-webui/src/screens/BankConfirmationScreen.tsx | 15++++++---------
Mpackages/wallet-webui/src/screens/DepositScreen.tsx | 2+-
Mpackages/wallet-webui/src/screens/DonauStatementsScreen.tsx | 2+-
Mpackages/wallet-webui/src/screens/PaymentScreen.tsx | 10+++++-----
Mpackages/wallet-webui/src/screens/PeerCreateScreen.tsx | 22++++++++++++----------
Mpackages/wallet-webui/src/screens/PeerReceiveScreen.tsx | 14+++++++-------
Mpackages/wallet-webui/src/screens/PeerShareScreen.tsx | 18+++++++++++-------
Mpackages/wallet-webui/src/screens/SettingsScreen.tsx | 2+-
Mpackages/wallet-webui/src/screens/StorybookScreen.tsx | 5++++-
Mpackages/wallet-webui/src/screens/TermsScreen.tsx | 2+-
Mpackages/wallet-webui/src/screens/TransactionDetailScreen.tsx | 6+++---
Mpackages/wallet-webui/src/screens/WireTransferScreen.tsx | 2+-
Mpackages/wallet-webui/src/ui/Shell.tsx | 4++--
Mpackages/wallet-webui/test/screens.test.tsx | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/wallet-webui/test/transaction-model.test.ts | 3+--
Mpackages/wallet-webui/test/withdrawal-model.test.ts | 5+++--
24 files changed, 202 insertions(+), 95 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts @@ -141,7 +141,7 @@ export async function runWalletWebUiDemoTest(t: GlobalTestState) { await page.getByText("Completed", { exact: true }).waitFor(); // Demo state is deliberately in-memory, so keep this navigation inside // the SPA instead of reloading and constructing a fresh demo wallet. - await page.getByRole("link", { name: "Assets", exact: true }).click(); + await page.getByRole("link", { name: "Balances", exact: true }).click(); await page .locator("strong:visible", { hasText: "37.40" }) .first() diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts @@ -169,7 +169,9 @@ export async function runWalletWebUiPwaPeerDepositTest(t: GlobalTestState) { await page.goto(`${senderUrl}#/peer/send?currency=TESTKUDOS`); await page.getByRole("heading", { name: "Send money" }).waitFor(); await page.getByLabel("Amount").fill("3"); - await page.getByLabel("Subject").fill("Browser push transfer"); + await page + .getByLabel("Payment description") + .fill("Browser push transfer"); await page.getByRole("button", { name: "Review" }).click(); await page.getByText("Recipient gets", { exact: true }).waitFor(); await page @@ -217,7 +219,9 @@ export async function runWalletWebUiPwaPeerDepositTest(t: GlobalTestState) { await page.getByRole("heading", { name: "Request money" }).waitFor(); await page.getByLabel("Currency and payment scope").waitFor(); await page.getByLabel("Amount (TESTKUDOS)").fill("2"); - await page.getByLabel("Subject").fill("Browser pull request"); + await page + .getByLabel("Payment description") + .fill("Browser pull request"); await page.getByRole("button", { name: "Review" }).click(); await page .getByText("You receive", { exact: true }) diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts @@ -184,7 +184,9 @@ export async function runWalletWebUiPwaPeerTosTest(t: GlobalTestState) { await page.getByRole("heading", { name: "Request money" }).waitFor(); await page.getByLabel("Currency and payment scope").waitFor(); await page.getByLabel("Amount (TESTKUDOS)").fill("2"); - await page.getByLabel("Subject").fill("Terms-gated browser request"); + await page + .getByLabel("Payment description") + .fill("Terms-gated browser request"); await page.getByRole("button", { name: "Review" }).click(); await page .getByText( diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts @@ -536,7 +536,7 @@ async function runPwaWithdrawalTest( const transferSubject = ( await page - .locator("dt", { hasText: /^Subject$/ }) + .locator("dt", { hasText: /^Payment reference$/ }) .first() .locator("xpath=following-sibling::dd[1]/span[1]") .textContent() @@ -620,7 +620,7 @@ async function runPwaWithdrawalTest( const transferSubject = ( await page - .locator("dt", { hasText: /^Subject$/ }) + .locator("dt", { hasText: /^Payment reference$/ }) .first() .locator("xpath=following-sibling::dd[1]/span[1]") .textContent() @@ -699,7 +699,9 @@ async function runPwaWithdrawalTest( ); } await page.locator('input[inputmode="decimal"]').fill("1"); - await page.getByLabel("Subject").fill("Global scope transfer"); + await page + .getByLabel("Payment description") + .fill("Global scope transfer"); await page.getByRole("button", { name: "Review" }).click(); await page .getByText("Global scope transfer", { exact: true }) diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx @@ -3019,7 +3019,7 @@ function QrScanRoute() { for (const track of stream?.getTracks() ?? []) track.stop(); if (!finished) setError( - i18n.str`Camera access is unavailable. You can paste the URI from the balances screen.`, + i18n.str`Camera access is unavailable. You can enter a Taler link manually.`, ); }); return () => { @@ -3708,7 +3708,6 @@ function IntegratedWithdrawalRoute() { return ( <BankConfirmationScreen amount={ - prepared?.details.amountEffective ?? prepared?.amount ?? info.amount ?? i18n.str`Amount selected at cash acceptor` @@ -4290,7 +4289,7 @@ function PaymentRoute() { const statusTransaction = repurchaseTransactionId ? (replayPayment ?? transaction) : transaction; - const statusFallback = i18n.str`Payment is ${transactionStateView(statusTransaction).label}.`; + const statusFallback = i18n.str`Payment status: ${transactionStateView(statusTransaction).label}`; const repurchaseError = repurchaseQuery.error && repurchaseTransactionId ? errorFromException( @@ -5770,7 +5769,7 @@ function SettingsRoute() { void durability.requestProtection().then((result) => { if (result === "granted") { setPersistenceMessage( - i18n.str`Wallet data protection was granted.`, + i18n.str`Wallet data is protected from automatic deletion.`, ); } else { setPersistenceMessage( diff --git a/packages/wallet-webui/src/routes/transaction-model.ts b/packages/wallet-webui/src/routes/transaction-model.ts @@ -106,7 +106,7 @@ function typePresentations(): Record< [TransactionType.Refresh]: { // Translators: Transaction type for reissuing digital coins, not for // reloading the user interface. - label: i18n.str`Refresh`, + label: i18n.str`Digital cash renewal`, icon: "↻", direction: "neutral", }, @@ -118,29 +118,29 @@ function typePresentations(): Record< direction: "debit", }, [TransactionType.PeerPushDebit]: { - label: i18n.str`Sent transfer`, + label: i18n.str`Outgoing transfer`, icon: "↗", direction: "debit", }, [TransactionType.PeerPushCredit]: { - label: i18n.str`Received transfer`, + label: i18n.str`Incoming transfer`, icon: "↙", direction: "credit", }, [TransactionType.PeerPullDebit]: { - label: i18n.str`Paid invoice`, + label: i18n.str`Payment`, icon: "↗", direction: "debit", }, [TransactionType.PeerPullCredit]: { - label: i18n.str`Invoice`, + label: i18n.str`Payment request`, icon: "↙", direction: "credit", }, [TransactionType.Recoup]: { // Translators: Taler transaction type for recovering value from digital // coins revoked by an exchange. - label: i18n.str`Recoup`, + label: i18n.str`Digital cash recovery`, icon: "↙", direction: "credit", }, @@ -198,17 +198,17 @@ function titleAndSubtitle(transaction: Transaction): { } case TransactionType.PeerPullCredit: case TransactionType.PeerPullDebit: - return { title: transaction.info.summary ?? i18n.str`Invoice` }; + return { title: transaction.info.summary ?? i18n.str`Payment request` }; case TransactionType.PeerPushCredit: case TransactionType.PeerPushDebit: return { title: transaction.info.summary ?? i18n.str`Transfer` }; case TransactionType.Refresh: return { - title: i18n.str`Refresh digital cash`, + title: i18n.str`Digital cash renewal`, subtitle: transaction.refreshReason, }; case TransactionType.Recoup: - return { title: i18n.str`Funds recovered from an exchange` }; + return { title: i18n.str`Digital cash recovery` }; case TransactionType.DenomLoss: return { title: i18n.str`Digital cash became unusable`, @@ -239,7 +239,7 @@ function minorLabels(): Partial<Record<TransactionMinorState, string>> { [TransactionMinorState.Ready]: i18n.ctx("transaction minor state")`Ready`, [TransactionMinorState.RebindSession]: i18n.str`Restoring merchant session`, [TransactionMinorState.SubmitPayment]: i18n.str`Submitting payment`, - [TransactionMinorState.Refresh]: i18n.str`Refreshing digital cash`, + [TransactionMinorState.Refresh]: i18n.str`Renewing digital cash`, [TransactionMinorState.Track]: i18n.str`Tracking bank transfer`, [TransactionMinorState.WaitingForOtherWallet]: i18n.str`Waiting for another wallet`, }; @@ -378,7 +378,7 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { fields.push( { label: i18n.str`Purchase price`, value: transaction.amountRaw }, { - label: i18n.str`Confirmed wallet debit`, + label: i18n.str`Wallet spends`, value: transaction.amountEffective, }, ); @@ -447,7 +447,7 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { value: transaction.amountRaw, }, { - label: i18n.str`Confirmed wallet debit`, + label: i18n.str`Wallet spends`, value: transaction.amountEffective, }, ); @@ -483,19 +483,19 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { fields.push( { // Translators: Amount entering a digital-coin refresh transaction. - label: i18n.str`Input`, + label: i18n.str`Amount before renewal`, value: transaction.refreshInputAmount, }, { // Translators: Amount produced by a digital-coin refresh transaction. - label: i18n.str`Output`, + label: i18n.str`Amount after renewal`, value: transaction.refreshOutputAmount, }, ); break; case TransactionType.PeerPullCredit: fields.push( - { label: i18n.str`Invoice amount`, value: transaction.amountRaw }, + { label: i18n.str`Requested amount`, value: transaction.amountRaw }, { label: i18n.str`Expected wallet credit`, value: transaction.amountEffective, @@ -515,9 +515,15 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { case TransactionType.PeerPullDebit: case TransactionType.PeerPushDebit: fields.push( - { label: i18n.str`Transfer amount`, value: transaction.amountRaw }, { - label: i18n.str`Confirmed wallet debit`, + label: + transaction.type === TransactionType.PeerPullDebit + ? i18n.str`Requested amount` + : i18n.str`Transfer amount`, + value: transaction.amountRaw, + }, + { + label: i18n.str`Wallet spends`, value: transaction.amountEffective, }, ); @@ -568,7 +574,7 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { break; case TransactionType.Recoup: fields.push( - { label: i18n.str`Recovered amount`, value: transaction.amountRaw }, + { label: i18n.str`Recovery amount`, value: transaction.amountRaw }, { label: i18n.str`Expected wallet credit`, value: transaction.amountEffective, diff --git a/packages/wallet-webui/src/routes/withdrawal-model.ts b/packages/wallet-webui/src/routes/withdrawal-model.ts @@ -202,7 +202,7 @@ export function wireOptions( if (amount) fields.push({ label: i18n.str`Amount`, value: amount }); if (subject) // Translators: Subject or remittance reference for a bank transfer. - fields.push({ label: i18n.str`Subject`, value: subject }); + fields.push({ label: i18n.str`Payment reference`, value: subject }); return { id: `${accountIndex}-${optionIndex}`, label: diff --git a/packages/wallet-webui/src/screens/ActionScreen.tsx b/packages/wallet-webui/src/screens/ActionScreen.tsx @@ -36,9 +36,9 @@ function actionKindLabel(kind: string): string { case "refund": return i18n.str`Refund`; case "pay-push": - return i18n.str`Send digital cash`; + return i18n.str`Receive money`; case "pay-pull": - return i18n.str`Request digital cash`; + return i18n.str`Payment request`; case "pay-template": return i18n.str`Payment template`; case "restore": diff --git a/packages/wallet-webui/src/screens/BankConfirmationScreen.tsx b/packages/wallet-webui/src/screens/BankConfirmationScreen.tsx @@ -17,12 +17,7 @@ export function BankConfirmationScreen(props: { <div class="mx-auto w-full max-w-2xl space-y-6"> <div> <p class="text-sm font-semibold uppercase tracking-wide text-primary"> - {status === "confirmed" - ? // The funding transfer from the user's bank to the exchange was accepted. - i18n.str`Withdrawal confirmed` - : status === "aborted" - ? i18n.str`Withdrawal canceled` - : i18n.str`Withdrawal created`} + {i18n.str`Digital cash withdrawal`} </p> <h1 class="mt-1 text-2xl font-medium"> {status === "confirmed" @@ -30,14 +25,16 @@ export function BankConfirmationScreen(props: { i18n.str`Bank transfer confirmed` : status === "aborted" ? i18n.str`No bank transfer will be made` - : i18n.str`Confirm with your bank`} + : props.hasConfirmationUrl + ? i18n.str`Confirm with your bank` + : i18n.str`Waiting for bank confirmation`} </h1> </div> <Card class="text-center"> <p class="text-sm text-secondary"> { - // Label above the amount of digital cash being acquired using bank funds. - i18n.str`Digital cash withdrawal` + // Translators: Amount transferred from the bank, before digital-cash withdrawal fees. + i18n.str`Bank transfer amount` } </p> <p class="mt-2 text-4xl font-semibold">{props.amount}</p> diff --git a/packages/wallet-webui/src/screens/DepositScreen.tsx b/packages/wallet-webui/src/screens/DepositScreen.tsx @@ -304,7 +304,7 @@ export function DepositScreen(props: { <dd class="text-right">{props.review.coinFee}</dd> <dt class="text-secondary">{i18n.str`Wire fees`}</dt> <dd class="text-right">{props.review.wireFee}</dd> - <dt class="text-secondary">{i18n.str`Refresh fees`}</dt> + <dt class="text-secondary">{i18n.str`Renewal fees`}</dt> <dd class="text-right">{props.review.refreshFee}</dd> </dl> </Card> diff --git a/packages/wallet-webui/src/screens/DonauStatementsScreen.tsx b/packages/wallet-webui/src/screens/DonauStatementsScreen.tsx @@ -102,7 +102,7 @@ export function DonauStatementsScreen(props: { <dt class="text-secondary"> { // Financial or legal jurisdiction reported by Donau, not an Internet domain name. - i18n.str`Legal domain` + i18n.str`Jurisdiction` } </dt> <dd>{statement.legalDomain}</dd> diff --git a/packages/wallet-webui/src/screens/PaymentScreen.tsx b/packages/wallet-webui/src/screens/PaymentScreen.tsx @@ -222,8 +222,8 @@ export function PaymentOptions(props: { {choice.unavailableReason ?? (choice.availableBalance ? // Token means a merchant-issued discount or subscription token, not an authentication token. - i18n.str`Insufficient balance or required token (${choice.availableBalance} available)` - : i18n.str`Insufficient balance or required token`)} + i18n.str`Insufficient balance or missing required token (${choice.availableBalance} available)` + : i18n.str`Insufficient balance or missing required token`)} </span> )} {choice.tokenWarning && ( @@ -251,8 +251,8 @@ export function PaymentOptions(props: { <p role="alert" class="mt-3 text-error"> {selected.unavailableReason ?? (selected.availableBalance - ? i18n.str`Insufficient balance (${selected.availableBalance} available)` - : i18n.str`Insufficient balance`)} + ? i18n.str`Insufficient balance or missing required token (${selected.availableBalance} available)` + : i18n.str`Insufficient balance or missing required token`)} </p> )} {selected.tokenWarning && ( @@ -292,7 +292,7 @@ export function PaymentOptions(props: { <p class="mt-2 text-sm"> {selected.donationReceipt.status === "setup" ? i18n.str`This payment can provide a donation receipt. Configure one of the supported donation authorities first.` - : i18n.str`The configured donation authority is not supported by this merchant choice.`} + : i18n.str`This payment option does not support the configured donation authority.`} </p> {props.onConfigureDonau && selected.donationReceipt.donauUrls[0] && ( diff --git a/packages/wallet-webui/src/screens/PeerCreateScreen.tsx b/packages/wallet-webui/src/screens/PeerCreateScreen.tsx @@ -257,7 +257,9 @@ export function PeerCreateScreen(props: { <button type="button" onClick={props.review ? props.onBack : props.onCancel} - aria-label={i18n.str`Back to balances`} + aria-label={ + props.review ? i18n.str`Change` : i18n.str`Back to balances` + } class="grid h-11 w-11 place-items-center rounded-full text-2xl hover:bg-secondaryContainer" > ← @@ -272,8 +274,8 @@ export function PeerCreateScreen(props: { </h1> <p class="text-sm text-secondary"> {send - ? i18n.str`Create a private claim link for the recipient` - : i18n.str`Create an invoice link for the payer`} + ? i18n.str`Create a payment link for the recipient` + : /* Translators: Create a peer-pull request. The person who opens it is being asked to pay this wallet. */ i18n.str`Create a payment request link`} </p> </div> </div> @@ -346,7 +348,7 @@ export function PeerCreateScreen(props: { <div class="form-field-stack space-y-5"> <label> {/* Translators: User-entered peer-payment subject shown to the other party. */} - <span class="mb-1 block text-sm text-secondary">{i18n.str`Subject`}</span> + <span class="mb-1 block text-sm text-secondary">{i18n.str`Payment description`}</span> <input required value={summary} @@ -505,8 +507,8 @@ export function PeerCreateScreen(props: { <Card class="text-center"> <p class="text-sm text-secondary"> {send - ? // Translators: Label for the effective amount received after fees. - i18n.str`Recipient gets` + ? // Translators: Total cost to this wallet, including fees; not the recipient's credit. + i18n.str`Wallet spends` : // Translators: Label for the effective amount this wallet receives after fees. i18n.str`You receive`} </p> @@ -519,10 +521,10 @@ export function PeerCreateScreen(props: { <dl class="grid grid-cols-2 gap-3 text-sm"> <dt class="text-secondary"> {send - ? // Translators: Label for the total amount debited from this wallet. - i18n.str`Wallet spends` - : // Translators: Label for the raw amount the remote payer sends. - i18n.str`Payer sends`} + ? // Translators: Amount in the payment link, before the recipient's withdrawal fees. + i18n.str`Transfer amount` + : // Translators: Requested amount, excluding any fees charged to the payer. + i18n.str`Requested amount`} </dt> <dd class="text-right font-semibold">{props.review.amountRaw}</dd> {/* Translators: GNU Taler exchange service, not a currency exchange action. */} diff --git a/packages/wallet-webui/src/screens/PeerReceiveScreen.tsx b/packages/wallet-webui/src/screens/PeerReceiveScreen.tsx @@ -40,8 +40,8 @@ export function PeerReceiveScreen(props: { {receive ? // Translators: Heading for accepting money from a peer push-payment link. i18n.str`Receive money` - : // Translators: Heading for paying a peer's pull-payment invoice link. - i18n.str`Pay request`} + : // Translators: Noun heading for reviewing another person's request for money. + i18n.str`Payment request`} </h1> <p class="text-sm text-secondary">{i18n.str`Review the peer payment before continuing`}</p> </div> @@ -70,8 +70,8 @@ export function PeerReceiveScreen(props: { {receive ? // Translators: Label for the effective amount this wallet receives after fees. i18n.str`You receive` - : // Translators: Label for the effective amount the remote recipient receives after fees. - i18n.str`Recipient gets`} + : // Translators: Total cost to this wallet, including fees; not the recipient's credit. + i18n.str`Wallet spends`} </p> <p class="mt-1 text-3xl font-semibold">{props.amountEffective}</p> <p class="mt-3 font-medium">{props.summary}</p> @@ -80,10 +80,10 @@ export function PeerReceiveScreen(props: { <dl class="grid grid-cols-2 gap-3 text-sm"> <dt class="text-secondary"> {receive - ? // Translators: Label for the raw amount transferred by the remote payer. + ? // Translators: Amount in the payment link, before this wallet's withdrawal fees. i18n.str`Transfer amount` - : // Translators: Label for the total amount debited from this wallet. - i18n.str`Wallet spends`} + : // Translators: Requested amount, excluding this wallet's payment fees. + i18n.str`Requested amount`} </dt> <dd class="text-right font-semibold">{props.amountRaw}</dd> {/* Translators: GNU Taler exchange service, not a currency exchange action. */} diff --git a/packages/wallet-webui/src/screens/PeerShareScreen.tsx b/packages/wallet-webui/src/screens/PeerShareScreen.tsx @@ -25,18 +25,22 @@ export function PeerShareScreen(props: { <h1 class="text-2xl font-medium"> {props.mode === "send" ? // Translators: Heading for sharing a push-payment claim link with its recipient. - i18n.str`Share money` - : // Translators: Heading for sharing a pull-payment invoice link with its payer. - i18n.str`Share request`} + i18n.str`Share payment link` + : // Translators: Heading for sharing a pull-payment request link with its payer. + i18n.str`Share payment request`} </h1> - <p class="text-sm text-secondary">{i18n.str`The link contains the claim. Share it only with the intended person.`}</p> + <p class="text-sm text-secondary"> + {props.mode === "send" + ? i18n.str`Anyone with this link can receive the money. Share it only with the intended recipient.` + : i18n.str`Share this link with the person you are requesting money from.`} + </p> </div> {props.loading && ( <Card> <p role="status"> {props.delayed - ? i18n.str`Waiting for a response from the exchange…` - : i18n.str`Preparing a secure share link…`} + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Waiting for a response from the exchange…` + : /* Translators: The exchange is preparing a link for a peer transfer or request that the user can share with the other person. */ i18n.str`Preparing a secure share link…`} </p> </Card> )} @@ -72,7 +76,7 @@ export function PeerShareScreen(props: { alt={ props.mode === "send" ? i18n.str`QR code for the peer payment link` - : i18n.str`QR code for the peer request link` + : /* Translators: This QR code contains a payment request for another person to pay, not a link that gives them money. */ i18n.str`QR code for the peer request link` } /> <Card> diff --git a/packages/wallet-webui/src/screens/SettingsScreen.tsx b/packages/wallet-webui/src/screens/SettingsScreen.tsx @@ -375,7 +375,7 @@ export function SettingsScreen(props: { </Card> {!props.demo && props.persistence && ( <Card> - <h2 class="font-semibold">{i18n.str`Wallet data protection`}</h2> + <h2 class="font-semibold">{i18n.str`Protection from automatic deletion`}</h2> <p class="my-3 text-sm text-secondary"> {props.persistence?.durability === "persistent" ? i18n.str`The browser has protected this origin from automatic storage eviction. You can still erase it explicitly by clearing site data.` diff --git a/packages/wallet-webui/src/screens/StorybookScreen.tsx b/packages/wallet-webui/src/screens/StorybookScreen.tsx @@ -1490,7 +1490,10 @@ Example exchange terms.`} value: "Taler Operations AG", }, { label: i18n.str`Amount`, value: "CHF:11.23" }, - { label: i18n.str`Subject`, value: "RESERVE123" }, + { + label: i18n.str`Payment reference`, + value: "RESERVE123", + }, ], }, { diff --git a/packages/wallet-webui/src/screens/TermsScreen.tsx b/packages/wallet-webui/src/screens/TermsScreen.tsx @@ -14,7 +14,7 @@ export function exchangeTermsStatusLabel(status: string | undefined): string { case "pending": return i18n.str`Pending`; case "proposed": - return i18n.str`Proposed`; + return i18n.str`Not yet accepted`; case "accepted": return i18n.str`Accepted`; case "missing-tos": diff --git a/packages/wallet-webui/src/screens/TransactionDetailScreen.tsx b/packages/wallet-webui/src/screens/TransactionDetailScreen.tsx @@ -222,7 +222,7 @@ export function TransactionDetailScreen(props: { <p class="mt-2 text-sm text-secondary"> {transaction.continuationAction.kind === "deposit-kyc-auth" ? i18n.str`Make a small authentication transfer from the deposit account before the exchange can send money to it.` - : i18n.str`Reopen the exchange account, amount, and mandatory transfer subject for this pending withdrawal.`} + : i18n.str`View the bank details, amount, and required payment reference for this withdrawal.`} </p> <div class="mt-4"> <Button onClick={() => props.onContinue?.()}> @@ -233,8 +233,8 @@ export function TransactionDetailScreen(props: { )} {transaction.peerShareUri && ( <Card> - <h2 class="font-semibold">{i18n.str`Share money`}</h2> - <p class="mt-2 text-sm text-secondary">{i18n.str`The link contains the claim. Share it only with the intended person.`}</p> + <h2 class="font-semibold">{i18n.str`Share payment link`}</h2> + <p class="mt-2 text-sm text-secondary">{i18n.str`Anyone with this link can receive the money. Share it only with the intended recipient.`}</p> <div class="mt-4"> <QrFrame content={transaction.peerShareUri} diff --git a/packages/wallet-webui/src/screens/WireTransferScreen.tsx b/packages/wallet-webui/src/screens/WireTransferScreen.tsx @@ -323,7 +323,7 @@ export function WireTransferScreen(props: { <div> <h4 class="text-lg font-semibold">{i18n.str`Wire transfer instructions`}</h4> {/* Translators: “subject” means the mandatory bank-transfer reference, not an email subject. */} - <p class="mt-2 text-sm text-secondary">{i18n.str`Copy these details exactly into your banking app. The subject is mandatory.`}</p> + <p class="mt-2 text-sm text-secondary">{i18n.str`Copy these details exactly into your banking app. The payment reference is required.`}</p> <dl class="mt-5 space-y-5"> {option.fields.map((field) => ( <div key={`${field.label}-${field.value}`}> diff --git a/packages/wallet-webui/src/ui/Shell.tsx b/packages/wallet-webui/src/ui/Shell.tsx @@ -36,7 +36,7 @@ export function Shell(props: { const depositLabel = i18n.ctx("wallet action")`Deposit`; // Translators: Mobile navigation item for all balances and currencies held // by the wallet, not generic files or property. - const assetsLabel = i18n.str`Assets`; + const balancesLabel = i18n.str`Balances`; return ( <div class="min-h-screen bg-background pb-24 text-onBackground md:pb-0"> <header @@ -162,7 +162,7 @@ export function Shell(props: { class="fixed inset-x-0 bottom-0 z-20 grid grid-cols-3 items-end border-t border-outlineVariant bg-surface px-4 pb-[max(.55rem,env(safe-area-inset-bottom))] pt-2 md:hidden" > <MobileNav href="/" icon="balances"> - {assetsLabel} + {balancesLabel} </MobileNav> <Link href="/scan" diff --git a/packages/wallet-webui/test/screens.test.tsx b/packages/wallet-webui/test/screens.test.tsx @@ -110,7 +110,7 @@ test("terms language selection preserves every ordered choice", async () => { languages, ); assert.equal(select.value, "it"); - assert(view.getByText("Status: Proposed")); + assert(view.getByText("Status: Not yet accepted")); await userEvent .setup({ document: window.document as unknown as Document }) .selectOptions(select, "fr"); @@ -400,6 +400,7 @@ test("DD 86 desktop shell exposes every supported navigation entry", async () => <p>Wallet content</p> </Shell>, ); + const desktop = view.container.querySelector("aside")!; view.getByRole("link", { name: "Enter Taler link" }); assert.equal(view.getAllByRole("link", { name: "Scan QR code" }).length, 2); for (const name of [ @@ -414,13 +415,29 @@ test("DD 86 desktop shell exposes every supported navigation entry", async () => "Donations", "Bank accounts", ]) { - view.getByRole("link", { name }); + assert( + view + .getAllByRole("link", { name }) + .some((link: HTMLElement) => desktop.contains(link)), + ); } assert.equal(view.queryByRole("link", { name: "Payment contacts" }), null); assert.equal( - view.getByRole("link", { name: "Balances" }).getAttribute("aria-current"), + view + .getAllByRole("link", { name: "Balances" }) + .find((link: HTMLElement) => desktop.contains(link))! + .getAttribute("aria-current"), "page", ); + assert.equal( + view + .getAllByRole("link", { name: "Balances" }) + .find((link: HTMLElement) => + view.getByRole("navigation", { name: "Wallet" }).contains(link), + )! + .getAttribute("href"), + "/", + ); cleanup(); await window.happyDOM.abort(); }); @@ -2351,6 +2368,69 @@ test("deposit review confirms the checked destination and amounts", async () => await window.happyDOM.abort(); }); +test("peer reviews distinguish wallet costs and credits from transfer amounts", async () => { + const window = installDom(); + const { render, cleanup } = await import("@testing-library/preact"); + for (const flow of ["send", "request", "receive", "pay"] as const) { + const debit = flow === "send" || flow === "pay"; + const amounts = { + amountRaw: "CHF:10", + amountEffective: debit ? "CHF:10.10" : "CHF:9.90", + }; + let confirmed = false; + const handlers = { + onConfirm: () => { + confirmed = true; + }, + onCancel: () => {}, + onWithdraw: () => {}, + }; + const view = render( + flow === "send" || flow === "request" ? ( + <PeerCreateScreen + mode={flow} + currency="CHF" + working={false} + review={{ + ...amounts, + exchange: "exchange.example", + summary: "Lunch", + }} + onReview={() => {}} + onBack={() => {}} + {...handlers} + /> + ) : ( + <PeerReceiveScreen + kind={flow} + loading={false} + working={false} + {...amounts} + {...handlers} + /> + ), + ); + assert.equal( + view.getByText(debit ? "Wallet spends" : "You receive").nextElementSibling + ?.textContent, + amounts.amountEffective, + `${flow}: the effective amount is this wallet's cost or credit`, + ); + assert.equal( + view.getByText( + flow === "request" || flow === "pay" + ? "Requested amount" + : "Transfer amount", + ).nextElementSibling?.textContent, + amounts.amountRaw, + `${flow}: the nominal amount excludes wallet fees`, + ); + assert.equal(confirmed, false, "reviewing does not authorize a transfer"); + cleanup(); + } + await window.happyDOM.abort(); +}); + test("peer transfer review blocks an insufficient transfer", async () => { const window = installDom(); const { render, cleanup } = await import("@testing-library/preact"); @@ -2525,7 +2605,10 @@ test("peer pull review explicitly uses the wallet default expiration", async () }); assert(view.getByRole("button", { name: "Change link expiry (Default)" })); await user.type(view.getByRole("textbox", { name: "Amount (CHF)" }), "2"); - await user.type(view.getByRole("textbox", { name: "Subject" }), "Dinner"); + await user.type( + view.getByRole("textbox", { name: "Payment description" }), + "Dinner", + ); assert(view.getByText("Checking default expiry…")); await view.findByText("in 1 day"); assert.equal(previewRequests, 1); @@ -2619,7 +2702,10 @@ test("peer creation keeps same-currency payment scopes distinct", async () => { "exchange:CHF:https://two.example/", ); await user.type(view.getByLabelText("Amount (CHF)"), "2"); - await user.type(view.getByRole("textbox", { name: "Subject" }), "Lunch"); + await user.type( + view.getByRole("textbox", { name: "Payment description" }), + "Lunch", + ); view.rerender( <main> <PeerCreateScreen @@ -3100,7 +3186,7 @@ test("wire instructions tab between options and copy the mandatory subject and p paytoUri, fields: [ { label: "IBAN", value: "CH68 0857" }, - { label: "Subject", value: "RESERVE123" }, + { label: "Payment reference", value: "RESERVE123" }, ], }, { @@ -3164,7 +3250,7 @@ test("wire instructions become unavailable when their expiry is reached", async { id: "bank", label: "Bank transfer", - fields: [{ label: "Subject", value: "RESERVE123" }], + fields: [{ label: "Payment reference", value: "RESERVE123" }], paytoUri: "payto://iban/CH680857?message=RESERVE123", qrContent: "payto://iban/CH680857?message=RESERVE123", externalUri: "https://bank.example/transfer/ABC", @@ -3205,7 +3291,7 @@ test("wire instructions explain and disable an incompatible source account", asy { id: "restricted", label: "Restricted exchange account", - fields: [{ label: "Subject", value: "KYC123" }], + fields: [{ label: "Payment reference", value: "KYC123" }], restrictionHints: [ "Only transfers from account CH12 are accepted.", ], @@ -3739,7 +3825,9 @@ test("developer settings require explicit confirmation before clearing the walle (arrow: Element) => arrow.getAttribute("aria-hidden") === "true", ), ); - assert(view.getByRole("heading", { name: "Wallet data protection" })); + assert( + view.getByRole("heading", { name: "Protection from automatic deletion" }), + ); assert(view.getByRole("heading", { name: "Browser integration" })); assert(view.getByRole("heading", { name: "Balance display" })); const diagnosticsFormat = view.getByRole("combobox", { @@ -3967,7 +4055,7 @@ test("completed withdrawals replace obsolete transfer instructions in place", as { id: "bank", label: "Bank transfer", - fields: [{ label: "Subject", value: "RESERVE" }], + fields: [{ label: "Payment reference", value: "RESERVE" }], }, ]} outcome={{ diff --git a/packages/wallet-webui/test/transaction-model.test.ts b/packages/wallet-webui/test/transaction-model.test.ts @@ -316,8 +316,7 @@ test("contract-less payment failures omit unknown price fields", () => { assert( !view.fields.some( (field) => - field.label === "Purchase price" || - field.label === "Confirmed wallet debit", + field.label === "Purchase price" || field.label === "Wallet spends", ), ); }); diff --git a/packages/wallet-webui/test/withdrawal-model.test.ts b/packages/wallet-webui/test/withdrawal-model.test.ts @@ -57,7 +57,8 @@ test("wire options expose accepted post-reserve transfer details", () => { ); assert( options[0].fields.some( - (field) => field.label === "Subject" && field.value === "RESERVE123", + (field) => + field.label === "Payment reference" && field.value === "RESERVE123", ), ); assert( @@ -259,7 +260,7 @@ test("Swiss QR bill options prefer the SPC code and its mandated visual style", assert( option.fields.some( (field) => - field.label === "Subject" && + field.label === "Payment reference" && field.value === "210000000003139471430009017", ), );