aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src')
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx44
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx60
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx240
-rw-r--r--packages/taler-wallet-webextension/src/components/Checkbox.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorMessage.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx83
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx74
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx10
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx63
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/state.ts48
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx34
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts5
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx31
-rw-r--r--packages/taler-wallet-webextension/src/components/Time.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1050
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx11
-rw-r--r--packages/taler-wallet-webextension/src/context/alert.ts89
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/state.ts3
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/views.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts (renamed from packages/taler-wallet-webextension/src/cta/Reward/index.ts)57
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx (renamed from packages/taler-wallet-webextension/src/cta/Reward/stories.tsx)23
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts65
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx74
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts10
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx26
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts15
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts3
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx48
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/test.ts10
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx58
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts2
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts153
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/state.ts3
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/views.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/cta/Reward/state.ts99
-rw-r--r--packages/taler-wallet-webextension/src/cta/Reward/test.ts228
-rw-r--r--packages/taler-wallet-webextension/src/cta/Reward/views.tsx92
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts21
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts11
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts15
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts196
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts32
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx182
-rw-r--r--packages/taler-wallet-webextension/src/cta/index.stories.ts1
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts3
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts3
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useSettings.ts14
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po16
-rw-r--r--packages/taler-wallet-webextension/src/i18n/es.po10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fi.po1967
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fr.po10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/nl.po13
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ru.po1977
-rw-r--r--packages/taler-wallet-webextension/src/i18n/tr.po4
-rw-r--r--packages/taler-wallet-webextension/src/i18n/uk.po1956
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/handlers.ts4
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormControl.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx22
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts87
-rw-r--r--packages/taler-wallet-webextension/src/platform/background.ts3
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts393
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts11
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts11
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx7
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/svg/search_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts134
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts314
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts19
-rw-r--r--packages/taler-wallet-webextension/src/utils/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts8
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts78
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts20
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts113
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts249
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx152
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx100
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts55
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts13
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx489
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts7
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx280
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx333
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts5
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/state.ts1
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx136
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx107
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx86
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx61
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx262
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx34
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx1000
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.tsx62
-rw-r--r--packages/taler-wallet-webextension/src/wallet/index.stories.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts72
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts376
123 files changed, 11112 insertions, 3755 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 167f1797c..fe348f7fb 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -34,6 +34,7 @@ import {
} from "./components/styled/index.js";
import { useBackendContext } from "./context/backend.js";
import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
+import searchIcon from "./svg/search_24px.inline.svg";
import qrIcon from "./svg/qr_code_24px.inline.svg";
import settingsIcon from "./svg/settings_black_24dp.inline.svg";
import warningIcon from "./svg/warning_24px.inline.svg";
@@ -55,7 +56,7 @@ type PageLocation<DynamicPart extends object> = {
function replaceAll(
pattern: string,
vars: Record<string, string>,
- values: Record<string, any>,
+ values: Record<string, string>,
): string {
let result = pattern;
for (const v in vars) {
@@ -75,16 +76,20 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
`page definition pattern ${pattern} doesn't have any parameter`,
);
- const vars = patternParams.reduce((prev, cur) => {
- const pName = cur.match(/(\w+)/g);
+ const vars = patternParams.reduce(
+ (prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
- //skip things like :? in the path pattern
- if (!pName || !pName[0]) return prev;
- const name = pName[0];
- return { ...prev, [name]: cur };
- }, {} as Record<string, string>);
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ },
+ {} as Record<string, string>,
+ );
- const f = (values: T): string => replaceAll(pattern, vars, values ?? {});
+ const f = (values: T): string =>
+ replaceAll(pattern, vars, (values ?? {}) as Record<string, string>);
f.pattern = pattern;
return f;
}
@@ -95,6 +100,9 @@ export const Pages = {
balanceHistory: pageDefinition<{ currency?: string }>(
"/balance/history/:currency?",
),
+ searchHistory: pageDefinition<{ currency?: string }>(
+ "/search/history/:currency?",
+ ),
balanceDeposit: pageDefinition<{ amount: string }>(
"/balance/deposit/:amount",
),
@@ -125,9 +133,10 @@ export const Pages = {
ctaPayTemplate: "/cta/pay/template",
ctaRecovery: "/cta/recovery",
ctaRefund: "/cta/refund",
- ctaTips: "/cta/tip",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
"/cta/invoice/create/:amount?",
),
@@ -146,16 +155,14 @@ const talerUriActionToPageName: {
} = {
[TalerUriAction.Withdraw]: "ctaWithdraw",
[TalerUriAction.Pay]: "ctaPay",
- [TalerUriAction.Reward]: "ctaTips",
[TalerUriAction.Refund]: "ctaRefund",
[TalerUriAction.PayPull]: "ctaInvoicePay",
[TalerUriAction.PayPush]: "ctaTransferPickup",
[TalerUriAction.Restore]: "ctaRecovery",
[TalerUriAction.PayTemplate]: "ctaPayTemplate",
[TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
- [TalerUriAction.DevExperiment]: undefined,
- [TalerUriAction.Exchange]: undefined,
- [TalerUriAction.Auditor]: undefined,
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
};
export function getPathnameForTalerURI(talerUri: string): string | undefined {
@@ -260,7 +267,7 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<Fragment />
)}
- <EnabledBySettings name="advanceMode">
+ <EnabledBySettings name="advancedMode">
<a href={Pages.dev} class={path === "dev" ? "active" : ""}>
<i18n.Translate>Dev tools</i18n.Translate>
</a>
@@ -269,6 +276,13 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
>
+ <a href={Pages.searchHistory({})}>
+ <SvgIcon
+ title={i18n.str`Search transactions`}
+ dangerouslySetInnerHTML={{ __html: searchIcon }}
+ color="white"
+ />
+ </a>
<a href={Pages.qr}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index d3733e6cc..6dd577b88 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -14,9 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, WalletBalance } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index.js";
+import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import {
+ TableWithRoundRows as TableWithRoundedRows
+} from "./styled/index.js";
export function BalanceTable({
balances,
@@ -26,29 +28,37 @@ export function BalanceTable({
goToWalletHistory: (currency: string) => void;
}): VNode {
return (
- <TableWithRoundedRows>
- {balances.map((entry, idx) => {
- const av = Amounts.parseOrThrow(entry.available);
+ <Fragment>
+ <TableWithRoundedRows>
+ {balances.map((entry, idx) => {
+ const av = Amounts.parseOrThrow(entry.available);
- return (
- <tr
- key={idx}
- onClick={() => goToWalletHistory(av.currency)}
- style={{ cursor: "pointer" }}
- >
- <td>{av.currency}</td>
- <td
- style={{
- fontSize: "2em",
- textAlign: "right",
- width: "100%",
- }}
+ return (
+ <tr
+ key={idx}
+ onClick={() => goToWalletHistory(av.currency)}
+ style={{ cursor: "pointer" }}
>
- {Amounts.stringifyValue(av, 2)}
- </td>
- </tr>
- );
- })}
- </TableWithRoundedRows>
+ <td>{av.currency}</td>
+ <td
+ style={{
+ fontSize: "2em",
+ textAlign: "right",
+ width: "100%",
+ }}
+ >
+ {Amounts.stringifyValue(av, 2)}
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {entry.scopeInfo.type === ScopeType.Exchange ||
+ entry.scopeInfo.type === ScopeType.Auditor
+ ? entry.scopeInfo.url
+ : undefined}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </TableWithRoundedRows>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index 9fd117b08..8b6377fc5 100644
--- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -21,11 +21,9 @@ import {
segwitMinAmount,
stringifyPaytoUri,
TranslatedString,
- WithdrawalExchangeAccountDetails
+ WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { CopiedIcon, CopyIcon } from "../svg/index.js";
@@ -36,75 +34,94 @@ import { Button } from "../mui/Button.js";
export interface BankDetailsProps {
subject: string;
amount: AmountJson;
- accounts: WithdrawalExchangeAccountDetails[],
+ accounts: WithdrawalExchangeAccountDetails[];
}
export function BankDetailsByPaytoType({
subject,
amount,
- accounts,
+ accounts: unsortedAccounts,
}: BankDetailsProps): VNode {
const { i18n } = useTranslationContext();
- const [index, setIndex] = useState(0)
- const [currency, setCurrency] = useState(amount.currency)
- if (!accounts.length) {
- return <div>the exchange account list is empty</div>
+ const [index, setIndex] = useState(0);
+
+ if (!unsortedAccounts.length) {
+ return <div>the exchange account list is empty</div>;
}
+
+ const accounts = unsortedAccounts.sort((a, b) => {
+ return (b.priority ?? 0) - (a.priority ?? 0);
+ });
+
const selectedAccount = accounts[index];
- const altCurrency = selectedAccount.currencySpecification?.name
+ const altCurrency = selectedAccount.currencySpecification?.name;
const payto = parsePaytoUri(selectedAccount.paytoUri);
if (!payto) return <Fragment />;
- payto.params["amount"] = currency === altCurrency ? selectedAccount.transferAmount! :Amounts.stringify(amount) ;
+ payto.params["amount"] = altCurrency
+ ? selectedAccount.transferAmount!
+ : Amounts.stringify(amount);
payto.params["message"] = subject;
+ function Frame({
+ title,
+ children,
+ }: {
+ title: TranslatedString;
+ children: ComponentChildren;
+ }): VNode {
+ return (
+ <section
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ width: "100%",
+ justifyContent: "space-between",
+ }}
+ >
+ <p style={{ marginTop: 0 }}>{title}</p>
+ <div></div>
+ </div>
- function Frame({ title, children }: { title: TranslatedString, children: ComponentChildren }): VNode {
- return <section
- style={{
- textAlign: "left",
- border: "solid 1px black",
- padding: 8,
- borderRadius: 4,
- }}
- >
- <div style={{ display: "flex", width: "100%", justifyContent: "space-between" }}>
- <p style={{ marginTop: 0 }}>
- {title}
- </p>
- {accounts.length > 1 ?
- <Button variant="contained"
- onClick={async () => {
- setIndex((index + 1) % accounts.length)
- }}
- >
- <i18n.Translate>Next</i18n.Translate>
- </Button>
- : undefined}
- </div>
+ {children}
- {children}
+ {accounts.length > 1 ? (
+ <Fragment>
+ {accounts.map((ac, acIdx) => {
+ const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ return (
+ <Button
+ key={acIdx}
+ variant={acIdx === index ? "contained" : "outlined"}
+ onClick={async () => {
+ setIndex(acIdx);
+ }}
+ >
+ {accountLabel} (
+ {ac.currencySpecification?.name ?? amount.currency})
+ </Button>
+ );
+ })}
- {altCurrency ?
- <Fragment>
- <Button variant={currency === amount.currency ? "contained" : "outlined"}
- onClick={async () => {
- setCurrency(amount.currency)
- }}
- >
- <i18n.Translate>{amount.currency}</i18n.Translate>
- </Button>
- <Button variant={currency === altCurrency ? "contained" : "outlined"}
+ {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
onClick={async () => {
setCurrency(altCurrency)
}}
>
<i18n.Translate>{altCurrency}</i18n.Translate>
- </Button>
- </Fragment>
- : undefined}
- </section>
+ </Button> */}
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
}
if (payto.isKnown && payto.targetType === "bitcoin") {
@@ -160,7 +177,9 @@ export function BankDetailsByPaytoType({
}
const accountPart = !payto.isKnown ? (
- <Row name={i18n.str`Account`} value={payto.targetPath} />
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.targetPath} />
+ </Fragment>
) : payto.targetType === "x-taler-bank" ? (
<Fragment>
<Row name={i18n.str`Bank host`} value={payto.host} />
@@ -175,51 +194,90 @@ export function BankDetailsByPaytoType({
</Fragment>
) : undefined;
- const receiver = payto.params["receiver"] || undefined;
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
return (
<Frame title={i18n.str`Bank transfer details`}>
<table>
- {accountPart}
- {currency === altCurrency ? <Fragment>
- <Row
- name={i18n.str`Amount`}
- value={<Amount value={selectedAccount.transferAmount!} />}
- />
- <Row
- name={i18n.str`Converted`}
- value={<Amount value={amount} />}
- />
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in
+ your banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row name={i18n.str`Subject`} value={subject} literal />
- </Fragment> :
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ If you don't already have it in your banking favourites list,
+ then copy and paste this IBAN and the name into the receiver
+ fields in your banking app or website
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
+ {receiver ? (
+ <Row name={i18n.str`Receiver name`} value={receiver} />
+ ) : undefined}
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Finish the wire transfer setting the amount in your banking app
+ or website, then this withdrawal will proceed automatically.
+ </i18n.Translate>
+ </td>
+ </tr>
<Row
name={i18n.str`Amount`}
- value={<Amount value={amount} />}
+ value={
+ <Amount
+ value={altCurrency ? selectedAccount.transferAmount! : amount}
+ hideCurrency
+ />
+ }
/>
- }
- <Row name={i18n.str`Subject`} value={subject} literal />
- {receiver ? (
- <Row name={i18n.str`Receiver name`} value={receiver} />
- ) : undefined}
- </table>
- <table>
- <tbody>
+
<tr>
- <td>
- <pre>
- <b>
- <a
- target="_bank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- Payto URI
- </a>
- </b>
- </pre>
+ <td colSpan={3}>
+ <WarningBox style={{ margin: 0 }}>
+ <span>
+ <i18n.Translate>
+ Make sure ALL data is correct, including the subject;
+ otherwise, the money will not arrive in this wallet. You can
+ use the copy buttons (<CopyIcon />) to prevent typing errors
+ or the "payto://" URI below to copy just one value.
+ </i18n.Translate>
+ </span>
+ </WarningBox>
</td>
- <td width="100%" style={{ wordBreak: "break-all" }}>
- {stringifyPaytoUri(payto)}
+ </tr>
+
+ <tr>
+ <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can use
+ this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
</td>
<td>
<CopyButton getContent={() => stringifyPaytoUri(payto)} />
@@ -227,14 +285,6 @@ export function BankDetailsByPaytoType({
</tr>
</tbody>
</table>
- <p>
- <WarningBox>
- <i18n.Translate>
- Make sure to use the correct subject, otherwise the money will not
- arrive in this wallet.
- </i18n.Translate>
- </WarningBox>
- </p>
</Frame>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/Checkbox.tsx b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
index 70dfab597..ec1b93a01 100644
--- a/packages/taler-wallet-webextension/src/components/Checkbox.tsx
+++ b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
@@ -31,6 +31,7 @@ export function Checkbox({
label,
description,
}: Props): VNode {
+
return (
<div>
<input
diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
index 0a53d33ba..06c8a81ef 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
@@ -18,15 +18,18 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import arrowDown from "../svg/chevron-down.inline.svg";
import { ErrorBox } from "./styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function ErrorMessage({
title,
description,
}: {
title: TranslatedString;
- description?: string | VNode;
+ description?: string | VNode | Error;
}): VNode | null {
const [showErrorDetail, setShowErrorDetail] = useState(false);
+ const [showMore, setShowMore] = useState(false);
+ const { i18n } = useTranslationContext();
return (
<ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}>
<div>
@@ -44,7 +47,14 @@ export function ErrorMessage({
</button>
)}
</div>
- {showErrorDetail && <p>{description}</p>}
+ {showErrorDetail && description && <p>
+ {description instanceof Error && !showMore ? description.message : description.toString()}
+ {description instanceof Error && <div>
+ <a href="#" onClick={(e) => {
+ setShowMore(!showMore)
+ e.preventDefault()
+ }}>{showMore ? i18n.str`show less` : i18n.str`show more`} </a> </div>}
+ </p>}
</ErrorBox>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index 72881c746..9be9326b2 100644
--- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -23,6 +23,8 @@ import {
TransactionType,
WithdrawalType,
TransactionMajorState,
+ DenomLossEventType,
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -134,23 +136,6 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
- case TransactionType.Reward:
- return (
- <Layout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.merchantBaseUrl).hostname}
- timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
- iconPath={"T"}
- currentState={tx.txState.major}
- description={
- tx.txState.major === TransactionMajorState.Pending
- ? i18n.str`Grabbing the tipping...`
- : undefined
- }
- />
- );
case TransactionType.Refresh:
return (
<Layout
@@ -168,13 +153,16 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
- case TransactionType.Deposit:
+ case TransactionType.Deposit:{
+ const payto = parsePaytoUri(tx.targetPaytoUri);
+ const title = payto === undefined || !payto.isKnown ? tx.targetPaytoUri :
+ payto.params["receiver-name"] ;
return (
<Layout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
- title={tx.targetPaytoUri}
+ title={title}
timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
iconPath={"D"}
currentState={tx.txState.major}
@@ -185,6 +173,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
+ }
case TransactionType.PeerPullCredit:
return (
<Layout
@@ -253,6 +242,58 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
+ case TransactionType.DenomLoss: {
+ switch (tx.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination expired`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination vanished`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination unoffered`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ default: {
+ assertUnreachable(tx.lossEventType);
+ }
+ }
+ break;
+ }
+ case TransactionType.Recoup:
+ throw Error("recoup transaction not implemented");
default: {
assertUnreachable(tx);
}
@@ -267,12 +308,12 @@ function Layout(props: LayoutProps): VNode {
style={{
backgroundColor:
props.currentState === TransactionMajorState.Pending ||
- props.currentState === TransactionMajorState.Dialog
+ props.currentState === TransactionMajorState.Dialog
? "lightcyan"
: props.currentState === TransactionMajorState.Failed
? "#ff000040"
: props.currentState === TransactionMajorState.Aborted ||
- props.currentState === TransactionMajorState.Aborting
+ props.currentState === TransactionMajorState.Aborting
? "#00000010"
: "inherit",
alignItems: "center",
diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx
index 11fa72181..f8c0f1651 100644
--- a/packages/taler-wallet-webextension/src/components/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -18,7 +18,7 @@ import { styled } from "@linaria/react";
import { ComponentChildren, h, VNode } from "preact";
import { ButtonHandler } from "../mui/handlers.js";
import closeIcon from "../svg/close_24px.inline.svg";
-import { Link, LinkPrimary, LinkWarning } from "./styled/index.js";
+import { Link } from "./styled/index.js";
interface Props {
children: ComponentChildren;
@@ -52,40 +52,44 @@ const Body = styled.div`
export function Modal({ title, children, onClose }: Props): VNode {
return (
- <FullSize onClick={onClose?.onClick}>
- <div
- onClick={(e) => e.stopPropagation()}
- style={{
- background: "white",
- width: 600,
- height: "80%",
- margin: "auto",
- borderRadius: 8,
- padding: 8,
- // overflow: "scroll",
- }}
- >
- <Header>
- <div>
- <h2>{title}</h2>
- </div>
- <Link onClick={onClose?.onClick}>
- <div
- style={{
- height: 24,
- width: 24,
- marginLeft: 4,
- marginRight: 4,
- // fill: "white",
- }}
- dangerouslySetInnerHTML={{ __html: closeIcon }}
- />
- </Link>
- </Header>
- <hr />
+ <div style={{ top: 0, width: "100%", height: "100%" }}>
- <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
- </div>
- </FullSize>
+ <FullSize onClick={onClose?.onClick}>
+ <div
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ background: "white",
+ width: 600,
+ height: "80%",
+ margin: "auto",
+ borderRadius: 8,
+ padding: 8,
+ zIndex: 100,
+ // overflow: "scroll",
+ }}
+ >
+ <Header>
+ <div>
+ <h2>{title}</h2>
+ </div>
+ <Link onClick={onClose?.onClick}>
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ // fill: "white",
+ }}
+ dangerouslySetInnerHTML={{ __html: closeIcon }}
+ />
+ </Link>
+ </Header>
+ <hr />
+
+ <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
+ </div>
+ </FullSize>
+ </div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index b95bbf3b7..2fb03308b 100644
--- a/packages/taler-wallet-webextension/src/components/Part.tsx
+++ b/packages/taler-wallet-webextension/src/components/Part.tsx
@@ -19,14 +19,15 @@ import {
stringifyPaytoUri,
TranslatedString,
} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
+import arrowDown from "../svg/chevron-down.inline.svg";
import {
ExtraLargeText,
LargeText,
- SmallBoldText,
- SmallLightText,
+ SmallBoldText
} from "./styled/index.js";
export type Kind = "positive" | "negative" | "neutral";
@@ -96,11 +97,8 @@ const CollasibleBox = styled.div`
}
}
`;
-import arrowDown from "../svg/chevron-down.inline.svg";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-export function PartCollapsible({ text, title, big, showSign }: Props): VNode {
- const Text = big ? ExtraLargeText : LargeText;
+export function PartCollapsible({ text, title }: Props): VNode {
const [collapsed, setCollapsed] = useState(true);
return (
diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
index 8cb1c49dd..7fa0376c9 100644
--- a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -17,25 +17,24 @@
import {
AmountJson,
Amounts,
- PayMerchantInsufficientBalanceDetails,
+ PaymentInsufficientBalanceDetails,
PreparePayResult,
PreparePayResultType,
TranslatedString,
parsePayUri,
- stringifyPayUri,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
import { Amount } from "./Amount.js";
import { Part } from "./Part.js";
import { QR } from "./QR.js";
import { LinkSuccess, WarningBox } from "./styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Button } from "../mui/Button.js";
-import { ButtonHandler } from "../mui/handlers.js";
-import { assertUnreachable } from "../utils/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
interface Props {
payStatus: PreparePayResult;
@@ -81,47 +80,46 @@ export function PaymentButtons({
case "age-acceptable": {
BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
payStatus.balanceDetails.balanceAgeAcceptable,
- )} ${amount.currency} to pay for contracts restricted for age above ${
- payStatus.contractTerms.minimum_age
- } years old`;
+ )} ${amount.currency} to pay for this contract which is restricted.`;
break;
}
case "available": {
BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
payStatus.balanceDetails.balanceAvailable,
- )} ${amount.currency} available.`;
+ )} ${amount.currency} available.`;
break;
}
case "merchant-acceptable": {
BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
- payStatus.balanceDetails.balanceMerchantAcceptable,
- )} ${
- amount.currency
- } . To know more you can check which exchange and auditors the merchant trust.`;
+ payStatus.balanceDetails.balanceReceiverAcceptable,
+ )} ${amount.currency
+ } . To know more you can check which exchange and auditors the merchant trust.`;
break;
}
case "merchant-depositable": {
BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
- payStatus.balanceDetails.balanceMerchantDepositable,
- )} ${
- amount.currency
- } . To know more you can check which wire methods the merchant accepts.`;
+ payStatus.balanceDetails.balanceReceiverDepositable,
+ )} ${amount.currency
+ } . To know more you can check which wire methods the merchant accepts.`;
break;
}
case "material": {
BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
payStatus.balanceDetails.balanceMaterial,
- )} ${
- amount.currency
- } to spend right know. There are some coins that need to be refreshed.`;
+ )} ${amount.currency
+ } to spend right know. There are some coins that need to be refreshed.`;
break;
}
case "fee-gap": {
BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue(
- payStatus.balanceDetails.feeGapEstimate,
- )} ${
- amount.currency
- } more balance in your wallet or ask your merchant to cover more of the fees.`;
+ Amounts.stringify(
+ Amounts.sub(
+ amount,
+ payStatus.balanceDetails.maxEffectiveSpendAmount,
+ ).amount,
+ ),
+ )} ${amount.currency
+ } more balance in your wallet or ask your merchant to cover more of the fees.`;
break;
}
default:
@@ -188,6 +186,9 @@ function PayWithMobile({ uri }: { uri: string }): VNode {
setShowQR(undefined);
}
}
+ if (!payUri) {
+ return <Fragment />
+ }
return (
<section>
<LinkSuccess upperCased onClick={sharePrivatePaymentURI}>
@@ -217,7 +218,7 @@ type NoEnoughBalanceReason =
| "fee-gap";
function getReason(
- info: PayMerchantInsufficientBalanceDetails,
+ info: PaymentInsufficientBalanceDetails,
): NoEnoughBalanceReason {
if (Amounts.cmp(info.amountRequested, info.balanceAvailable) > 0) {
return "available";
@@ -228,10 +229,10 @@ function getReason(
if (Amounts.cmp(info.amountRequested, info.balanceAgeAcceptable) > 0) {
return "age-acceptable";
}
- if (Amounts.cmp(info.amountRequested, info.balanceMerchantAcceptable) > 0) {
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverAcceptable) > 0) {
return "merchant-acceptable";
}
- if (Amounts.cmp(info.amountRequested, info.balanceMerchantDepositable) > 0) {
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverDepositable) > 0) {
return "merchant-depositable";
}
return "fee-gap";
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
index 372ca7cb7..c94010ede 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -42,7 +42,10 @@ interface Props extends JSX.HTMLAttributes {
*/
const cache = { tx: [] as Transaction[] };
-export function PendingTransactions({ goToTransaction, goToURL }: Props): VNode {
+export function PendingTransactions({
+ goToTransaction,
+ goToURL,
+}: Props): VNode {
const api = useBackendContext();
const state = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.GetTransactions, {}),
@@ -59,8 +62,8 @@ export function PendingTransactions({ goToTransaction, goToURL }: Props): VNode
!state || state.hasError
? cache.tx
: state.response.transactions.filter(
- (t) => t.txState.major === TransactionMajorState.Pending,
- );
+ (t) => t.txState.major === TransactionMajorState.Pending,
+ );
if (state && !state.hasError) {
cache.tx = transactions;
@@ -87,50 +90,52 @@ export function PendingTransactionsView({
transactions: Transaction[];
}): VNode {
const { i18n } = useTranslationContext();
- const kycTransaction = transactions.find(tx => tx.kycUrl)
+ const kycTransaction = transactions.find((tx) => tx.kycUrl);
if (kycTransaction) {
- return <div
- style={{
- backgroundColor: "lightcyan",
- display: "flex",
- justifyContent: "center",
- }}
- >
- <Banner
- titleHead={i18n.str`KYC requirement`}
+ return (
+ <div
style={{
- backgroundColor: "lightred",
- maxHeight: 150,
- padding: 8,
- flexGrow: 1,
- maxWidth: 500,
- overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ backgroundColor: "#fff3cd",
+ color: "#664d03",
+ display: "flex",
+ justifyContent: "center",
}}
>
- <Grid
- container
- item
- xs={1}
- wrap="nowrap"
- role="button"
- spacing={1}
- alignItems="center"
- onClick={() => {
- goToURL(kycTransaction.kycUrl ?? "#")
+ <Banner
+ titleHead={i18n.str`KYC requirement`}
+ style={{
+ backgroundColor: "lightred",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1, //#fff3cd //#ffecb5
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
}}
>
- <Grid item>
- <Typography inline bold>
- One or more transaction require a KYC step to complete
- </Typography>
+ <Grid
+ container
+ item
+ xs={1}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToURL(kycTransaction.kycUrl ?? "#");
+ }}
+ >
+ <Grid item>
+ <Typography inline bold>
+ One or more transaction require a KYC step to complete
+ </Typography>
+ </Grid>
</Grid>
-
- </Grid>
- </Banner>
- </div>
+ </Banner>
+ </div>
+ );
}
- if (!goToTransaction) return <Fragment />
+ if (!goToTransaction) return <Fragment />;
return (
<div
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
index 555b300c2..0e23d5850 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -42,14 +42,12 @@ const cd: WalletContractData = {
"0YA1WETV15R6K8QKS79QA3QMT16010F42Q49VSKYQ71HVQKAG0A4ZJCA4YTKHE9EA5SP156TJSKZEJJJ87305N6PS80PC48RNKYZE08",
orderId: "2022.220-0281XKKB8W7YE",
summary: "w",
- maxWireFee: "ARS:1" as AmountString,
payDeadline: {
t_s: 1660002673,
},
refundDeadline: {
t_s: 1660002673,
},
- wireFeeAmortization: 1,
allowedExchanges: [
{
exchangeBaseUrl: "https://exchange.taler.ar/",
@@ -83,7 +81,7 @@ export const ShowingSimpleOrder = tests.createExample(ShowView, {
contractTerms: cd,
});
export const Error = tests.createExample(ErrorView, {
- proposalId: "asd",
+ transactionId: "asd",
error: {
hasError: true,
message: "message",
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
index 0b3cca0b2..e655def39 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -17,6 +17,7 @@ import {
AbsoluteTime,
Duration,
Location,
+ TransactionIdStr,
WalletContractData,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -83,7 +84,7 @@ export namespace States {
}
export interface Error {
status: "error";
- proposalId: string;
+ transactionId: string;
error: HookError;
hideHandler: ButtonHandler;
}
@@ -99,17 +100,17 @@ export namespace States {
}
interface Props {
- proposalId: string;
+ transactionId: TransactionIdStr;
}
-function useComponentState({ proposalId }: Props): State {
+function useComponentState({ transactionId }: Props): State {
const api = useBackendContext();
const [show, setShow] = useState(false);
const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(async () => {
if (!show) return undefined;
return await api.wallet.call(WalletApiOperation.GetContractTermsDetails, {
- proposalId,
+ transactionId,
});
}, [show]);
@@ -127,7 +128,7 @@ function useComponentState({ proposalId }: Props): State {
}
if (!hook) return { status: "loading", hideHandler };
if (hook.hasError)
- return { status: "error", proposalId, error: hook, hideHandler };
+ return { status: "error", transactionId, error: hook, hideHandler };
if (!hook.response) return { status: "loading", hideHandler };
return {
status: "show",
@@ -160,16 +161,17 @@ export function LoadingView({ hideHandler }: States.Loading): VNode {
export function ErrorView({
hideHandler,
error,
- proposalId,
+ transactionId,
}: States.Error): VNode {
const { i18n } = useTranslationContext();
return (
<Modal title="Full detail" onClose={hideHandler}>
<ErrorAlertView
error={alertFromError(
+ i18n,
i18n.str`Could not load purchase proposal details`,
error,
- { proposalId },
+ { transactionId },
)}
/>
</Modal>
@@ -336,8 +338,8 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
!contractTerms.autoRefund
? Duration.getZero()
: Duration.fromTalerProtocolDuration(
- contractTerms.autoRefund,
- ),
+ contractTerms.autoRefund,
+ ),
)}
format="dd MMMM yyyy, HH:mm"
/>
@@ -383,20 +385,6 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
<Amount value={contractTerms.maxDepositFee} />
</td>
</tr>
- <tr>
- <td>
- <i18n.Translate>Max fee</i18n.Translate>
- </td>
- <td>
- <Amount value={contractTerms.maxWireFee} />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Minimum age</i18n.Translate>
- </td>
- <td>{contractTerms.minimumAge}</td>
- </tr>
{/* <tr>
<td>Extra</td>
<td>
@@ -405,12 +393,6 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
</tr> */}
<tr>
<td>
- <i18n.Translate>Wire fee amortization</i18n.Translate>
- </td>
- <td>{contractTerms.wireFeeAmortization}</td>
- </tr>
- <tr>
- <td>
<i18n.Translate>Exchanges</i18n.Translate>
</td>
<td>
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
index b089e17a6..1585e3992 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
@@ -14,11 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangeListItem } from "@gnu-taler/taler-util";
+import { ComponentChildren } from "preact";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
-import { ToggleHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { SelectFieldHandler, ToggleHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { ErrorAlertView } from "../CurrentAlerts.js";
import { useComponentState } from "./state.js";
import { TermsState } from "./utils.js";
@@ -27,7 +27,6 @@ import {
ShowButtonsNonAcceptedTosView,
ShowTosContentView,
} from "./views.js";
-import { ComponentChildren } from "preact";
export interface Props {
exchangeUrl: string;
@@ -62,6 +61,8 @@ export namespace State {
status: "show-content";
termsAccepted: ToggleHandler;
showingTermsOfService?: ToggleHandler;
+ tosLang: SelectFieldHandler;
+ tosFormat: SelectFieldHandler;
}
export interface ShowButtonsAccepted extends BaseInfo {
status: "show-buttons-accepted";
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
index ed4715301..76524f0f4 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
@@ -23,12 +23,24 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js";
import { buildTermsOfServiceState } from "./utils.js";
+const supportedFormats = {
+ "text/html": "HTML",
+ "text/xml" : "XML",
+ "text/markdown" : "Markdown",
+ "text/plain" : "Plain text",
+ "text/pdf" : "PDF",
+}
+
export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, children }: Props): State {
const api = useBackendContext();
const [showContent, setShowContent] = useState<boolean>(!!readOnly);
- const { i18n } = useTranslationContext();
+ const { i18n, lang } = useTranslationContext();
+ const [tosLang, setTosLang] = useState<string>()
const { pushAlertOnError } = useAlertContext();
+ const [format, setFormat] = useState("text/html")
+
+ const acceptedLang = tosLang ?? lang
/**
* For the exchange selected, bring the status of the terms of service
*/
@@ -37,14 +49,20 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
WalletApiOperation.GetExchangeTos,
{
exchangeBaseUrl: exchangeUrl,
- acceptedFormat: ["text/xml"],
+ acceptedFormat: [format],
+ acceptLanguage: acceptedLang,
},
);
+ const supportedLangs = exchangeTos.tosAvailableLanguages.reduce((prev, cur) => {
+ prev[cur] = cur
+ return prev;
+ }, {} as Record<string, string>)
+
const state = buildTermsOfServiceState(exchangeTos);
- return { state };
- }, []);
+ return { state, supportedLangs };
+ }, [acceptedLang, format]);
if (!terms) {
return {
@@ -56,12 +74,13 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load the status of the term of service`,
terms,
),
};
}
- const { state } = terms.response;
+ const { state, supportedLangs } = terms.response;
async function onUpdate(accepted: boolean): Promise<void> {
if (!state) return;
@@ -69,14 +88,9 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
if (accepted) {
await api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
exchangeBaseUrl: exchangeUrl,
- etag: state.version,
});
} else {
// mark as not accepted
- await api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
- exchangeBaseUrl: exchangeUrl,
- etag: undefined,
- });
}
terms?.retry()
}
@@ -121,6 +135,20 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
terms: state,
showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
termsAccepted: base.termsAccepted,
+ tosFormat: {
+ onChange: pushAlertOnError(async (s) => {
+ setFormat(s)
+ }),
+ list: supportedFormats,
+ value: format ?? ""
+ },
+ tosLang: {
+ onChange: pushAlertOnError(async (s) => {
+ setTosLang(s)
+ }),
+ list: supportedLangs,
+ value: tosLang ?? lang
+ }
};
}
//showing buttons
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
index c578774ed..a28729eae 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
@@ -20,10 +20,40 @@
*/
import * as tests from "@gnu-taler/web-util/testing";
-// import { ReadyView } from "./views.js";
+import { ShowTosContentView } from "./views.js";
+import { ExchangeTosStatus } from "@gnu-taler/taler-util";
export default {
title: "TermsOfService",
};
-// export const Ready = tests.createExample(ReadyView, {});
+export const Ready = tests.createExample(ShowTosContentView, {
+ tosLang: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ tosFormat: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ terms: {
+ content: {
+ type: "plain",
+ content: "hola"
+ },
+ status: ExchangeTosStatus.Accepted,
+ version: "1"
+ },
+ status: "show-content",
+ termsAccepted: {
+ button: {},
+ }
+});
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
index fdca78ee5..96e268689 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
@@ -46,8 +46,7 @@ function parseTermsOfServiceContent(
}
} else if (type === "text/html") {
try {
- const href = new URL(text);
- return { type: "html", href };
+ return { type: "html", html: text };
} catch (e) {
logger.error("error parsing url", e);
}
@@ -90,7 +89,7 @@ export interface TermsDocumentXml {
export interface TermsDocumentHtml {
type: "html";
- href: URL;
+ html: string;
}
export interface TermsDocumentPlain {
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
index 3a9f9e85d..40cfba3bc 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
@@ -15,18 +15,20 @@
*/
import { ExchangeTosStatus } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { CheckboxOutlined } from "../../components/CheckboxOutlined.js";
import { ExchangeXmlTos } from "../../components/ExchangeToS.js";
import {
+ Input,
LinkSuccess,
TermsOfServiceStyle,
- WarningBox,
- WarningText,
+ WarningBox
} from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
+import { SelectList } from "../SelectList.js";
+import { EnabledBySettings } from "../EnabledBySettings.js";
export function ShowButtonsAcceptedTosView({
termsAccepted,
@@ -120,6 +122,8 @@ export function ShowTosContentView({
termsAccepted,
showingTermsOfService,
terms,
+ tosLang,
+ tosFormat,
}: State.ShowContent): VNode {
const { i18n } = useTranslationContext();
const ableToReviewTermsOfService =
@@ -127,6 +131,25 @@ export function ShowTosContentView({
return (
<section>
+ <Input style={{ display: "flex", justifyContent: "end" }}>
+ <EnabledBySettings name="selectTosFormat">
+ <SelectList
+ label={i18n.str`Format`}
+ list={tosFormat.list}
+ name="format"
+ value={tosFormat.value}
+ onChange={tosFormat.onChange}
+ />
+ </EnabledBySettings>
+ <SelectList
+ label={i18n.str`Language`}
+ list={tosLang.list}
+ name="lang"
+ value={tosLang.value}
+ onChange={tosLang.onChange}
+ />
+ </Input>
+
{!terms.content && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
<WarningBox>
@@ -164,7 +187,7 @@ export function ShowTosContentView({
</div>
))}
{terms.content.type === "html" && (
- <iframe src={terms.content.href.toString()} />
+ <iframe style={{ width: "100%" }} srcDoc={terms.content.html} />
)}
{terms.content.type === "pdf" && (
<a href={terms.content.location.toString()} download="tos.pdf">
diff --git a/packages/taler-wallet-webextension/src/components/Time.tsx b/packages/taler-wallet-webextension/src/components/Time.tsx
index 7ec91d56c..eee295756 100644
--- a/packages/taler-wallet-webextension/src/components/Time.tsx
+++ b/packages/taler-wallet-webextension/src/components/Time.tsx
@@ -18,6 +18,11 @@ import { AbsoluteTime } from "@gnu-taler/taler-util";
import { formatISO, format } from "date-fns";
import { h, VNode } from "preact";
+/**
+ *
+ * @deprecated use web-util
+ * @returns
+ */
export function Time({
timestamp,
format: formatString,
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
new file mode 100644
index 000000000..a77a69fa6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -0,0 +1,1050 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ NotificationType,
+ ObservabilityEventType,
+ RequestProgressNotification,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TaskProgressNotification,
+ WalletNotification,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { Button } from "../mui/Button.js";
+import { TextField } from "../mui/TextField.js";
+import { SafeHandler } from "../mui/handlers.js";
+import { WxApiType } from "../wxApi.js";
+import { WalletActivityTrack } from "../wxBackend.js";
+import { Modal } from "./Modal.js";
+import { Time } from "./Time.js";
+
+const OPEN_ACTIVITY_HEIGHT_PX = 250;
+const CLOSE_ACTIVITY_HEIGHT_PX = 40;
+
+export function WalletActivity(): VNode {
+ const { i18n } = useTranslationContext();
+ const [, updateSettings] = useSettings();
+
+ const [collapsed, setCollcapsed] = useState(true);
+
+ useEffect(() => {
+ document.body.style.marginBottom = `${
+ collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX
+ }px`;
+ return () => {
+ document.body.style.marginBottom = "0px";
+ };
+ }, [collapsed]);
+
+ const [table, setTable] = useState<"tasks" | "events">("events");
+ if (collapsed) {
+ return (
+ <div
+ style={{
+ position: "fixed",
+ bottom: 0,
+ background: "lightgrey",
+ zIndex: 1,
+ height: CLOSE_ACTIVITY_HEIGHT_PX,
+ overflowY: "scroll",
+ width: "100%",
+ }}
+ onClick={() => {
+ setCollcapsed(!collapsed);
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-around",
+ marginTop: 10,
+ cursor: "pointer",
+ }}
+ >
+ click here to open
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div
+ style={{
+ position: "fixed",
+ bottom: 0,
+ background: "lightgrey",
+ zIndex: 1,
+ height: OPEN_ACTIVITY_HEIGHT_PX,
+ overflowY: "scroll",
+ width: "100%",
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-around",
+ cursor: "pointer",
+ }}
+ onClick={() => {
+ setCollcapsed(!collapsed);
+ }}
+ >
+ <Button
+ variant={table === "events" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("events");
+ }}
+ >
+ <i18n.Translate>Events</i18n.Translate>
+ </Button>
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("tasks");
+ }}
+ >
+ <i18n.Translate>Active tasks</i18n.Translate>
+ </Button>
+
+ <Button
+ variant="outlined"
+ style={{ margin: 4 }}
+ onClick={async () => {
+ updateSettings("showWalletActivity", false);
+ }}
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ </div>
+ <div
+ style={{
+ backgroundColor: "white",
+ }}
+ >
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
+ }
+ })()}
+ </div>
+ </div>
+ );
+}
+
+interface MoreInfoPRops {
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ onClick: (content: VNode) => void;
+}
+
+function ShowBalanceChange({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BalanceChange) return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.hintTransactionId}
+ href={Pages.balanceTransaction({ tid: not.hintTransactionId })}
+ >
+ {not.hintTransactionId.substring(0, 10)}
+ </a>
+ </dd>
+ </Fragment>
+ );
+}
+
+function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BackupOperationError) return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ const error = not.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.error.code]}
+ </a>
+ </dd>
+ </Fragment>
+ );
+}
+
+function ShowTransactionStateTransition({
+ events,
+ onClick,
+}: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.TransactionStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Old state</dt>
+ <dd>
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
+ </dd>
+ <dt>New state</dt>
+ <dd>
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
+ </dd>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.transactionId}
+ href={Pages.balanceTransaction({ tid: not.transactionId })}
+ >
+ {not.transactionId.substring(0, 10)}
+ </a>
+ </dd>
+ {not.errorInfo ? (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (!not.errorInfo) return;
+ e.preventDefault();
+ const error = not.errorInfo;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Message</dt>
+ <dd>{error.message ?? "--"}</dd>
+ </dl>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.errorInfo.code]}
+ </a>
+ </dd>
+ </Fragment>
+ ) : undefined}
+ <dt>Experimental</dt>
+ <dd>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
+ </dd>
+ </Fragment>
+ );
+}
+function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.ExchangeStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Exchange</dt>
+ <dd>{not.exchangeBaseUrl}</dd>
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeEntryStatus !==
+ not.oldExchangeState?.exchangeEntryStatus && (
+ <Fragment>
+ <dt>Entry status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeEntryStatus} to{" "}
+ {not.newExchangeState.exchangeEntryStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeUpdateStatus !==
+ not.oldExchangeState?.exchangeUpdateStatus && (
+ <Fragment>
+ <dt>Update status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeUpdateStatus} to{" "}
+ {not.newExchangeState.exchangeUpdateStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && (
+ <Fragment>
+ <dt>Tos status</dt>
+ <dd>
+ from {not.oldExchangeState.tosStatus} to{" "}
+ {not.newExchangeState.tosStatus}
+ </dd>
+ </Fragment>
+ )}
+ </Fragment>
+ );
+}
+
+type ObservaNotifWithTime = (
+ | TaskProgressNotification
+ | RequestProgressNotification
+) & {
+ when: AbsoluteTime;
+};
+function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
+ // let prev: ObservaNotifWithTime;
+ const asd = events.map((not, idx) => {
+ if (
+ not.type !== NotificationType.RequestObservabilityEvent &&
+ not.type !== NotificationType.TaskObservabilityEvent
+ )
+ return <Fragment />;
+
+ const title = (function () {
+ switch (not.event.type) {
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess:
+ case ObservabilityEventType.HttpFetchStart:
+ return "HTTP Request";
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.DbQueryStart:
+ return "Database";
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.RequestStart:
+ return "Wallet";
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.CryptoStart:
+ return "Crypto";
+ case ObservabilityEventType.TaskStart:
+ return "Task start";
+ case ObservabilityEventType.TaskStop:
+ return "Task stop";
+ case ObservabilityEventType.TaskReset:
+ return "Task reset";
+ case ObservabilityEventType.ShepherdTaskResult:
+ return "Schedule";
+ case ObservabilityEventType.DeclareTaskDependency:
+ return "Task dependency";
+ case ObservabilityEventType.Message:
+ return "Message";
+ }
+ })();
+
+ return (
+ <ShowObervavilityDetails
+ key={idx}
+ title={title}
+ notif={not}
+ onClick={onClick}
+ />
+ );
+ });
+ return (
+ <table>
+ <thead>
+ <td>Event</td>
+ <td>Info</td>
+ <td>Start</td>
+ <td>End</td>
+ </thead>
+ <tbody>{asd}</tbody>
+ </table>
+ );
+}
+
+function ShowObervavilityDetails({
+ title,
+ notif,
+ onClick,
+ prev,
+}: {
+ title: string;
+ notif: ObservaNotifWithTime;
+ prev?: ObservaNotifWithTime;
+ onClick: (content: VNode) => void;
+}): VNode {
+ switch (notif.event.type) {
+ case ObservabilityEventType.HttpFetchStart:
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.url}{" "}
+ {prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishSuccess ? (
+ `(${prev.event.status})`
+ ) : prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishError ? (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ if (
+ prev.event.type !==
+ ObservabilityEventType.HttpFetchFinishError
+ )
+ return;
+ const error = prev.event.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </dd>
+ </dl>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ fail
+ </a>
+ ) : undefined}
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.location} {notif.event.name}
+ </td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.TaskReset: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.taskId}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.ShepherdTaskResult: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.resultType}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.operation}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.type}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.Message:
+ // FIXME
+ return <></>;
+ }
+}
+
+function refresh(
+ api: WxApiType,
+ onUpdate: (list: WalletActivityTrack[]) => void,
+ filter: string,
+) {
+ api.background
+ .call("getNotifications", { filter })
+ .then((notif) => {
+ onUpdate(notif);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+}
+
+export function ObservabilityEventsTable(): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+
+ const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]);
+ const [showDetails, setShowDetails] = useState<VNode>();
+ const [filter, onChangeFilter] = useState("");
+
+ useEffect(() => {
+ let lastTimeout: ReturnType<typeof setTimeout>;
+ function periodicRefresh() {
+ refresh(api, setNotifications, filter);
+
+ lastTimeout = setTimeout(() => {
+ periodicRefresh();
+ }, 1000);
+
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }
+ return periodicRefresh();
+ }, [filter]);
+
+ return (
+ <div>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <TextField
+ label="Filter"
+ variant="outlined"
+ value={filter}
+ onChange={onChangeFilter}
+ />
+ <div
+ style={{
+ padding: 4,
+ margin: 2,
+ border: "solid 1px black",
+ alignSelf: "center",
+ }}
+ onClick={() => {
+ api.background.call("clearNotifications", undefined).then(() => {
+ refresh(api, setNotifications, filter);
+ });
+ }}
+ >
+ clear
+ </div>
+ </div>
+ {showDetails && (
+ <Modal
+ title="event details"
+ onClose={{
+ onClick: (async () => {
+ setShowDetails(undefined);
+ }) as SafeHandler<void>,
+ }}
+ >
+ {showDetails}
+ </Modal>
+ )}
+ {notifications.map((not) => {
+ return (
+ <details key={not.id}>
+ <summary>
+ <div
+ style={{
+ width: "90%",
+ display: "inline-flex",
+ justifyContent: "space-between",
+ padding: 4,
+ }}
+ >
+ <div style={{ padding: 4 }}>
+ {(() => {
+ switch (not.type) {
+ case NotificationType.BalanceChange:
+ return i18n.str`Balance change`;
+ case NotificationType.BackupOperationError:
+ return i18n.str`Backup failed`;
+ case NotificationType.TransactionStateTransition:
+ return i18n.str`Transaction updated`;
+ case NotificationType.ExchangeStateTransition:
+ return i18n.str`Exchange updated`;
+ case NotificationType.Idle:
+ return i18n.str`Idle`;
+ case NotificationType.TaskObservabilityEvent:
+ return i18n.str`task.${
+ (not.events[0] as TaskProgressNotification).taskId
+ }`;
+ case NotificationType.RequestObservabilityEvent:
+ return i18n.str`wallet.${
+ (not.events[0] as RequestProgressNotification)
+ .operation
+ }(${
+ (not.events[0] as RequestProgressNotification)
+ .requestId
+ })`;
+ case NotificationType.WithdrawalOperationTransition: {
+ return `---`;
+ }
+ default: {
+ assertUnreachable(not.type);
+ }
+ }
+ })()}
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
+ </div>
+ </summary>
+ {(() => {
+ switch (not.type) {
+ case NotificationType.BalanceChange: {
+ return (
+ <ShowBalanceChange
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.BackupOperationError: {
+ return (
+ <ShowBackupOperationError
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.TransactionStateTransition: {
+ return (
+ <ShowTransactionStateTransition
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.ExchangeStateTransition: {
+ return (
+ <ShowExchangeStateTransition
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.Idle: {
+ return <div>not implemented</div>;
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ return (
+ <ShowObservabilityEvent
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ return (
+ <ShowObservabilityEvent
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.WithdrawalOperationTransition: {
+ return <div>not implemented</div>;
+ }
+ }
+ })()}
+ </details>
+ );
+ })}
+ </div>
+ );
+}
+
+function ErroDetailModal({
+ error,
+ onClose,
+}: {
+ error: TalerErrorDetail;
+ onClose: () => void;
+}): VNode {
+ return (
+ <Modal
+ title="Full detail"
+ onClose={{
+ onClick: onClose as SafeHandler<void>,
+ }}
+ >
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Modal>
+ );
+}
+
+export function ActiveTasksTable(): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const state = useAsyncAsHook(() => {
+ return api.wallet.call(WalletApiOperation.GetActiveTasks, {});
+ });
+ const [showError, setShowError] = useState<TalerErrorDetail>();
+ const tasks = state && !state.hasError ? state.response.tasks : [];
+
+ useEffect(() => {
+ if (!state || state.hasError) return;
+ const lastTimeout = setTimeout(() => {
+ state.retry();
+ }, 1000);
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }, [tasks]);
+
+ return (
+ <Fragment>
+ {showError && (
+ <ErroDetailModal
+ error={showError}
+ onClose={async () => {
+ setShowError(undefined);
+ }}
+ />
+ )}
+
+ <table style={{ width: "100%" }}>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Id</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Since</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Next try</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Error</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Transaction</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {tasks.map((task) => {
+ const [type, id] = task.taskId.split(":");
+ return (
+ <tr key={id}>
+ <td>{type}</td>
+ <td title={id}>{id.substring(0, 10)}</td>
+ <td>
+ <Time
+ timestamp={task.firstTry}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time timestamp={task.nextTry} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {!task.lastError?.code ? (
+ ""
+ ) : (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowError(task.lastError);
+ }}
+ >
+ {TalerErrorCode[task.lastError.code]}
+ </a>
+ )}
+ </td>
+ <td>
+ {task.transaction ? (
+ <a
+ title={task.transaction}
+ href={Pages.balanceTransaction({ tid: task.transaction })}
+ >
+ {task.transaction.substring(0, 10)}
+ </a>
+ ) : (
+ "--"
+ )}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 2501c61c8..739b71064 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -35,7 +35,6 @@ export const WalletAction = styled.div`
align-items: center;
margin: auto;
- height: 100%;
& h1:first-child {
margin-top: 0;
@@ -691,6 +690,16 @@ export const SmallBoldText = styled.div`
font-weight: bold;
`;
+export const AgeSign = styled.div<{size:number}>`
+ display: inline-block;
+ border: red solid 1px;
+ border-radius: 100%;
+ width: ${({ size }: {size:number}) => (`${size}px`)};
+ height: ${({ size }: {size:number}) => (`${size}px`)};
+ line-height: ${({ size }: {size:number}) => (`${size}px`)};
+ padding: 3px;
+`;
+
export const LargeText = styled.div`
font-size: large;
`;
diff --git a/packages/taler-wallet-webextension/src/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts
index 1ae15f1ec..e30fdd72c 100644
--- a/packages/taler-wallet-webextension/src/context/alert.ts
+++ b/packages/taler-wallet-webextension/src/context/alert.ts
@@ -19,13 +19,22 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useState } from "preact/hooks";
import { HookError } from "../hooks/useAsyncAsHook.js";
import { SafeHandler, withSafe } from "../mui/handlers.js";
import { BackgroundError } from "../wxApi.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { platform } from "../platform/foreground.js";
export type AlertType = "info" | "warning" | "error" | "success";
@@ -102,24 +111,24 @@ export const AlertProvider = ({ children }: Props): VNode => {
setAlerts((ns: AlertWithDate[]) => ns.filter((n) => n !== alert));
};
+ const { i18n } = useTranslationContext();
+
function pushAlertOnError<T>(
handler: (p: T) => Promise<void>,
): SafeHandler<T> {
return withSafe(handler, (e) => {
- const a = alertFromError(e.message as TranslatedString, e);
+ const a = alertFromError(i18n, e.message as TranslatedString, e);
pushAlert(a);
});
}
- const { i18n } = useTranslationContext();
-
function safely<T>(
name: string,
handler: (p: T) => Promise<void>,
): SafeHandler<T> {
const message = i18n.str`Error was thrown trying to: "${name}"`;
return withSafe(handler, (e) => {
- const a = alertFromError(message, e);
+ const a = alertFromError(i18n, message, e);
pushAlert(a);
});
}
@@ -133,24 +142,28 @@ export const AlertProvider = ({ children }: Props): VNode => {
export const useAlertContext = (): Type => useContext(Context);
export function alertFromError(
+ i18n: InternationalizationAPI,
message: TranslatedString,
error: HookError,
...context: any[]
): ErrorAlert;
export function alertFromError(
+ i18n: InternationalizationAPI,
message: TranslatedString,
error: Error,
...context: any[]
): ErrorAlert;
export function alertFromError(
+ i18n: InternationalizationAPI,
message: TranslatedString,
error: TalerErrorDetail,
...context: any[]
): ErrorAlert;
export function alertFromError(
+ i18n: InternationalizationAPI,
message: TranslatedString,
error: HookError | TalerErrorDetail | Error,
...context: any[]
@@ -170,14 +183,33 @@ export function alertFromError(
//HookError
description = error.message as TranslatedString;
if (error.type === "taler") {
+ const msg = isWalletNotAvailable(i18n, error.details);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.details);
+ if (msg2) {
+ description = msg2;
+ }
+ }
cause = {
details: error.details,
};
}
} else {
if (error instanceof BackgroundError) {
- description = (error.errorDetail.hint ??
- `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ const msg = isWalletNotAvailable(i18n, error.errorDetail);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.errorDetail);
+ if (msg2) {
+ description = msg2;
+ } else {
+ description = (error.errorDetail.hint ??
+ `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ }
+ }
cause = {
details: error.errorDetail,
stack: error.stack,
@@ -202,3 +234,44 @@ export function alertFromError(
context,
};
}
+
+function isWalletNotAvailable(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE &&
+ detail.lastError
+ ) {
+ const le = detail.lastError as TalerErrorDetail;
+ if (le.code === TalerErrorCode.WALLET_DB_UNAVAILABLE) {
+ if (platform.isFirefox() && platform.runningOnPrivateMode()) {
+ return i18n.str`Could not open the wallet database. Firefox is known to run into this problem under "permanent private mode".`;
+ } else {
+ return i18n.str`Could not open the wallet database.`;
+ }
+ } else {
+ return (detail.hint ?? `Error code: ${detail.code}`) as TranslatedString;
+ }
+ }
+ return undefined;
+}
+
+function isHttpError(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ detail.errorResponse
+ ) {
+ const er = detail.errorResponse as TalerErrorDetail;
+ return (
+ (er.hint as TranslatedString) ??
+ detail.hint ??
+ i18n.str`Unexpected request error, code: ${er.code}`
+ );
+ }
+ return undefined;
+}
+//
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
index ec0106f6e..efcef8c28 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
@@ -48,7 +48,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the status of deposit`,
info,
),
};
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
index c352e394e..c683a755c 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
@@ -15,12 +15,10 @@
*/
import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import { SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
diff --git a/packages/taler-wallet-webextension/src/cta/Reward/index.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
index 5e56db7bc..ec09fd9f1 100644
--- a/packages/taler-wallet-webextension/src/cta/Reward/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
@@ -14,71 +14,60 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
-import { AcceptedView, IgnoredView, ReadyView } from "./views.js";
+import { InsertLostView, InsertPendingRefreshView, UnknownView } from "./views.js";
export interface Props {
- talerTipUri?: string;
+ talerExperimentUri: string | undefined;
onCancel: () => Promise<void>;
- onSuccess: (tx: string) => Promise<void>;
+ onSuccess: () => Promise<void>;
}
-export type State =
- | State.Loading
- | State.LoadingUriError
- | State.Ignored
- | State.Accepted
- | State.Ready
- | State.Ignored;
+export type State = State.Loading | State.LoadingUriError | State.Unknown | State.InsertLost | State.PendingRefresh;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
-
export interface LoadingUriError {
status: "error";
error: ErrorAlert;
}
-
- export interface BaseInfo {
- merchantBaseUrl: string;
- amount: AmountJson;
- exchangeBaseUrl: string;
+ export interface InsertLost {
+ status: "insertLost";
error: undefined;
- cancel: ButtonHandler;
- }
-
- export interface Ignored extends BaseInfo {
- status: "ignored";
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
}
-
- export interface Accepted extends BaseInfo {
- status: "accepted";
+ export interface PendingRefresh {
+ status: "pendingRefresh";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
}
- export interface Ready extends BaseInfo {
- status: "ready";
- accept: ButtonHandler;
+ export interface Unknown {
+ status: "unknown";
+ experimentId: string;
+ error: undefined;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
- accepted: AcceptedView,
- ignored: IgnoredView,
- ready: ReadyView,
+ pendingRefresh: InsertPendingRefreshView,
+ insertLost: InsertLostView,
+ unknown: UnknownView,
};
-export const TipPage = compose(
- "Tip",
+export const DevExperimentPage = compose(
+ "DevExperiment",
(p: Props) => useComponentState(p),
viewMapping,
);
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
new file mode 100644
index 000000000..774a1129d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parseDevExperimentUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerExperimentUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+
+ async function doApply(): Promise<void> {
+ if (!talerExperimentUri) return;
+ await api.wallet.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: talerExperimentUri
+ })
+ // const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ // amount: Amounts.stringify(amount),
+ // depositPaytoUri: uri,
+ // });
+ onSuccess();
+ }
+ const uri = talerExperimentUri === undefined ? undefined : parseDevExperimentUri(talerExperimentUri);
+
+ if (!uri) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Invalid dev experiment URI.`,
+ description: i18n.str`URI: ${talerExperimentUri}`,
+ cause: {},
+ context: {},
+ },
+ };
+ }
+ if (uri.devExperimentId === "insert-denom-loss") {
+ return {
+ status: "insertLost",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ if (uri.devExperimentId === "insert-pending-refresh") {
+ return {
+ status: "pendingRefresh",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ return {
+ status: "unknown",
+ error: undefined,
+ experimentId: uri.devExperimentId,
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Reward/stories.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
index bd5fdefd9..c9851495f 100644
--- a/packages/taler-wallet-webextension/src/cta/Reward/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
@@ -19,28 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
-import { AcceptedView, ReadyView } from "./views.js";
+import { InsertLostView } from "./views.js";
export default {
- title: "tip",
+ title: "dev-experiment",
};
-export const Accepted = tests.createExample(AcceptedView, {
- status: "accepted",
+export const Ready = tests.createExample(InsertLostView, {
+ status: "insertLost",
+ confirm: {},
error: undefined,
- amount: Amounts.parseOrThrow("EUR:1"),
- exchangeBaseUrl: "",
- merchantBaseUrl: "",
-});
-
-export const Ready = tests.createExample(ReadyView, {
- status: "ready",
- error: undefined,
- amount: Amounts.parseOrThrow("EUR:1"),
- merchantBaseUrl: "http://merchant.url/",
- exchangeBaseUrl: "http://exchange.url/",
- accept: {},
- cancel: {},
});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
new file mode 100644
index 000000000..d4f2ca8b1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("DevExperiment CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
+ talerExperimentUri: undefined,
+ onCancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("error");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
new file mode 100644
index 000000000..afad17ad1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "../../components/Amount.js";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+export function InsertLostView(state: State.InsertLost): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Insert lost denomination`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function InsertPendingRefreshView(state: State.PendingRefresh): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Pending refresh`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function UnknownView(state: State.Unknown): VNode {
+ return <div>unknown experiment "{state.experimentId}"</div>
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
index 81caf9878..daa3ee76d 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -50,7 +50,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the list of exchanges`,
hook,
),
};
@@ -103,7 +104,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the invoice status`,
hook,
),
};
@@ -166,8 +168,8 @@ export function useComponentState({
subject === undefined
? undefined
: !subject
- ? "Can't be empty"
- : undefined,
+ ? "Can't be empty"
+ : undefined,
value: subject ?? "",
onInput: pushAlertOnError(async (e) => setSubject(e)),
},
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index c6d3e689c..e2c37fbba 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -14,27 +14,19 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
-import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
-import {
- SubTitle,
- SvgIcon,
- WalletAction,
-} from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
-import editIcon from "../../svg/edit_24px.inline.svg";
import {
ExchangeDetails,
getAmountWithFee,
InvoiceCreationDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-import { TermsOfService } from "../../components/TermsOfService/index.js";
export function ReadyView({
exchangeUrl,
@@ -43,7 +35,7 @@ export function ReadyView({
create,
toBeReceived,
requestAmount,
- doSelectExchange,
+ doSelectExchange: _doSelectExchange,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -62,10 +54,10 @@ export function ReadyView({
);
}
}
- async function _20DaysExpiration(): Promise<void> {
+ async function _30DaysExpiration(): Promise<void> {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
@@ -81,13 +73,13 @@ export function ReadyView({
}}
>
<i18n.Translate>Exchange</i18n.Translate>
- <Button onClick={doSelectExchange.onClick} variant="text">
+ {/* <Button onClick={doSelectExchange.onClick} variant="text">
<SvgIcon
title="Edit"
dangerouslySetInnerHTML={{ __html: editIcon }}
color="black"
/>
- </Button>
+ </Button> */}
</div>
}
text={<ExchangeDetails exchange={exchangeUrl} />}
@@ -135,9 +127,9 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index 8bae9470f..99de03d2d 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -23,10 +23,10 @@ import {
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useEffect } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js";
@@ -64,7 +64,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the transfer payment status`,
hook,
),
};
@@ -76,12 +77,8 @@ export function useComponentState({
// };
// }
- const {
- contractTerms,
- peerPullDebitId,
- amountEffective,
- amountRaw,
- } = hook.response.p2p;
+ const { contractTerms, transactionId, amountEffective, amountRaw } =
+ hook.response.p2p;
const amountStr: string = contractTerms.amount;
const amount = Amounts.parseOrThrow(amountStr);
@@ -155,7 +152,7 @@ export function useComponentState({
const resp = await api.wallet.call(
WalletApiOperation.ConfirmPeerPullDebit,
{
- peerPullDebitId,
+ transactionId,
},
);
onSuccess(resp.transactionId);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index 986b31d77..547d5ac9a 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -16,7 +16,6 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { Amount } from "../../components/Amount.js";
import { Part } from "../../components/Part.js";
import { PaymentButtons } from "../../components/PaymentButtons.js";
import { Time } from "../../components/Time.js";
@@ -35,7 +34,6 @@ export function ReadyView(
<Fragment>
<section style={{ textAlign: "left" }}>
<Part title={i18n.str`Subject`} text={<div>{summary}</div>} />
- <Part title={i18n.str`Amount`} text={<Amount value={raw} />} />
<Part
title={i18n.str`Details`}
text={
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index d171ecbac..4733e5aee 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -84,7 +84,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the payment and balance status`,
hook,
),
};
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index eee5fb684..d03f48746 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -57,9 +57,11 @@ export const NoEnoughBalanceAvailable = tests.createExample(BaseView, {
balanceAvailable: "USD:9" as AmountString,
balanceMaterial: "USD:9" as AmountString,
balanceAgeAcceptable: "USD:9" as AmountString,
- balanceMerchantAcceptable: "USD:9" as AmountString,
- balanceMerchantDepositable: "USD:9" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
@@ -97,9 +99,11 @@ export const NoEnoughBalanceMaterial = tests.createExample(BaseView, {
balanceAvailable: "USD:10" as AmountString,
balanceMaterial: "USD:9" as AmountString,
balanceAgeAcceptable: "USD:9" as AmountString,
- balanceMerchantAcceptable: "USD:9" as AmountString,
- balanceMerchantDepositable: "USD:0" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:0" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
@@ -137,9 +141,11 @@ export const NoEnoughBalanceAgeAcceptable = tests.createExample(BaseView, {
balanceAvailable: "USD:10" as AmountString,
balanceMaterial: "USD:10" as AmountString,
balanceAgeAcceptable: "USD:9" as AmountString,
- balanceMerchantAcceptable: "USD:9" as AmountString,
- balanceMerchantDepositable: "USD:9" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
@@ -178,9 +184,11 @@ export const NoEnoughBalanceMerchantAcceptable = tests.createExample(BaseView, {
balanceAvailable: "USD:10" as AmountString,
balanceMaterial: "USD:10" as AmountString,
balanceAgeAcceptable: "USD:10" as AmountString,
- balanceMerchantAcceptable: "USD:9" as AmountString,
- balanceMerchantDepositable: "USD:9" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
@@ -220,9 +228,11 @@ export const NoEnoughBalanceMerchantDepositable = tests.createExample(
balanceAvailable: "USD:10" as AmountString,
balanceMaterial: "USD:10" as AmountString,
balanceAgeAcceptable: "USD:10" as AmountString,
- balanceMerchantAcceptable: "USD:10" as AmountString,
- balanceMerchantDepositable: "USD:9" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
@@ -261,9 +271,11 @@ export const NoEnoughBalanceFeeGap = tests.createExample(BaseView, {
balanceAvailable: "USD:10" as AmountString,
balanceMaterial: "USD:10" as AmountString,
balanceAgeAcceptable: "USD:10" as AmountString,
- balanceMerchantAcceptable: "USD:10" as AmountString,
- balanceMerchantDepositable: "USD:10" as AmountString,
- feeGapEstimate: "USD:1" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:10" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
},
talerUri: "taler://pay/..",
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
index 5e009b3de..5847cc833 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
@@ -29,6 +29,7 @@ import {
PreparePayResultPaymentPossible,
PreparePayResultType,
ScopeType,
+ TransactionMajorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai";
@@ -549,8 +550,13 @@ describe("Payment CTA states", () => {
// expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(state.payHandler.onClick).not.undefined;
- handler.notifyEventFromWallet(
- NotificationType.TransactionStateTransition,
+ handler.notifyEventFromWallet({
+ type: NotificationType.TransactionStateTransition,
+ newTxState: {} as any,
+ oldTxState: {} as any,
+ transactionId: "123",
+ }
+
);
},
(state) => {
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index c00e570f9..68d161ab2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -21,18 +21,20 @@ import {
PreparePayResultType,
TranslatedString,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { Part } from "../../components/Part.js";
import { PaymentButtons } from "../../components/PaymentButtons.js";
-import { SuccessBox, WarningBox } from "../../components/styled/index.js";
+import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import {
- getAmountWithFee,
- MerchantDetails,
- PurchaseDetails,
-} from "../../wallet/Transaction.js";
+ AgeSign,
+ SuccessBox,
+ WarningBox,
+} from "../../components/styled/index.js";
+import { MerchantDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
type SupportedStates =
| State.Ready
@@ -58,7 +60,17 @@ export function BaseView(state: SupportedStates): VNode {
<section style={{ textAlign: "left" }}>
<Part
- title={i18n.str`Purchase`}
+ title={
+ contractTerms.minimum_age ? (
+ <Fragment>
+ <i18n.Translate>Purchase</i18n.Translate>
+ &nbsp;
+ <AgeSign size={20} title={i18n.str`This purchase is age restricted.`}>{contractTerms.minimum_age}+</AgeSign>
+ </Fragment>
+ ) : (
+ <i18n.Translate>Purchase</i18n.Translate>
+ )
+ }
text={contractTerms.summary as TranslatedString}
kind="neutral"
/>
@@ -67,29 +79,6 @@ export function BaseView(state: SupportedStates): VNode {
text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral"
/>
- <Part
- title={i18n.str`Details`}
- text={
- <PurchaseDetails
- price={getAmountWithFee(effective, state.amount, "debit")}
- info={{
- ...contractTerms,
- orderId: contractTerms.order_id,
- contractTermsHash: "",
- // products: contractTerms.products!,
- }}
- proposalId={state.payStatus.proposalId}
- />
- }
- kind="neutral"
- />
- {contractTerms.order_id && (
- <Part
- title={i18n.str`Receipt`}
- text={`#${contractTerms.order_id}` as TranslatedString}
- kind="neutral"
- />
- )}
{contractTerms.pay_deadline && (
<Part
title={i18n.str`Valid until`}
@@ -105,6 +94,13 @@ export function BaseView(state: SupportedStates): VNode {
/>
)}
</section>
+ <EnabledBySettings name="advancedMode">
+ <section style={{ textAlign: "left" }}>
+ <ShowFullContractTermPopup
+ transactionId={state.payStatus.transactionId}
+ />
+ </section>
+ </EnabledBySettings>
<PaymentButtons
amount={effective}
payStatus={state.payStatus}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
index f5a8c8814..1e903fe46 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
@@ -53,9 +53,9 @@ export namespace State {
export interface FillTemplate {
status: "fill-template";
error: undefined;
- currency: string;
amount?: AmountFieldHandler;
summary?: TextFieldHandler;
+ minAge: number;
onCreate: ButtonHandler;
}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
index 4a0b2911a..1a92c4073 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -14,14 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, PreparePayResult } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
export function useComponentState({
@@ -29,43 +30,32 @@ export function useComponentState({
cancel,
goToWalletManualWithdraw,
onSuccess,
-}: Props): State {
+}: Props): RecursiveState<State> {
const api = useBackendContext();
const { i18n } = useTranslationContext();
const { safely } = useAlertContext();
- const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined;
-
- const amountParam = !url
- ? undefined
- : url.searchParams.get("amount") ?? undefined;
- const summaryParam = !url
- ? undefined
- : url.searchParams.get("summary") ?? undefined;
+ // const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined;
+ // const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam);
+ // const currency = parsedAmount ? parsedAmount.currency : amountParam;
- const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam);
- const currency = parsedAmount ? parsedAmount.currency : amountParam;
+ // const initialAmount =
+ // parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined);
- const initialAmount =
- parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined);
- const [amount, setAmount] = useState(initialAmount);
- const [summary, setSummary] = useState(summaryParam);
const [newOrder, setNewOrder] = useState("");
const hook = useAsyncAsHook(async () => {
if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
- let payStatus;
- if (!amountParam && !summaryParam) {
- payStatus = await api.wallet.call(
- WalletApiOperation.PreparePayForTemplate,
- {
- talerPayTemplateUri: talerTemplateUri,
- templateParams: {},
- },
- );
+ const templateP = await api.wallet.call(
+ WalletApiOperation.CheckPayForTemplate, { talerPayTemplateUri: talerTemplateUri },
+ );
+ const requireMoreInfo = !templateP.templateDetails.template_contract.amount || !templateP.templateDetails.template_contract.summary;
+ let payStatus: PreparePayResult | undefined = undefined;
+ if (!requireMoreInfo) {
+ payStatus = await api.wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: talerTemplateUri });
}
const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
- return { payStatus, balance, uri: talerTemplateUri };
+ return { payStatus, balance, uri: talerTemplateUri, templateP };
}, []);
if (!hook) {
@@ -79,6 +69,7 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load the status of the order template`,
hook,
),
@@ -107,63 +98,89 @@ export function useComponentState({
};
}
- async function createOrder() {
- try {
- const templateParams: Record<string, string> = {};
- if (amount) {
- templateParams["amount"] = Amounts.stringify(amount);
+ return () => {
+ const cfg = hook.response.templateP.templateDetails.template_contract;
+ const def = hook.response.templateP.templateDetails.editable_defaults;
+
+ const fixedAmount = cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined;
+ const fixedSummary = cfg.summary !== undefined ? cfg.summary : undefined;
+
+ const defaultAmount = def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined;
+ const defaultSummary = def?.summary !== undefined ? def.summary : undefined;
+
+ const zero = fixedAmount ? Amounts.zeroOfAmount(fixedAmount) :
+ cfg.currency !== undefined ? Amounts.zeroOfCurrency(cfg.currency) :
+ defaultAmount !== undefined ? Amounts.zeroOfAmount(defaultAmount) :
+ def?.currency !== undefined ? Amounts.zeroOfCurrency(def.currency) :
+ Amounts.zeroOfCurrency(hook.response.templateP.supportedCurrencies[0]);
+
+ const [amount, setAmount] = useState(defaultAmount ?? zero);
+ const [summary, setSummary] = useState(defaultSummary ?? "");
+
+ async function createOrder() {
+ try {
+ const templateParams: Record<string, string> = {};
+ if (amount && !fixedAmount) {
+ templateParams["amount"] = Amounts.stringify(amount);
+ }
+ if (summary && !fixedSummary) {
+ templateParams["summary"] = summary;
+ }
+ const payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: talerTemplateUri,
+ templateParams,
+ },
+ );
+ setNewOrder(payStatus.talerUri!);
+ } catch (e) {
+ console.error(e);
}
- if (summary) {
- templateParams["summary"] = summary;
- }
- const payStatus = await api.wallet.call(
- WalletApiOperation.PreparePayForTemplate,
- {
- talerPayTemplateUri: talerTemplateUri,
- templateParams,
- },
- );
- setNewOrder(payStatus.talerUri!);
- } catch (e) {}
- }
- const errors = undefinedIfEmpty({
- amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
- summary: summary !== undefined && !summary ? i18n.str`required` : undefined,
- });
- return {
- status: "fill-template",
- error: undefined,
- currency: currency!, //currency is always not null
- amount:
- amount !== undefined
- ? ({
+ }
+
+ const errors = undefinedIfEmpty({
+ amount: fixedAmount !== undefined ? undefined : amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
+ summary: fixedSummary !== undefined ? undefined : summary !== undefined && !summary ? i18n.str`required` : undefined,
+ });
+ return {
+ status: "fill-template",
+ error: undefined,
+ minAge: cfg.minimum_age ?? 0,
+ amount:
+ fixedAmount === undefined
+ ? ({
onInput: (a) => {
setAmount(a);
},
value: amount,
error: errors?.amount,
} as AmountFieldHandler)
- : undefined,
- summary:
- summary !== undefined
- ? ({
+ : undefined,
+ summary:
+ fixedSummary === undefined
+ ? ({
onInput: (t) => {
setSummary(t);
},
value: summary,
error: errors?.summary,
} as TextFieldHandler)
- : undefined,
- onCreate: {
- onClick: errors
- ? undefined
- : safely("create order for pay template", createOrder),
- },
- };
+ : undefined,
+ onCreate: {
+ onClick: errors
+ ? undefined
+ : safely("create order for pay template", createOrder),
+ },
+ };
+ }
+
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, unknown>)[k] !== undefined,
+ )
? obj
: undefined;
}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
index 88658b5e1..ce53c3cf9 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
@@ -14,17 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js";
-import { Part } from "../../components/Part.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
import { State } from "./index.js";
+import { AgeSign } from "../../components/styled/index.js";
export function ReadyView({
- currency,
amount,
+ minAge,
summary,
onCreate,
}: State.FillTemplate): VNode {
@@ -67,6 +67,12 @@ export function ReadyView({
</p>
)}
</section>
+ {minAge && (
+ <section>
+ <AgeSign size={25}>{minAge}+</AgeSign>
+ <i18n.Translate>This purchase is age restricted.</i18n.Translate>
+ </section>
+ )}
<section>
<Button onClick={onCreate.onClick} variant="contained" color="success">
<i18n.Translate>Review order</i18n.Translate>
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/state.ts b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
index 6c0f37471..6f0a98151 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
@@ -72,7 +72,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the refund status`,
info,
),
};
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
index ef21a511e..ae4d728f3 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
@@ -29,7 +29,7 @@ export function IgnoredView(state: State.Ignored): VNode {
<Fragment>
<section>
<p>
- <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
+ <i18n.Translate>You&apos;ve ignored the refund.</i18n.Translate>
</p>
</section>
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/cta/Reward/state.ts b/packages/taler-wallet-webextension/src/cta/Reward/state.ts
deleted file mode 100644
index a71ad6acc..000000000
--- a/packages/taler-wallet-webextension/src/cta/Reward/state.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { Amounts } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { alertFromError, useAlertContext } from "../../context/alert.js";
-import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { Props, State } from "./index.js";
-
-export function useComponentState({
- talerTipUri: talerRewardUri,
- onCancel,
- onSuccess,
-}: Props): State {
- const api = useBackendContext();
- const { i18n } = useTranslationContext();
- const { pushAlertOnError } = useAlertContext();
- const tipInfo = useAsyncAsHook(async () => {
- if (!talerRewardUri) throw Error("ERROR_NO-URI-FOR-TIP");
- const tip = await api.wallet.call(WalletApiOperation.PrepareReward, {
- talerRewardUri,
- });
- return { tip };
- });
-
- if (!tipInfo) {
- return {
- status: "loading",
- error: undefined,
- };
- }
- if (tipInfo.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
- tipInfo,
- ),
- };
- }
- // if (tipInfo.hasError) {
- // return {
- // status: "loading-uri",
- // error: tipInfo,
- // };
- // }
-
- const { tip } = tipInfo.response;
-
- const doAccept = async (): Promise<void> => {
- const res = await api.wallet.call(WalletApiOperation.AcceptReward, {
- walletRewardId: tip.transactionId,
- });
-
- //FIX: this may not be seen since we are moving to the success also
- tipInfo.retry();
- onSuccess(res.transactionId);
- };
-
- const baseInfo = {
- merchantBaseUrl: tip.merchantBaseUrl,
- exchangeBaseUrl: tip.exchangeBaseUrl,
- amount: Amounts.parseOrThrow(tip.rewardAmountEffective),
- error: undefined,
- cancel: {
- onClick: pushAlertOnError(onCancel),
- },
- };
-
- if (tip.accepted) {
- return {
- status: "accepted",
- ...baseInfo,
- };
- }
-
- return {
- status: "ready",
- ...baseInfo,
- accept: {
- onClick: pushAlertOnError(doAccept),
- },
- };
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Reward/test.ts b/packages/taler-wallet-webextension/src/cta/Reward/test.ts
deleted file mode 100644
index 0e378f366..000000000
--- a/packages/taler-wallet-webextension/src/cta/Reward/test.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { AmountString, Amounts } from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
-import * as tests from "@gnu-taler/web-util/testing";
-import { nullFunction } from "../../mui/handlers.js";
-import { createWalletApiMock } from "../../test-utils.js";
-import { Props } from "./index.js";
-import { useComponentState } from "./state.js";
-
-describe("Tip CTA states", () => {
- it("should tell the user that the URI is missing", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- const props: Props = {
- talerTipUri: undefined,
- onCancel: nullFunction,
- onSuccess: nullFunction,
- };
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- ({ status, error }) => {
- expect(status).equals("loading");
- expect(error).undefined;
- },
- ({ status, error }) => {
- expect(status).equals("error");
- if (!error) expect.fail();
- expect(error.description).eq("ERROR_NO-URI-FOR-TIP");
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be ready for accepting the tip", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
- accepted: false,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- rewardAmountEffective: "EUR:1" as AmountString,
- walletRewardId: "tip_id",
- transactionId: "txn:tip:ABC1234",
- expirationTimestamp: {
- t_s: 1,
- },
- rewardAmountRaw: "EUR:0" as AmountString,
- });
-
- const props: Props = {
- talerTipUri: "taler://tip/asd",
- onCancel: nullFunction,
- onSuccess: nullFunction,
- };
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- ({ status, error }) => {
- expect(status).equals("loading");
- expect(error).undefined;
- },
- (state) => {
- if (state.status !== "ready") {
- expect(state).eq({ status: "ready" });
- return;
- }
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- if (state.accept.onClick === undefined) expect.fail();
-
- handler.addWalletCallResponse(WalletApiOperation.AcceptReward);
- state.accept.onClick();
-
- handler.addWalletCallResponse(
- WalletApiOperation.PrepareReward,
- undefined,
- {
- accepted: true,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- rewardAmountEffective: "EUR:1" as AmountString,
- walletRewardId: "tip_id",
- transactionId: "txn:tip:ABC1234",
- expirationTimestamp: {
- t_s: 1,
- },
- rewardAmountRaw: "EUR:0" as AmountString,
- },
- );
- },
- (state) => {
- if (state.status !== "accepted") expect.fail();
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it.skip("should be ignored after clicking the ignore button", async () => {
- const { handler, TestingContext } = createWalletApiMock();
- handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- rewardAmountEffective: "EUR:1" as AmountString,
- walletRewardId: "tip_id",
- transactionId: "txn:tip:ABC1234",
- accepted: false,
- expirationTimestamp: {
- t_s: 1,
- },
- rewardAmountRaw: "EUR:0" as AmountString,
- });
-
- const props: Props = {
- talerTipUri: "taler://tip/asd",
- onCancel: nullFunction,
- onSuccess: nullFunction,
- };
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- ({ status, error }) => {
- expect(status).equals("loading");
- expect(error).undefined;
- },
- (state) => {
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
-
- //FIXME: add ignore button
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should render accepted if the tip has been used previously", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
- accepted: true,
- exchangeBaseUrl: "exchange url",
- merchantBaseUrl: "merchant url",
- rewardAmountEffective: "EUR:1" as AmountString,
- walletRewardId: "tip_id",
- transactionId: "txn:tip:ABC1234",
- expirationTimestamp: {
- t_s: 1,
- },
- rewardAmountRaw: "EUR:0" as AmountString,
- });
-
- const props: Props = {
- talerTipUri: "taler://tip/asd",
- onCancel: nullFunction,
- onSuccess: nullFunction,
- };
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- ({ status, error }) => {
- expect(status).equals("loading");
- expect(error).undefined;
- },
- (state) => {
- if (state.status !== "accepted") expect.fail();
- if (state.error) expect.fail();
- expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
- expect(state.merchantBaseUrl).eq("merchant url");
- expect(state.exchangeBaseUrl).eq("exchange url");
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
-});
diff --git a/packages/taler-wallet-webextension/src/cta/Reward/views.tsx b/packages/taler-wallet-webextension/src/cta/Reward/views.tsx
deleted file mode 100644
index 3c3190a07..000000000
--- a/packages/taler-wallet-webextension/src/cta/Reward/views.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { Amount } from "../../components/Amount.js";
-import { LogoHeader } from "../../components/LogoHeader.js";
-import { Part } from "../../components/Part.js";
-import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Button } from "../../mui/Button.js";
-import { State } from "./index.js";
-import { TermsOfService } from "../../components/TermsOfService/index.js";
-
-export function IgnoredView(state: State.Ignored): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- <span>
- <i18n.Translate>You&apos;ve ignored the tip.</i18n.Translate>
- </span>
- </Fragment>
- );
-}
-
-export function ReadyView(state: State.Ready): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- <section>
- <p>
- <i18n.Translate>The merchant is offering you a tip</i18n.Translate>
- </p>
- <Part
- title={i18n.str`Amount`}
- text={<Amount value={state.amount} />}
- kind="positive"
- />
- <Part
- title={i18n.str`Merchant URL`}
- text={state.merchantBaseUrl as TranslatedString}
- kind="neutral"
- />
- <Part
- title={i18n.str`Exchange`}
- text={state.exchangeBaseUrl as TranslatedString}
- kind="neutral"
- />
- </section>
- <section>
- <TermsOfService key="terms" exchangeUrl={state.exchangeBaseUrl} >
- <Button
- variant="contained"
- color="success"
- onClick={state.accept.onClick}
- >
- <i18n.Translate>
- Receive &nbsp; {<Amount value={state.amount} />}
- </i18n.Translate>
- </Button>
- </TermsOfService>
- </section>
- </Fragment>
- );
-}
-
-export function AcceptedView(state: State.Accepted): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- <section>
- <i18n.Translate>
- Tip from <code>{state.merchantBaseUrl}</code> accepted. Check your
- transactions list for more details.
- </i18n.Translate>
- </section>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
index 297e8a56b..f092801ed 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -17,19 +17,18 @@
import {
AmountString,
Amounts,
- TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { isFuture, parse } from "date-fns";
-import { useEffect, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { Props, State } from "./index.js";
import { BackgroundError, WxApiType } from "../../wxApi.js";
+import { Props, State } from "./index.js";
export function useComponentState({
amount: amountStr,
@@ -59,7 +58,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the max amount to transfer`,
hook,
),
};
@@ -163,11 +163,14 @@ async function checkPeerPushDebitAndCheckMax(
const material = Amounts.parseOrThrow(
e.errorDetail.insufficientBalanceDetails.balanceMaterial,
);
- const gap = Amounts.parseOrThrow(
- e.errorDetail.insufficientBalanceDetails.feeGapEstimate,
- );
- const newAmount = Amounts.sub(material, gap).amount;
const amount = Amounts.parseOrThrow(amountState);
+ const gap = Amounts.sub(
+ amount,
+ Amounts.parseOrThrow(
+ e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount,
+ ),
+ ).amount;
+ const newAmount = Amounts.sub(material, gap).amount;
if (Amounts.cmp(newAmount, amount) === 0) {
//insufficient balance and the exception didn't give
//a good response that allow us to try again
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
index 8489b0643..bc855f33d 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
@@ -50,10 +50,10 @@ export function ReadyView({
);
}
}
- async function _20DaysExpiration() {
+ async function _30DaysExpiration() {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
@@ -100,9 +100,9 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
index 06ef80760..67f6d9113 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
@@ -20,9 +20,9 @@ import {
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js";
@@ -50,7 +50,8 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
- i18n.str`Could not load the status of the term of service`,
+ i18n,
+ i18n.str`Could not load the invoice payment status`,
hook,
),
};
@@ -58,10 +59,10 @@ export function useComponentState({
const {
contractTerms,
- peerPushCreditId,
+ transactionId,
amountEffective,
amountRaw,
- exchangeBaseUrl
+ exchangeBaseUrl,
} = hook.response;
const effective = Amounts.parseOrThrow(amountEffective);
@@ -73,7 +74,7 @@ export function useComponentState({
const resp = await api.wallet.call(
WalletApiOperation.ConfirmPeerPushCredit,
{
- peerPushCreditId,
+ transactionId,
},
);
onSuccess(resp.transactionId);
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index 04713f3c4..d33abffee 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -18,8 +18,7 @@ import {
AmountJson,
AmountString,
CurrencySpecification,
- ExchangeListItem,
- WithdrawalExchangeAccountDetails,
+ ExchangeListItem
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
@@ -38,7 +37,7 @@ import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { ErrorAlert } from "../../context/alert.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
-import { SelectAmountView, SuccessView } from "./views.js";
+import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@@ -60,6 +59,7 @@ export type State =
| SelectExchangeState.NoExchangeFound
| SelectExchangeState.Selecting
| State.SelectAmount
+ | State.AlreadyCompleted
| State.Success;
export namespace State {
@@ -80,6 +80,14 @@ export namespace State {
amount: AmountFieldHandler;
currency: string;
}
+ export interface AlreadyCompleted {
+ status: "already-completed";
+ operationState: "confirmed" | "aborted" | "selected";
+ thisWallet: boolean;
+ redirectToTx: () => void;
+ confirmTransferUrl?: string,
+ error: undefined;
+ }
export type Success = {
status: "success";
@@ -116,6 +124,7 @@ const viewMapping: StateViewMap<State> = {
"no-exchange-found": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
success: SuccessView,
+ "already-completed": FinalStateOperation,
};
export const WithdrawPageFromURI = compose(
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 7bff13e51..f592072ff 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -14,21 +14,21 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/* eslint-disable react-hooks/rules-of-hooks */
import {
AmountJson,
+ AmountString,
Amounts,
ExchangeFullDetails,
ExchangeListItem,
- ExchangeTosStatus,
- TalerError,
+ NotificationType,
+ TransactionMajorState,
parseWithdrawExchangeUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js";
@@ -44,6 +44,7 @@ export function useComponentStateFromParams({
const api = useBackendContext();
const { i18n } = useTranslationContext();
const paramsAmount = amount ? Amounts.parse(amount) : undefined;
+ const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
const uriInfoHook = useAsyncAsHook(async () => {
const exchanges = await api.wallet.call(
WalletApiOperation.ListExchanges,
@@ -52,12 +53,12 @@ export function useComponentStateFromParams({
const uri = maybeTalerUri
? parseWithdrawExchangeUri(maybeTalerUri)
: undefined;
- const exchangeByTalerUri = uri?.exchangeBaseUrl;
+ const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl;
+
let ex: ExchangeFullDetails | undefined;
- if (exchangeByTalerUri && uri.exchangePub) {
+ if (exchangeByTalerUri) {
await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchangeByTalerUri,
- masterPub: uri.exchangePub,
});
const info = await api.wallet.call(
WalletApiOperation.GetExchangeDetailedInfo,
@@ -79,6 +80,7 @@ export function useComponentStateFromParams({
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load the list of exchanges`,
uriInfoHook,
),
@@ -140,8 +142,8 @@ export function useComponentStateFromParams({
confirm: {
onClick: isValid
? pushAlertOnError(async () => {
- onAmountChanged(Amounts.stringify(amount));
- })
+ onAmountChanged(Amounts.stringify(amount));
+ })
: undefined,
},
amount: {
@@ -158,6 +160,7 @@ export function useComponentStateFromParams({
async function doManualWithdraw(
exchange: string,
ageRestricted: number | undefined,
+ amount: AmountString,
): Promise<{
transactionId: string;
confirmTransferUrl: string | undefined;
@@ -166,7 +169,7 @@ export function useComponentStateFromParams({
WalletApiOperation.AcceptManualWithdrawal,
{
exchangeBaseUrl: exchange,
- amount: Amounts.stringify(chosenAmount),
+ amount,
restrictAge: ageRestricted,
},
);
@@ -185,6 +188,7 @@ export function useComponentStateFromParams({
chosenAmount,
exchangeList,
exchangeByTalerUri,
+ setUpdatedExchangeByUser,
);
}
@@ -195,6 +199,8 @@ export function useComponentStateFromURI({
}: PropsFromURI): RecursiveState<State> {
const api = useBackendContext();
const { i18n } = useTranslationContext();
+
+ const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
/**
* Ask the wallet about the withdraw URI
*/
@@ -205,26 +211,65 @@ export function useComponentStateFromURI({
: maybeTalerUri;
const uriInfo = await api.wallet.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
{
talerWithdrawUri,
+ selectedExchange: updatedExchangeByUser,
},
);
- const { amount, defaultExchangeBaseUrl, possibleExchanges } = uriInfo;
+ const {
+ amount,
+ defaultExchangeBaseUrl,
+ possibleExchanges,
+ confirmTransferUrl,
+ status,
+ } = uriInfo.info;
+ const txInfo =
+ uriInfo.transactionId === undefined
+ ? undefined
+ : await api.wallet.call(WalletApiOperation.GetTransactionById, {
+ transactionId: uriInfo.transactionId,
+ });
return {
talerWithdrawUri,
+ status,
+ transactionId: uriInfo.transactionId,
+ txInfo: txInfo,
+ confirmTransferUrl,
amount: Amounts.parseOrThrow(amount),
thisExchange: defaultExchangeBaseUrl,
exchanges: possibleExchanges,
};
});
+ const readyToListen = uriInfoHook && !uriInfoHook.hasError;
+
+ useEffect(() => {
+ if (!uriInfoHook || uriInfoHook.hasError) {
+ return;
+ }
+ const txId = uriInfoHook.response.transactionId;
+
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ (notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === txId
+ ) {
+ uriInfoHook.retry();
+ }
+ },
+ );
+ }, [readyToListen]);
+
if (!uriInfoHook) return { status: "loading", error: undefined };
if (uriInfoHook.hasError) {
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load info from URI`,
uriInfoHook,
),
@@ -232,6 +277,7 @@ export function useComponentStateFromURI({
}
const uri = uriInfoHook.response.talerWithdrawUri;
+ const txId = uriInfoHook.response.transactionId;
const chosenAmount = uriInfoHook.response.amount;
const defaultExchange = uriInfoHook.response.thisExchange;
const exchangeList = uriInfoHook.response.exchanges;
@@ -239,26 +285,40 @@ export function useComponentStateFromURI({
async function doManagedWithdraw(
exchange: string,
ageRestricted: number | undefined,
+ amount: AmountString,
): Promise<{
transactionId: string;
confirmTransferUrl: string | undefined;
}> {
- const res = await api.wallet.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange,
- talerWithdrawUri: uri,
- restrictAge: ageRestricted,
- },
- );
+ if (!txId) {
+ throw Error("can't confirm transaction");
+ }
+ const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, {
+ exchangeBaseUrl: exchange,
+ amount,
+ restrictAge: ageRestricted,
+ transactionId: txId,
+ });
return {
confirmTransferUrl: res.confirmTransferUrl,
transactionId: res.transactionId,
};
}
- return () =>
- exchangeSelectionState(
+ if (uriInfoHook.response.txInfo && uriInfoHook.response.status !== "pending") {
+ const info = uriInfoHook.response.txInfo;
+ return {
+ status: "already-completed",
+ operationState: uriInfoHook.response.status,
+ confirmTransferUrl: uriInfoHook.response.confirmTransferUrl,
+ thisWallet: info.txState.major === TransactionMajorState.Pending,
+ redirectToTx: () => onSuccess(info.transactionId),
+ error: undefined,
+ };
+ }
+
+ return useCallback(() => {
+ return exchangeSelectionState(
doManagedWithdraw,
cancel,
onSuccess,
@@ -266,12 +326,15 @@ export function useComponentStateFromURI({
chosenAmount,
exchangeList,
defaultExchange,
+ setUpdatedExchangeByUser,
);
+ }, []);
}
type ManualOrManagedWithdrawFunction = (
exchange: string,
ageRestricted: number | undefined,
+ amount: AmountString,
) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>;
function exchangeSelectionState(
@@ -282,6 +345,7 @@ function exchangeSelectionState(
chosenAmount: AmountJson,
exchangeList: ExchangeListItem[],
exchangeSuggestedByTheBank: string | undefined,
+ onExchangeUpdated: (ex: string) => void,
): RecursiveState<State> {
const api = useBackendContext();
const selectedExchange = useSelectedExchange({
@@ -290,17 +354,32 @@ function exchangeSelectionState(
list: exchangeList,
});
+ const current =
+ selectedExchange.status !== "ready"
+ ? undefined
+ : selectedExchange.selected.exchangeBaseUrl;
+ useEffect(() => {
+ if (current) {
+ onExchangeUpdated(current);
+ }
+ }, [current]);
+
if (selectedExchange.status !== "ready") {
return selectedExchange;
}
- return (): State.Success | State.LoadingUriError | State.Loading => {
+ return useCallback(():
+ | State.Success
+ | State.LoadingUriError
+ | State.Loading => {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
const [ageRestricted, setAgeRestricted] = useState(0);
const currentExchange = selectedExchange.selected;
- const [selectedCurrency, setSelectedCurrency] = useState<string>(chosenAmount.currency)
+ const [selectedCurrency, setSelectedCurrency] = useState<string>(
+ chosenAmount.currency,
+ );
/**
* With the exchange and amount, ask the wallet the information
* about the withdrawal
@@ -323,13 +402,10 @@ function exchangeSelectionState(
return {
amount: withdrawAmount,
ageRestrictionOptions: info.ageRestrictionOptions,
- accounts: info.withdrawalAccountsList
+ accounts: info.withdrawalAccountsList,
};
}, []);
- const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
- undefined,
- );
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
async function doWithdrawAndCheckError(): Promise<void> {
@@ -338,6 +414,7 @@ function exchangeSelectionState(
const res = await doWithdraw(
currentExchange.exchangeBaseUrl,
!ageRestricted ? undefined : ageRestricted,
+ Amounts.stringify(chosenAmount),
);
if (res.confirmTransferUrl) {
document.location.href = res.confirmTransferUrl;
@@ -345,9 +422,9 @@ function exchangeSelectionState(
onSuccess(res.transactionId);
}
} catch (e) {
- if (e instanceof TalerError) {
- setWithdrawError(e);
- }
+ console.error(e);
+ // if (e instanceof TalerError) {
+ // }
}
setDoingWithdraw(false);
}
@@ -359,6 +436,7 @@ function exchangeSelectionState(
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load the withdrawal details`,
amountHook,
),
@@ -388,23 +466,34 @@ function exchangeSelectionState(
//TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled
? {
- list: ageRestrictionOptions,
- value: String(ageRestricted),
- onChange: pushAlertOnError(async (v: string) =>
- setAgeRestricted(parseInt(v, 10)),
- ),
- }
+ list: ageRestrictionOptions,
+ value: String(ageRestricted),
+ onChange: pushAlertOnError(async (v: string) =>
+ setAgeRestricted(parseInt(v, 10)),
+ ),
+ }
: undefined;
- const altCurrencies = amountHook.response.accounts.filter(a => !!a.currencySpecification).map(a => a.currencySpecification!.name)
- const chooseCurrencies = altCurrencies.length === 0 ? [] : [toBeReceived.currency, ...altCurrencies]
- const convAccount = amountHook.response.accounts.find(c => {
- return c.currencySpecification && c.currencySpecification.name === selectedCurrency
- })
- const conversionInfo = !convAccount ? undefined : ({
- spec: convAccount.currencySpecification!,
- amount: Amounts.parseOrThrow(convAccount.transferAmount!)
- })
+ const altCurrencies = amountHook.response.accounts
+ .filter((a) => !!a.currencySpecification)
+ .map((a) => a.currencySpecification!.name);
+ const chooseCurrencies =
+ altCurrencies.length === 0
+ ? []
+ : [toBeReceived.currency, ...altCurrencies];
+
+ const convAccount = amountHook.response.accounts.find((c) => {
+ return (
+ c.currencySpecification &&
+ c.currencySpecification.name === selectedCurrency
+ );
+ });
+ const conversionInfo = !convAccount
+ ? undefined
+ : {
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
return {
status: "success",
@@ -414,19 +503,20 @@ function exchangeSelectionState(
toBeReceived,
chooseCurrencies,
selectedCurrency,
- changeCurrency: (s) => { setSelectedCurrency(s) },
+ changeCurrency: (s) => {
+ setSelectedCurrency(s);
+ },
conversionInfo,
withdrawalFee,
chosenAmount,
talerWithdrawUri,
ageRestriction,
doWithdrawal: {
- onClick:
- doingWithdraw
- ? undefined
- : pushAlertOnError(doWithdrawAndCheckError),
+ onClick: doingWithdraw
+ ? undefined
+ : pushAlertOnError(doWithdrawAndCheckError),
},
cancel,
};
- };
+ }, []);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index a3127fafc..29f39054f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -23,7 +23,7 @@ import { CurrencySpecification, ExchangeListItem } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import { nullFunction } from "../../mui/handlers.js";
// import { TermsState } from "../../utils/index.js";
-import { SuccessView } from "./views.js";
+import { SuccessView, FinalStateOperation } from "./views.js";
export default {
title: "withdraw",
@@ -67,6 +67,23 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
chooseCurrencies: [],
});
+export const AlreadyAborted = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "aborted"
+});
+export const AlreadySelected = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "selected"
+});
+export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "confirmed"
+});
+
+
export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index 3493415d9..860cf1099 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -99,7 +99,7 @@ describe("Withdraw CTA states", () => {
expect(handler.getCallingQueueState()).eq("empty");
});
- it("should tell the user that there is not known exchange", async () => {
+ it.skip("should tell the user that there is not known exchange", async () => {
const { handler, TestingContext } = createWalletApiMock();
const props = {
talerWithdrawUri: "taler-withdraw://",
@@ -108,11 +108,16 @@ describe("Withdraw CTA states", () => {
};
handler.addWalletCallResponse(
- WalletApiOperation.GetWithdrawalDetailsForUri,
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
undefined,
{
- amount: "EUR:2" as AmountString,
- possibleExchanges: [],
+ transactionId: "123",
+ info: {
+ status: "pending",
+ operationId: "123",
+ amount: "EUR:2" as AmountString,
+ possibleExchanges: [],
+ }
},
);
@@ -135,7 +140,7 @@ describe("Withdraw CTA states", () => {
expect(handler.getCallingQueueState()).eq("empty");
});
- it("should be able to withdraw if tos are ok", async () => {
+ it.skip("should be able to withdraw if tos are ok", async () => {
const { handler, TestingContext } = createWalletApiMock();
const props = {
talerWithdrawUri: "taler-withdraw://",
@@ -144,12 +149,17 @@ describe("Withdraw CTA states", () => {
};
handler.addWalletCallResponse(
- WalletApiOperation.GetWithdrawalDetailsForUri,
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
undefined,
{
- amount: "ARS:2" as AmountString,
- possibleExchanges: exchanges,
- defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ transactionId: "123",
+ info: {
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
+ possibleExchanges: exchanges,
+ defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ }
},
);
handler.addWalletCallResponse(
@@ -217,6 +227,8 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "ARS:2" as AmountString,
possibleExchanges: exchangeWithNewTos,
defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
@@ -245,6 +257,8 @@ describe("Withdraw CTA states", () => {
WalletApiOperation.GetWithdrawalDetailsForUri,
undefined,
{
+ status: "pending",
+ operationId: "123",
amount: "ARS:2" as AmountString,
possibleExchanges: exchanges,
defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 748b65817..cdddd9bbc 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -14,26 +14,132 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, ExchangeTosStatus } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
+import { AmountField } from "../../components/AmountField.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js";
-import { Input, LinkSuccess, SvgIcon } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Input,
+ LinkSuccess,
+ SvgIcon,
+ WarningBox,
+} from "../../components/styled/index.js";
import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
import editIcon from "../../svg/edit_24px.inline.svg";
import {
ExchangeDetails,
- getAmountWithFee,
WithdrawDetails,
+ getAmountWithFee,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
-import { Grid } from "../../mui/Grid.js";
-import { AmountField } from "../../components/AmountField.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
+
+export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
+ const { i18n } = useTranslationContext();
+ // document.location.href = res.confirmTransferUrl
+ if (state.thisWallet) {
+ switch (state.operationState) {
+ case "confirmed": {
+ state.redirectToTx();
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been completed.
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ }
+ case "aborted": {
+ state.redirectToTx();
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been aborted
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ }
+ case "selected": {
+ if (state.confirmTransferUrl) {
+ document.location.href = state.confirmTransferUrl;
+ }
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has started and should be completed in the bank.
+ </i18n.Translate>
+ </div>
+ {state.confirmTransferUrl && (
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ You can confirm the operation in
+ </i18n.Translate>
+ &nbsp;
+ <a
+ target="_bank"
+ rel="noreferrer"
+ href={state.confirmTransferUrl}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ )}
+ </WarningBox>
+ );
+ }
+ }
+ }
+
+ switch (state.operationState) {
+ case "confirmed":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been completed by another wallet.
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ case "aborted":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been aborted
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ case "selected":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been used by another wallet.
+ </i18n.Translate>
+ </div>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>It can be confirmed in</i18n.Translate>&nbsp;
+ <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}>
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ </WarningBox>
+ );
+ }
+}
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
@@ -51,13 +157,15 @@ export function SuccessView(state: State.Success): VNode {
}}
>
<i18n.Translate>Exchange</i18n.Translate>
- <Button onClick={state.doSelectExchange.onClick} variant="text">
- <SvgIcon
- title="Edit"
- dangerouslySetInnerHTML={{ __html: editIcon }}
- color="black"
- />
- </Button>
+ <EnabledBySettings name="showExchangeManagement">
+ <Button onClick={state.doSelectExchange.onClick} variant="text">
+ <SvgIcon
+ title="Edit"
+ dangerouslySetInnerHTML={{ __html: editIcon }}
+ color="black"
+ />
+ </Button>
+ </EnabledBySettings>
</div>
}
text={
@@ -66,21 +174,31 @@ export function SuccessView(state: State.Success): VNode {
kind="neutral"
big
/>
- {state.chooseCurrencies.length > 0 ?
+ {state.chooseCurrencies.length > 0 ? (
<Fragment>
<p>
- {state.chooseCurrencies.map(currency => {
- return <Button variant={currency === state.selectedCurrency ? "contained" : "outlined"}
- onClick={async () => {
- state.changeCurrency(currency)
- }}
- >
- {currency}
- </Button>
+ {state.chooseCurrencies.map((currency) => {
+ return (
+ <Button
+ key={currency}
+ variant={
+ currency === state.selectedCurrency
+ ? "contained"
+ : "outlined"
+ }
+ onClick={async () => {
+ state.changeCurrency(currency);
+ }}
+ >
+ {currency}
+ </Button>
+ );
})}
</p>
</Fragment>
- : <Fragment />}
+ ) : (
+ <Fragment />
+ )}
<Part
title={i18n.str`Details`}
@@ -109,6 +227,7 @@ export function SuccessView(state: State.Success): VNode {
</section>
<section>
+ {/* <div> */}
<TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
<Button
variant="contained"
@@ -121,6 +240,20 @@ export function SuccessView(state: State.Success): VNode {
</i18n.Translate>
</Button>
</TermsOfService>
+ {/* </div>
+ <div style={{ marginTop: 20 }}>
+ <Button
+ variant="text"
+ color="success"
+
+ disabled={!state.doAbort.onClick}
+ onClick={state.doAbort.onClick}
+ >
+ <i18n.Translate>
+ Cancel
+ </i18n.Translate>
+ </Button>
+ </div> */}
</section>
{state.talerWithdrawUri ? (
<WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
@@ -158,7 +291,6 @@ function WithdrawWithMobile({
}
export function SelectAmountView({
- currency,
amount,
exchangeBaseUrl,
confirm,
diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts
index 06b11ef6d..36e9cd1b9 100644
--- a/packages/taler-wallet-webextension/src/cta/index.stories.ts
+++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts
@@ -22,7 +22,6 @@
export * as a1 from "./Deposit/stories.jsx";
export * as a3 from "./Payment/stories.jsx";
export * as a4 from "./Refund/stories.jsx";
-export * as a5 from "./Reward/stories.js";
export * as a6 from "./Withdraw/stories.jsx";
export * as a8 from "./InvoiceCreate/stories.js";
export * as a9 from "./InvoicePay/stories.js";
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index a5e357f7d..bd430f2ef 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -48,14 +48,13 @@ export type HookResponseWithRetry<T> =
export function useAsyncAsHook<T>(
fn: () => Promise<T | false>,
- deps?: any[],
+ deps?: unknown[],
): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
const args = useMemo(
() => ({
fn,
- // eslint-disable-next-line react-hooks/exhaustive-deps
}),
deps || [],
);
diff --git a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
index ca2054931..e2ba5b285 100644
--- a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
@@ -14,7 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ProviderInfo, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { ProviderInfo } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
index dd3822c1a..a79a71087 100644
--- a/packages/taler-wallet-webextension/src/hooks/useSettings.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
@@ -36,12 +36,16 @@ export const codecForSettings = (): Codec<Settings> =>
.property("walletAllowHttp", codecForBoolean())
.property("injectTalerSupport", codecForBoolean())
.property("autoOpen", codecForBoolean())
- .property("advanceMode", codecForBoolean())
+ .property("advancedMode", codecForBoolean())
.property("backup", codecForBoolean())
.property("langSelector", codecForBoolean())
.property("showJsonOnError", codecForBoolean())
.property("extendedAccountTypes", codecForBoolean())
.property("suspendIndividualTransaction", codecForBoolean())
+ .property("showRefeshTransactions", codecForBoolean())
+ .property("showExchangeManagement", codecForBoolean())
+ .property("selectTosFormat", codecForBoolean())
+ .property("showWalletActivity", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
@@ -50,11 +54,11 @@ export function useSettings(): [
Readonly<Settings>,
<T extends keyof Settings>(key: T, value: Settings[T]) => void,
] {
- const { value, update } = useLocalStorage(SETTINGS_KEY);
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
- const parsed: Settings = value ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
- update({ ...parsed, [k]: v });
+ update({ ...value, [k]: v });
}
- return [parsed, updateField];
+
+ return [value, updateField];
}
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index 1a285499c..bc66f2136 100644
--- a/packages/taler-wallet-webextension/src/i18n/de.po
+++ b/packages/taler-wallet-webextension/src/i18n/de.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-11-25 17:24+0000\n"
+"PO-Revision-Date: 2024-05-07 14:32+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/de/>\n"
@@ -26,7 +26,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.4.3\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -56,7 +56,7 @@ msgstr "Dev"
#: src/mui/Typography.tsx:122
#, c-format
msgid "%1$s"
-msgstr ""
+msgstr "%1$s"
#: src/components/PendingTransactions.tsx:74
#, c-format
@@ -215,7 +215,7 @@ msgstr ""
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr "Abbrechen"
+msgstr "Zurück"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -325,7 +325,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:189
#, c-format
msgid "Summary"
-msgstr ""
+msgstr "Zusammenfassung"
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
@@ -370,7 +370,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:256
#, c-format
msgid "Delivery date"
-msgstr ""
+msgstr "Lieferdatum"
#: src/components/ShowFullContractTermPopup.tsx:271
#, c-format
@@ -405,7 +405,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:354
#, c-format
msgid "Fulfillment URL"
-msgstr ""
+msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)"
#: src/components/ShowFullContractTermPopup.tsx:360
#, c-format
@@ -1061,7 +1061,7 @@ msgstr "Konnte die Umsatzanzeige nicht laden"
#: src/wallet/ExchangeSelection/views.tsx:131
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Schließen"
#: src/wallet/ExchangeSelection/views.tsx:160
#, fuzzy, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/es.po b/packages/taler-wallet-webextension/src/i18n/es.po
index a482b9550..ea1fa9803 100644
--- a/packages/taler-wallet-webextension/src/i18n/es.po
+++ b/packages/taler-wallet-webextension/src/i18n/es.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-08-13 10:14+0000\n"
+"PO-Revision-Date: 2024-03-07 07:03+0000\n"
"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/es/>\n"
@@ -26,12 +26,12 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.13.1\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
msgid "Balance"
-msgstr "Balance"
+msgstr "Saldo"
#: src/NavigationBar.tsx:142
#, c-format
@@ -598,7 +598,7 @@ msgstr "Confirmar"
#: src/wallet/Transaction.tsx:267
#, c-format
msgid "Withdrawal"
-msgstr "Retirada"
+msgstr "Extracción"
#: src/wallet/Transaction.tsx:286
#, c-format
@@ -1890,7 +1890,7 @@ msgstr "Escanear un código QR o ingresar taler:// URI debajo"
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr "Abrir"
+msgstr "Abierto"
#: src/wallet/QrReader.tsx:128
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/fi.po b/packages/taler-wallet-webextension/src/i18n/fi.po
new file mode 100644
index 000000000..c6196b7f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/fi.po
@@ -0,0 +1,1967 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-20 00:10+0000\n"
+"Last-Translator: Sara Korpinen <sara.a.korpinen@gmail.com>\n"
+"Language-Team: Finnish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/fi/>\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Varmuuskopio"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR -lukija ja Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Asetukset"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Kehitys"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "ODOTTAVAT TOIMINNOT"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Lataa"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Varmuuskopion tarjoajia ei voitu ladata"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Varmuuskopion tarjoajia ei ole määritetty"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Lisää palveluntarjoaja"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Synkronoi kaikki varmuuskopiot"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Synkronoi nyt"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Viimeksi synkronoitu"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Ei synkronoitu"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Vanhenee"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Virhe ladattaessa palveluntarjoajan tietoja kohteelle &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Ei tunneta palveluntarjoajaa, jonka URL-osoite on &quot;%1$s&quot;."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Katso palveluntarjoajat"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Viimeisin varmuuskopio"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Varmuuskopioi"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Palveluntarjoajan maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "vuodessa"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Laajenna"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"ehdot ovat muuttuneet, palvelun laajentaminen tarkoittaa uusien "
+"käyttöehtojen hyväksymistä"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "vanha"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "uusi"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "tila"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Poista palveluntarjoaja"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Tämä palveluntarjoaja on ilmoittanut virheestä"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Ristiriita toisen varmuuskopion kanssa kohteesta %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Varmuuskopiota ei voi lukea"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Tuntematon varmuuskopiointi ongelma: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "palvelu maksettu"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format, fuzzy
+msgid "Backup valid until"
+msgstr "Varmuuskopio voimassa"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Peruuta"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Avaa varaussivu"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Avaa maksusivu"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Avaa hyvityssivu"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Avaa tippi sivu"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Avaa nostosivu"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Hanki digitaalista käteistä"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Ei voitu ladata saldosivua"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Lisää"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Lähetä %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler toiminta"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "Tällä sivulla on maksutoiminto."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "Tällä sivulla on nosto toiminto."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "Tällä sivulla on tippaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "Tällä sivulla on ilmoitus varaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Ilmoita"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "Tällä sivulla on hyvitys toiminto."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "Tällä sivulla on väärin muotoiltu taler uri."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Hylkää"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "tämä ponnahdusikkuna suljetaan ja sinut ohjataan osoitteeseen %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Ostoehdotuksen tietoja ei voitu ladata"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Tilausnumero"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Yhteenveto"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Summa"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Kauppiaan nimi"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Kauppiaan toimivalta"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Kauppiaan osoite"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Kauppiaan logo"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Kauppiaan nettisivut"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Kauppiaan sähköposti"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Kauppiaan julkinen avain"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Toimituspäivä"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Toimituspaikka"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Tuotteet"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Luotu"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Palautuksen määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Automaattinen palautus"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Maksun määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "Toteutus-URL"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Toteutusviesti"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Max talletusmaksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "Max maksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Alaikäraja"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Pankkimaksun lyhennys"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Tilintarkastajat"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Vaihdot"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Pankkitili"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Bitcoin osoite"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "Talletuksen tilaa ei voitu ladata"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Digitaalinen käteistalletus"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Kustannus"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Maksu"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "Vastaanotettava"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Lähetä &nbsp; %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Bitcoin -siirron tiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+"Pörssi tarvitsee tapahtuman, jossa on 3 lähtöä, joista yksi on vaihtotili ja "
+"kaksi muuta ovat segwit fake -osoitteita metatiedoille vähimmäismäärällä."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+"Käytä bitcoincore-lompakossa &apos;Lisää vastaanottaja&apos; -painiketta "
+"lisätäksesi kaksi muuta vastaanottajaa ja kopioidaksesi osoitteet ja summat"
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+"Varmista, että summa näyttää %1$s BTC, muuten sinun on vaihdettava "
+"perusyksikkö BTC:ksi"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Tili"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Pankin isäntä"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Pankkisiirtotiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Aihe"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Vastaanottajan nimi"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "Tapahtumatietoja ei voitu ladata"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "Tapahtuman suorittamisessa tapahtui virhe"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Tätä tapahtumaa ei ole suoritettu loppuun"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Lähetä"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Yritä uudelleen"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Unohda"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Varoitus!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+"Jos olet jo siirtänyt rahaa vaihtoon, menetät mahdollisuuden saada kolikot "
+"siitä."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Vahvista"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Nosto"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Varmista, että käytät oikeaa aihetta, muuten rahat eivät tule tähän "
+"lompakkoon."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+"Pankki ei ole vielä vahvistanut pankkisiirtoa. Siirry kohtaan %1$s %2$s ja "
+"tarkista, ettei odottavaa vaihetta ole."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Avoin"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/fr.po b/packages/taler-wallet-webextension/src/i18n/fr.po
index 3ed1104b2..462eb30f7 100644
--- a/packages/taler-wallet-webextension/src/i18n/fr.po
+++ b/packages/taler-wallet-webextension/src/i18n/fr.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-03-06 22:06+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/fr/>\n"
"Language: fr\n"
@@ -26,7 +26,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
-"X-Generator: Weblate 4.13.1\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -213,7 +213,7 @@ msgstr ""
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr ""
+msgstr "Annuler"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -1051,7 +1051,7 @@ msgstr ""
#: src/wallet/ExchangeSelection/views.tsx:131
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Fermer"
#: src/wallet/ExchangeSelection/views.tsx:160
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/nl.po b/packages/taler-wallet-webextension/src/i18n/nl.po
index 26f543b52..4f11592dd 100644
--- a/packages/taler-wallet-webextension/src/i18n/nl.po
+++ b/packages/taler-wallet-webextension/src/i18n/nl.po
@@ -6,15 +6,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: Automatically generated\n"
-"Language-Team: none\n"
+"PO-Revision-Date: 2024-03-02 16:54+0000\n"
+"Last-Translator: Midgard <midgard@users.noreply.weblate.taler.net>\n"
+"Language-Team: Dutch <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -697,7 +700,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:635
#, c-format
msgid "Exchange"
-msgstr ""
+msgstr "Beurs"
#: src/wallet/Transaction.tsx:641
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/ru.po b/packages/taler-wallet-webextension/src/i18n/ru.po
new file mode 100644
index 000000000..aa002c984
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/ru.po
@@ -0,0 +1,1977 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-05-10 00:13+0000\n"
+"Last-Translator: Lily Ponomareva <lilyponomareva2017@gmail.com>\n"
+"Language-Team: Russian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/ru/>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.4.3\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Резервная копия"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "Считыватель QR-кодов и URI Taler"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Настройки"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Dev"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "ОЖИДАЮЩИЕ ОПЕРАЦИИ"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Загружаются"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Не удалось загрузить поставщиков резервного копирования"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Поставщики резервного копирования не настроены"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Добавить сервис"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Синхронизация всех резервных копий"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Синхронизировать сейчас"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Последняя синхронизация"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Не синхронизировано"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Срок действия истекает в"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+"Произошла ошибка при загрузке сведений о поставщике для &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Нет провайдера с url &quot;%1$s&quot;."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Посмотреть провайдеров"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Последняя резервная копия"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Создать резервную копию"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Комиссия провайдера"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "в год"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Расширить"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"изменились условия, продление сервиса будет означать принятие новых условий "
+"предоставления услуг"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "старый"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "новый"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "комиссия"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "хранение"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Удалить провадер"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Этот провайдер сообщил об ошибке"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Возник конфликт с другой резервной копией из %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Резервная копия не читается"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Неизвестная проблема резервного копирования: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "Услуга платная"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Резервная копия действительна до"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Отмена"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Открыть резервную страницу"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Открыть страницу оплаты"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Открыть страницу возврата средств"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Открыть страницу чаевых"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Открыть страницу вывода средств"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Получите цифровую наличность"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Не удалось загрузить страницу баланса"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Добавить"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Отправить %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Действие Талер"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "На этой странице есть платное действие."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "На этой странице есть действие по выводу средств."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "На этой странице есть действие чаевых."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "На этой странице есть действие уведомить о резерве."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Уведомить"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "На этой странице есть действие по возврату средств."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "На этой странице неправильно сформирован Taler URI."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Закрыть"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "Это всплывающее окно закрывается и вы перенаправляетесь на %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Не удалось загрузить сведения о предложении покупки"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Номер заказа"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Вкратце"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Сумма"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Название продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Юрисдикция продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Адрес продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Логотип продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Сайт продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Email продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Публичный ключ продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Дата поставки"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Адрес доставки"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Продукты"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Создано в"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Крайний срок возврата средств"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Автоматический возврат средств"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Крайний срок оплаты"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL-адрес выполнения"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Сообщение о выполнении"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Максимальная комиссия за депозит"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "максимальная комиссия"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Минимальный возраст"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Комиссия за банковский перевод"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Аудиторы"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Обменники"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Баковский счёт"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Биткоин адрес"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "Не удалось загрузить статус депозита"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Депозит цифровой налички"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Стоимость"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Комиссия"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "К получению"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Отправить &nbsp; %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Подробности перевода биткоина"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+"Обменнику нужна транзакция с 3 выходами, один выход - это счёт обменника, а "
+"два других - это сегвит фейк адрес для метаданных с минимальной суммой."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+"Убедитесь что сумма показывает %1$s BTC, в противном случае вам придётся "
+"изменить базовую единицу на BTC"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Счёт"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Хост банка"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Подробности банковского перевода"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Причина"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Имя получателя"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "Не удалось загрузить информацию о транзакции"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "При попытке завершить транзакцию произошла ошибка"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Эта транзакция не завершена"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Отправить"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Повторить попытку"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Забыть"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Внимание!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+"Если вы уже перевели деньги на обменник вы потеряете шанс получить монеты с "
+"нее."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Подтвердить"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Вывод"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Убедитесь что вы указали правильное назначение, иначе деньги не поступят на "
+"этот кошелек."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+"Банк пока не подтвердил перевод. Перейдите к %1$s %2$s и проверьте нет ли "
+"ожидающих шагов."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr "Банк подтвердил перевод. Ожидание пока обменик отправит монеты"
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr "Подробности"
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr "Платёж"
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr "Возвраты"
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr "%1$s %2$s на %3$s"
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+"Продавец создал возврат средств за этот заказ, но не был автоматически "
+"забран."
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr "Предложение"
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr "Принять"
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr "Продавец"
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr "№ счёта-фактуры"
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr "Депозит"
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr "Обновить"
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr "Чаевые"
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr "Возврат"
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr "№ исходного заказа"
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr "Сводка о покупке"
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr "копировать"
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr "спрятать qr"
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr "показать qr"
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr "Кредит"
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr "Счёт-фактура"
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Обменник"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr "URI"
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr "Дебит"
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr "Перевести"
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr "Страна"
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr "Строки адреса"
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr "Номер дома"
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr "Название дома"
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr "Улица"
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr "Почтовый индекс"
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr "Область города"
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr "Город"
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr "Район"
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr "Регион страны"
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr "Дата"
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr "Комиссия транзакции"
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr "Всего"
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Снять средства"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr "Цена"
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr "Возвращено на счёт"
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr "Поставка"
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr "Итого перевод"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr "Не удалось загрузить статус оплаты"
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr "Оплата цифровой наличкой"
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr "Покупка"
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr "Чек"
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr "Действительно до"
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr "Список продуктов"
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr "комиссия"
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr "Уже оплачено, вы будете перенаправлены на %1$s"
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr "Уже оплачено"
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr "Уже заявлено"
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr "Оплата с помощью мобильного телефона"
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr "Скрыть QR"
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr "Отсканируйте QR код или &nbsp; %1$s"
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr "Заплатить &nbsp; %1$s"
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr "У вас нет баланса в этой валюте. Сначала снимите цифровые деньги."
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+"Не удалось найти достаточно монет для оплаты. Даже если у вас достаточно %1$"
+"s, могут применяться некоторые ограничения."
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr "Недостаточно средств на балансе."
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr "Сообщение продавца"
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr "Не удалось загрузить статус возврата"
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr "Возврат цифровой налички"
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr "Вы проигнорировали чаевые."
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr "Возврат средств в выполняется."
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr "Всего к возврату"
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr "Продавец «%1$s»‎ предлагает вам возврат средств."
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr "Сумма заказа"
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr "Уже возвращено"
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr "Предложен возврат средств"
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr "Принять &nbsp; %1$s"
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr "Не удалось загрузить статус чаевых"
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr "Чаевые цифровой налички"
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr "Продавец предлагает вам чаевые"
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr "URL-адрес продавца"
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr "Получить &nbsp; %1$s"
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr "Чаевые от %1$s приняты. Проверьте список транзакций для подробностей."
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr "Выберете одну опцию"
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr "Невозможно загрузить"
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr "Показать Условия использования"
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr "Я принимаю эти Условия использования"
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr "Обменник не имеет условий использования"
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr "Ознакомиться с Условиями использования"
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr "Ознакомиться с новой версией Условий использования"
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr "Биржа ответитила с пустыми условиями использования"
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr "Скачать Условия использования"
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr "Скрыть Условия использования"
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr "Не удалось загрузить комиссию за обмен"
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr "Закрыть"
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr "Не удалось найти ни одного обменника"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr "Не удалось найти ни одного обменника для валюты %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr "Описание комиссии за услугу"
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr "Выберите %1$s обменник"
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr "Сбросить"
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr "Использовать этот обменник"
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr "Не имеет аудиторов"
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr "валюта"
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr "Операции"
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr "Депозиты"
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr "Деноминация"
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr "до"
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr "Выводы средств"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr "Валюта"
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr "Операции моент"
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+"Каждая операция в этом разделе может отличаться номиналом и действительна в "
+"течение определенного периода времени. Биржа будет взимать указанную сумму "
+"каждый раз, когда монета используется в такой операции."
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr "Операции переводов"
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+"Каждая операция в этом разделе может отличаться в зависимости от типа "
+"перевода и действительна в течение определенного периода времени. Обменник "
+"будет взимать указанную сумму каждый раз при совершении перевода."
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr "Операция"
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr "Операции кошелька"
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr "Возможность"
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr "Не удалось получить информацию из URI"
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr "Не удалось получить информацию о выводе средств"
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr "Вывод цифровых наличных"
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr "Ограничения возраста"
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr "Вывести &nbsp; %1$s"
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr "Вывести на мобильный телефон"
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr "Цифровой счёт-фактура"
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr "Создать"
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr "Добавить Обменник"
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr "Начать вывод"
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr "Добавить Счёт"
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr "Выберете счёт"
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr "Добавить другой счёт"
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr "Комиссия депозита"
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr "Всего к депозиту"
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr "Выберете тип счёта"
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr "URL обменника"
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr "Добавить Обменник"
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr "Добавить новый Обменник"
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr "загрузка"
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr "Версия"
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr "Далее"
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr "Ожидание подтверждения"
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr "ОЖИДАЕТ"
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr "Добавить провайдера резервной копии"
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr "Название"
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr "URL провайдера"
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr "комиссия за пополнение"
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr "Хранилище"
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr "Навигатор"
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr "Доверять"
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr "Условия использования"
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr "ok"
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr "изменено"
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr "не принято"
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr "Исправление проблем"
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr "Режим разработчика"
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr "Отбражение"
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr "Расширение браузера"
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr "Разрешения"
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr "Следующий шаг"
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr "Попробовать демо"
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr "Узнайте как пополнить ваш баланс на кошельке"
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr "Инструменты отладки"
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr "сбросить"
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr "импортировать базу данных"
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr "экспортировать базу данных"
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr "Монеты"
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr "значение"
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr "статус"
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr "кликите чтобы показать"
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Открыть"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr "Из моего банковского счёта"
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr "На мой банковский счёт"
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr "На другой кошелек"
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr "Импорт резервной копии, отображение информации"
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr "Все готово, ваша транзакция выполняется"
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr "Изменить"
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr "Не удалось загрузить список известных обменников"
diff --git a/packages/taler-wallet-webextension/src/i18n/tr.po b/packages/taler-wallet-webextension/src/i18n/tr.po
index 0d5132b61..5848b9f3a 100644
--- a/packages/taler-wallet-webextension/src/i18n/tr.po
+++ b/packages/taler-wallet-webextension/src/i18n/tr.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-12-05 21:51+0000\n"
+"PO-Revision-Date: 2024-03-08 01:14+0000\n"
"Last-Translator: Alp <berna.alp@digitalekho.com>\n"
"Language-Team: Turkish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/tr/>\n"
@@ -1863,7 +1863,7 @@ msgstr ""
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr ""
+msgstr "Açık"
#: src/wallet/QrReader.tsx:128
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/uk.po b/packages/taler-wallet-webextension/src/i18n/uk.po
new file mode 100644
index 000000000..c4f5d6537
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/uk.po
@@ -0,0 +1,1956 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-05 13:03+0000\n"
+"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Бекап"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR-читалка та Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Налаштування"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Розробка"
+
+#: src/mui/Typography.tsx:122
+#, c-format, fuzzy
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "НЕЗАВЕРШЕНІ ОПЕРАЦІЇ"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Завантаження"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Не вдалося завантажити зберігачів резервних копій"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Не налаштовано жодного зберігача резервних копій"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Додати зберігача"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Синхронізувати всі резервні копії"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Синхронізувати зараз"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Останній раз синхронізовано"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Не синхронізовано"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Термін дії закінчується в"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Виникла помилка при завантаженні інформації зберігача &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Зберігач з посиланням &quot;%1$s&quot; невідомий."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Подивитись зберігачів"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Остання резервна копія"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Зробити резервну копію"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Комісія зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "на рік"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Подовжити"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"умови надання послуг змінились, продовження послуги означатиме прийняття "
+"нових умов"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "старий"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "новий"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "комісія"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "сховище"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Видалити зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Цей постачальник повідомив про помилку"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Конфлікт з іншою резервною копією з %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Резервна копія пошкоджена або не може бути прочитана"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Невідома помилка резервного копіювання: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "послуга сплачена"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Резервна копія дійсна до"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Відмінити"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Показати резерв"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Показати сторінку оплати"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Показати відшкодування"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Показати чайові"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Показати списання"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Отримати е-готівку"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Не вдалося показати залишок"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Додати"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Переказати %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler Дія"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Доступні"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx
index 1af281d42..12a4d91ea 100644
--- a/packages/taler-wallet-webextension/src/mui/Button.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -371,7 +371,11 @@ function ButtonBase({
);
}
return (
- <button onClick={doClick} class={classNames} {...rest}>
+ <button onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ doClick();
+ }} class={classNames} {...rest}>
{children}
</button>
);
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx
index 4d7c9a472..ab29fb78d 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx
@@ -30,7 +30,7 @@ export interface Props {
autoFocus?: boolean;
color?: Colors;
disabled?: boolean;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
helperText?: VNode | string;
id?: string;
diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts
index 735e8523f..a194bd02a 100644
--- a/packages/taler-wallet-webextension/src/mui/handlers.ts
+++ b/packages/taler-wallet-webextension/src/mui/handlers.ts
@@ -18,13 +18,13 @@ import { AmountJson } from "@gnu-taler/taler-util";
export interface TextFieldHandler {
onInput?: SafeHandler<string>;
value: string;
- error?: string;
+ error?: string | Error;
}
export interface AmountFieldHandler {
onInput?: SafeHandler<AmountJson>;
value: AmountJson;
- error?: string;
+ error?: string | Error;
}
declare const __safe_handler: unique symbol;
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
index 23dfcfd08..45f5a81d1 100644
--- a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
@@ -22,7 +22,7 @@ import { Colors } from "../style.js";
export interface Props {
color: Colors;
disabled: boolean;
- error?: string;
+ error?: string | Error;
focused: boolean;
fullWidth: boolean;
hiddenLabel: boolean;
@@ -124,7 +124,7 @@ export interface FCCProps {
// setAdornedStart,
color: Colors;
disabled: boolean;
- error: string | undefined;
+ error: string | undefined | Error;
filled: boolean;
focused: boolean;
fullWidth: boolean;
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
index 5fa48a169..3b80b0f23 100644
--- a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
@@ -43,7 +43,7 @@ const containedStyle = css`
interface Props {
disabled?: boolean;
- error?: string;
+ error?: string | Error;
filled?: boolean;
focused?: boolean;
margin?: "dense";
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
index a984f8451..0707046f3 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
@@ -27,7 +27,7 @@ export interface Props {
defaultValue?: string;
disabled?: boolean;
disableUnderline?: boolean;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
id?: string;
margin?: "dense" | "normal" | "none";
@@ -89,9 +89,9 @@ const filledRootStyle = css`
border-top-left-radius: ${theme.shape.borderRadius}px;
border-top-right-radius: ${theme.shape.borderRadius}px;
transition: ${theme.transitions.create("background-color", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
// when is not disabled underline
&:hover {
background-color: ${backgroundColorHover};
@@ -124,9 +124,9 @@ const underlineStyle = css`
right: 0px;
transform: scaleX(0);
transition: ${theme.transitions.create("transform", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
pointer-events: none;
}
&[data-focused]:after {
@@ -139,8 +139,8 @@ const underlineStyle = css`
&:before {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
left: 0px;
bottom: 0px;
right: 0px;
@@ -156,8 +156,8 @@ const underlineStyle = css`
@media (hover: none) {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
}
}
&[data-disabled]:before {
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
index f7b5040e4..7352c5ec1 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
@@ -27,7 +27,7 @@ export interface Props {
disabled?: boolean;
disableUnderline?: boolean;
endAdornment?: VNode;
- error?: string;
+ error?: string | Error;
fullWidth?: boolean;
id?: string;
margin?: "dense" | "normal" | "none";
@@ -82,9 +82,9 @@ const underlineStyle = css`
right: 0px;
transform: scaleX(0);
transition: ${theme.transitions.create("transform", {
- duration: theme.transitions.duration.shorter,
- easing: theme.transitions.easing.easeOut,
- })};
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
pointer-events: none;
}
&[data-focused]:after {
@@ -97,8 +97,8 @@ const underlineStyle = css`
&:before {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
left: 0px;
bottom: 0px;
right: 0px;
@@ -114,8 +114,8 @@ const underlineStyle = css`
@media (hover: none) {
border-bottom: 1px solid
${theme.palette.mode === "light"
- ? "rgba(0, 0, 0, 0.42)"
- : "rgba(255, 255, 255, 0.7)"};
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
}
}
&[data-disabled]:before {
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index a2b26441b..3c116fab2 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -17,12 +17,10 @@
import {
CoreApiResponse,
TalerUri,
- WalletNotification
+ WalletNotification,
+ WalletRunConfig,
} from "@gnu-taler/taler-util";
-import {
- WalletConfig,
- WalletOperations
-} from "@gnu-taler/taler-wallet-core";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import {
ExtensionOperations,
MessageFromExtension,
@@ -46,30 +44,48 @@ export interface Permissions {
* Compatibility API that works on multiple browsers.
*/
export interface CrossBrowserPermissionsApi {
- containsHostPermissions(): Promise<boolean>;
- requestHostPermissions(): Promise<boolean>;
- removeHostPermissions(): Promise<boolean>;
-
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
+}
- addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
- ): void;
+export enum ExtensionNotificationType {
+ SettingsChange = "settings-change",
+ ClearNotifications = "clear-notifications",
}
-export type MessageFromBackend = WalletNotification;
+export interface SettingsChangeNotification {
+ type: ExtensionNotificationType.SettingsChange;
+
+ currentValue: Settings;
+}
+export interface ClearNotificaitonNotification {
+ type: ExtensionNotificationType.ClearNotifications;
+}
+
+export type ExtensionNotification =
+ | SettingsChangeNotification
+ | ClearNotificaitonNotification;
+
+export type MessageFromBackend =
+ | {
+ type: "wallet";
+ notification: WalletNotification;
+ }
+ | {
+ type: "web-extension";
+ notification: ExtensionNotification;
+ };
export type MessageFromFrontend<
Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
> = Op extends BackgroundOperations
? MessageFromFrontendBackground<keyof BackgroundOperations>
: Op extends ExtensionOperations
- ? MessageFromExtension<keyof ExtensionOperations>
- : Op extends WalletOperations
- ? MessageFromFrontendWallet<keyof WalletOperations>
- : never;
+ ? MessageFromExtension<keyof ExtensionOperations>
+ : Op extends WalletOperations
+ ? MessageFromFrontendWallet<keyof WalletOperations>
+ : never;
export type MessageFromFrontendBackground<
Op extends keyof BackgroundOperations,
@@ -92,8 +108,7 @@ export interface WalletWebExVersion {
version: string;
}
-type F = WalletConfig["features"];
-type kf = keyof F;
+type F = WalletRunConfig["features"];
type WebexWalletConfig = {
[P in keyof F as `wallet${Capitalize<P>}`]: F[P];
};
@@ -101,24 +116,32 @@ type WebexWalletConfig = {
export interface Settings extends WebexWalletConfig {
injectTalerSupport: boolean;
autoOpen: boolean;
- advanceMode: boolean;
+ advancedMode: boolean;
backup: boolean;
langSelector: boolean;
showJsonOnError: boolean;
extendedAccountTypes: boolean;
+ showRefeshTransactions: boolean;
suspendIndividualTransaction: boolean;
+ showExchangeManagement: boolean;
+ selectTosFormat: boolean;
+ showWalletActivity: boolean;
}
export const defaultSettings: Settings = {
injectTalerSupport: true,
autoOpen: true,
- advanceMode: false,
+ advancedMode: false,
backup: false,
langSelector: false,
+ showRefeshTransactions: false,
suspendIndividualTransaction: false,
showJsonOnError: false,
extendedAccountTypes: false,
+ showExchangeManagement: false,
walletAllowHttp: false,
+ selectTosFormat: false,
+ showWalletActivity: false,
};
/**
@@ -208,15 +231,19 @@ export interface BackgroundPlatformAPI {
): void;
/**
- * Use by the wallet backend to activate the listener of HTTP request
+ * Change web extension Icon
*/
- registerTalerHeaderListener(): void;
-
- containsTalerHeaderListener(): boolean;
-
+ setAlertedIcon(): void;
+ setNormalIcon(): void;
}
+
export interface ForegroundPlatformAPI {
/**
+ * Check if the extension is running under
+ * chrome incognito or firefox private mode.
+ */
+ runningOnPrivateMode(): boolean;
+ /**
* FIXME: should not be needed
*
* check if the platform is firefox
@@ -248,7 +275,7 @@ export interface ForegroundPlatformAPI {
/**
* Open a page and close the popup
- * @param url
+ * @param url
*/
openNewURLFromPopup(url: URL): void;
/**
@@ -287,6 +314,12 @@ export interface ForegroundPlatformAPI {
): Promise<MessageResponse>;
/**
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
+ triggerWalletEvent(message: MessageFromBackend): void;
+
+ /**
* Used from the frontend to receive notifications about new information
* @param listener
* @return function to unsubscribe the listener
diff --git a/packages/taler-wallet-webextension/src/platform/background.ts b/packages/taler-wallet-webextension/src/platform/background.ts
index 9f3764c25..13808af2b 100644
--- a/packages/taler-wallet-webextension/src/platform/background.ts
+++ b/packages/taler-wallet-webextension/src/platform/background.ts
@@ -16,7 +16,8 @@
import { BackgroundPlatformAPI } from "./api.js";
-export let platform: BackgroundPlatformAPI = undefined as any;
+// it should never be undefined :)
+export let platform: BackgroundPlatformAPI = undefined!;
export function setupPlatform(impl: BackgroundPlatformAPI): void {
platform = impl;
}
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index 20cf54035..e63040f5c 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -16,11 +16,10 @@
import {
Logger,
- TalerErrorCode,
- TalerUriAction,
TalerError,
- parseTalerUri,
+ TalerErrorCode,
TalerUri,
+ TalerUriAction,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
@@ -28,11 +27,11 @@ import { BackgroundOperations } from "../wxApi.js";
import {
BackgroundPlatformAPI,
CrossBrowserPermissionsApi,
+ ExtensionNotificationType,
ForegroundPlatformAPI,
MessageFromBackend,
MessageFromFrontend,
MessageResponse,
- Permissions,
Settings,
defaultSettings,
} from "./api.js";
@@ -43,7 +42,9 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
findTalerUriInActiveTab,
findTalerUriInClipboard,
getPermissionsApi,
+ runningOnPrivateMode,
getWalletWebExVersion,
+ triggerWalletEvent,
listenToWalletBackground,
notifyWhenAppIsReady,
openWalletPage,
@@ -52,7 +53,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels: listenToAllChannels as any,
+ listenToAllChannels ,
registerReloadOnNewVersion,
sendMessageToAllChannels,
openNewURLFromPopup,
@@ -60,28 +61,33 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
useServiceWorkerAsBackgroundProcess,
keepAlive,
listenNetworkConnectionState,
- registerTalerHeaderListener,
- containsTalerHeaderListener,
+ setAlertedIcon,
+ setNormalIcon,
};
export default api;
const logger = new Logger("chrome.ts");
-async function getSettingsFromStorage(): Promise<Settings> {
- const data = await chrome.storage.local.get("wallet-settings");
- if (!data) return defaultSettings;
- const settings = data["wallet-settings"];
- if (!settings) return defaultSettings;
+const WALLET_STORAGE_KEY = "wallet-settings";
+
+function jsonParseOrDefault(unparsed: string, def: unknown) {
+ if (!unparsed) return def;
try {
- const parsed = JSON.parse(settings);
- return parsed;
+ return JSON.parse(unparsed);
} catch (e) {
- return defaultSettings;
+ return def;
}
}
-function keepAlive(callback: any): void {
+async function getSettingsFromStorage(): Promise<Settings> {
+ const data = await chrome.storage.local.get(WALLET_STORAGE_KEY);
+ if (!data) return defaultSettings;
+ const settings = data[WALLET_STORAGE_KEY];
+ return jsonParseOrDefault(settings, defaultSettings);
+}
+
+function keepAlive(callback: () => void): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@@ -98,9 +104,8 @@ function isFirefox(): boolean {
return false;
}
-
export function containsClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(false);
// chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -113,7 +118,7 @@ export function containsClipboardPermissions(): Promise<boolean> {
}
export async function requestClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(false);
// chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -125,10 +130,8 @@ export async function requestClipboardPermissions(): Promise<boolean> {
});
}
-
-
export function removeClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(true);
// chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -140,21 +143,8 @@ export function removeClipboardPermissions(): Promise<boolean> {
});
}
-function addPermissionsListener(
- callback: (p: Permissions, lastError?: string) => void,
-): void {
- chrome.permissions.onAdded.addListener((perm: Permissions) => {
- const lastError = chrome.runtime.lastError?.message;
- callback(perm, lastError);
- });
-}
-
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- containsHostPermissions,
- requestHostPermissions,
- removeHostPermissions,
- addPermissionsListener,
requestClipboardPermissions,
removeClipboardPermissions,
containsClipboardPermissions,
@@ -166,7 +156,7 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
* @param callback function to be called
*/
function notifyWhenAppIsReady(): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
if (extensionIsManifestV3()) {
resolve();
} else {
@@ -205,11 +195,6 @@ function openWalletURIFromPopup(uri: TalerUri): void {
`static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
);
break;
- case TalerUriAction.Reward:
- url = chrome.runtime.getURL(
- `static/wallet.html#/cta/tip?talerUri=${encodeURIComponent(talerUri)}`,
- );
- break;
case TalerUriAction.Refund:
url = chrome.runtime.getURL(
`static/wallet.html#/cta/refund?talerUri=${encodeURIComponent(
@@ -238,15 +223,16 @@ function openWalletURIFromPopup(uri: TalerUri): void {
)}`,
);
break;
+ case TalerUriAction.AddExchange:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/add/exchange?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
case TalerUriAction.DevExperiment:
logger.warn(`taler://dev-experiment URIs are not allowed in headers`);
return;
- case TalerUriAction.Exchange:
- logger.warn(`taler://exchange not yet supported`);
- return;
- case TalerUriAction.Auditor:
- logger.warn(`taler://auditor not yet supported`);
- return;
default: {
const error: never = uri;
logger.warn(
@@ -271,7 +257,6 @@ function openWalletPageFromPopup(page: string): void {
chrome.tabs.create({ active: true, url }, () => {
window.close();
});
-
}
function openNewURLFromPopup(url: URL): void {
// const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
@@ -290,14 +275,19 @@ let nextMessageIndex = 0;
async function sendMessageToBackground<
Op extends WalletOperations | BackgroundOperations,
>(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
- const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
+ const messageWithId = { ...message, id: `id_${nextMessageIndex}` };
- return new Promise<any>((resolve, reject) => {
+ return new Promise<MessageResponse>((resolve, reject) => {
logger.trace("send operation to the wallet background", message);
let timedout = false;
const timerId = setTimeout(() => {
timedout = true;
- reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {}));
+ reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }));
}, 20 * 1000);
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
if (timedout) {
@@ -319,7 +309,7 @@ async function sendMessageToBackground<
* To be used by the foreground
*/
let notificationPort: chrome.runtime.Port | undefined;
-function listenToWalletBackground(listener: (m: any) => void): () => void {
+function listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void {
if (notificationPort === undefined) {
notificationPort = chrome.runtime.connect({ name: "notifications" });
}
@@ -334,6 +324,17 @@ function listenToWalletBackground(listener: (m: any) => void): () => void {
const allPorts: chrome.runtime.Port[] = [];
+function triggerWalletEvent(message: MessageFromBackend): void {
+ for (const notif of allPorts) {
+ // const message: MessageFromBackend = { type: msg.type };
+ try {
+ notif.postMessage(message);
+ } catch (e) {
+ logger.error("error posting a message", e);
+ }
+ }
+}
+
function sendMessageToAllChannels(message: MessageFromBackend): void {
for (const notif of allPorts) {
// const message: MessageFromBackend = { type: msg.type };
@@ -363,6 +364,20 @@ function registerAllIncomingConnections(): void {
logger.error("error trying to save incoming connection", e);
}
});
+ chrome.storage.onChanged.addListener((event) => {
+ if (event[WALLET_STORAGE_KEY]) {
+ sendMessageToAllChannels({
+ type: "web-extension",
+ notification: {
+ type: ExtensionNotificationType.SettingsChange,
+ currentValue: jsonParseOrDefault(
+ event[WALLET_STORAGE_KEY].newValue,
+ defaultSettings,
+ ),
+ },
+ });
+ }
+ });
}
function listenToAllChannels(
@@ -402,14 +417,17 @@ function registerReloadOnNewVersion(): void {
});
}
-async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
- let queryOptions = { active: true, lastFocusedWindow: true };
- let [tab] = await chrome.tabs.query(queryOptions);
+// async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
+// let queryOptions = { active: true, lastFocusedWindow: true };
+// let [tab] = await chrome.tabs.query(queryOptions);
- return redirectTabToWalletPage(tab.id!, page);
-}
+// return redirectTabToWalletPage(tab.id!, page);
+// }
-async function redirectTabToWalletPage(tabId: number, page: string): Promise<void> {
+async function redirectTabToWalletPage(
+ tabId: number,
+ page: string,
+): Promise<void> {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
logger.trace("redirecting tabId: ", tabId, " to: ", url);
await chrome.tabs.update(tabId, { url });
@@ -650,7 +668,7 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
return;
}
} else {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
//manifest v2
chrome.tabs.executeScript(
tabId,
@@ -676,9 +694,9 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
}
}
-async function timeout(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
+// async function timeout(ms: number): Promise<void> {
+// return new Promise((resolve) => setTimeout(resolve, ms));
+// }
async function findTalerUriInClipboard(): Promise<string | undefined> {
//FIXME: add clipboard feature
// try {
@@ -723,253 +741,6 @@ function listenNetworkConnectionState(
};
}
-type HeaderListenerFunc = (
- details: chrome.webRequest.WebResponseHeadersDetails,
-) => void;
-let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
-
-// type TabListenerFunc = (tabId: number, info: chrome.tabs.TabChangeInfo) => void;
-// let currentTabListener: TabListenerFunc | undefined = undefined;
-
-
-function containsTalerHeaderListener(): boolean {
- return (
- currentHeaderListener !== undefined
- // || currentTabListener !== undefined
- );
+function runningOnPrivateMode(): boolean {
+ return chrome.extension.inIncognitoContext;
}
-
-function headerListener(
- details: chrome.webRequest.WebResponseHeadersDetails,
-): chrome.webRequest.BlockingResponse | undefined {
- logger.info("header listener run", details.statusCode, chrome.runtime.lastError)
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- return;
- }
-
- if (
- details.statusCode === 402 ||
- details.statusCode === 202 ||
- details.statusCode === 200
- ) {
- const values = (details.responseHeaders || [])
- .filter((h) => h.name.toLowerCase() === "taler")
- .map((h) => h.value)
- .filter((value): value is string => !!value);
-
- const talerUri = values.length > 0 ? values[0] : undefined
- if (talerUri) {
- logger.info(
- `Found a Taler URI in a response header for the request ${details.url} from tab ${details.tabId}: ${talerUri}`,
- );
- parseTalerUriAndRedirect(details.tabId, talerUri);
- return;
- }
- }
- return details;
-}
-function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
- const talerUri = maybeTalerUri.startsWith("ext+")
- ? maybeTalerUri.substring(4)
- : maybeTalerUri;
- const uri = parseTalerUri(talerUri);
- if (!uri) {
- logger.warn(
- `Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
- );
- return;
- }
- redirectTabToWalletPage(
- tabId,
- `/taler-uri/${encodeURIComponent(talerUri)}`,
- );
-}
-
-/**
- * Not needed anymore since SPA use taler support
- */
-
-// async function tabListener(
-// tabId: number,
-// info: chrome.tabs.TabChangeInfo,
-// ): Promise<void> {
-// if (tabId < 0) return;
-// const tabLocationHasBeenUpdated = info.status === "complete";
-// const tabTitleHasBeenUpdated = info.title !== undefined;
-// if (tabLocationHasBeenUpdated || tabTitleHasBeenUpdated) {
-// const uri = await findTalerUriInTab(tabId);
-// if (!uri) return;
-// logger.info(`Found a Taler URI in the tab ${tabId}`);
-// parseTalerUriAndRedirect(tabId, uri);
-// }
-// }
-
-/**
- * unused, declarative redirect is not good enough
- *
- */
-// async function registerDeclarativeRedirect() {
-// await chrome.declarativeNetRequest.updateDynamicRules({
-// removeRuleIds: [1],
-// addRules: [
-// {
-// id: 1,
-// priority: 1,
-// condition: {
-// urlFilter: "https://developer.chrome.com/docs/extensions/mv2/",
-// regexFilter: ".*taler_uri=([^&]*).*",
-// // isUrlFilterCaseSensitive: false,
-// // requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET]
-// // resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
-// },
-// action: {
-// type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
-// redirect: {
-// regexSubstitution: `chrome-extension://${chrome.runtime.id}/static/wallet.html?action=\\1`,
-// },
-// },
-// },
-// ],
-// });
-// }
-
-function registerTalerHeaderListener(): void {
- logger.info("setting up header listener");
-
- const prevHeaderListener = currentHeaderListener;
- // const prevTabListener = currentTabListener;
-
- if (
- prevHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
- ) {
- return;
- // console.log("removming on header listener")
- // chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
- // chrome.webRequest.onCompleted.removeListener(prevHeaderListener);
- // chrome.webRequest.onResponseStarted.removeListener(prevHeaderListener);
- // chrome.webRequest.onErrorOccurred.removeListener(prevHeaderListener);
- }
-
- // if (
- // prevTabListener &&
- // chrome?.tabs?.onUpdated?.hasListener(prevTabListener)
- // ) {
- // console.log("removming on tab listener")
- // chrome.tabs.onUpdated.removeListener(prevTabListener);
- // }
-
- console.log("headers on, disabled:", chrome?.webRequest?.onHeadersReceived === undefined)
- if (chrome?.webRequest) {
- if (extensionIsManifestV3()) {
- chrome.webRequest.onHeadersReceived.addListener(headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders"]
- );
- } else {
- chrome.webRequest.onHeadersReceived.addListener(headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders"]
- );
- }
- // chrome.webRequest.onCompleted.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["responseHeaders", "extraHeaders"]
- // );
- // chrome.webRequest.onResponseStarted.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["responseHeaders", "extraHeaders"]
- // );
- // chrome.webRequest.onErrorOccurred.addListener(headerListener,
- // { urls: ["<all_urls>"] },
- // ["extraHeaders"]
- // );
- currentHeaderListener = headerListener;
- }
-
- // const tabsEvent: chrome.tabs.TabUpdatedEvent | undefined =
- // chrome?.tabs?.onUpdated;
- // if (tabsEvent) {
- // tabsEvent.addListener(tabListener);
- // currentTabListener = tabListener;
- // }
-
- //notify the browser about this change, this operation is expensive
- chrome?.webRequest?.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
-}
-
-const hostPermissions = {
- permissions: ["webRequest"],
- origins: ["http://*/*", "https://*/*"],
-};
-
-export function containsHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.contains(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export async function requestHostPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
- chrome.permissions.request(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-}
-
-export async function removeHostPermissions(): Promise<boolean> {
- //if there is a handler already, remove it
- if (
- currentHeaderListener &&
- chrome?.webRequest?.onHeadersReceived?.hasListener(currentHeaderListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(currentHeaderListener);
- }
- // if (
- // currentTabListener &&
- // chrome?.tabs?.onUpdated?.hasListener(currentTabListener)
- // ) {
- // chrome.tabs.onUpdated.removeListener(currentTabListener);
- // }
-
- currentHeaderListener = undefined;
- // currentTabListener = undefined;
-
- //notify the browser about this change, this operation is expensive
- if ("webRequest" in chrome) {
- chrome.webRequest.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- logger.error(JSON.stringify(chrome.runtime.lastError));
- }
- });
- }
-
- if (extensionIsManifestV3()) {
- // Trying to remove host permissions with manifest >= v3 throws an error
- return true;
- }
- return new Promise((res, rej) => {
- chrome.permissions.remove(hostPermissions, (resp) => {
- const le = chrome.runtime.lastError?.message;
- if (le) {
- rej(le);
- }
- res(resp);
- });
- });
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index 51744e318..d6e743147 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -29,6 +29,7 @@ import {
const logger = new Logger("dev.ts");
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
+ runningOnPrivateMode: () => false,
isFirefox: () => false,
getSettingsFromStorage: () => Promise.resolve(defaultSettings),
keepAlive: (cb: VoidFunction) => cb(),
@@ -36,19 +37,15 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
findTalerUriInClipboard: async () => undefined,
listenNetworkConnectionState,
openNewURLFromPopup: () => undefined,
+ triggerWalletEvent: () => undefined,
+ setAlertedIcon: () => undefined,
+ setNormalIcon : () => undefined,
getPermissionsApi: () => ({
- addPermissionsListener: () => undefined,
- containsHostPermissions: async () => true,
- removeHostPermissions: async () => false,
- requestHostPermissions: async () => false,
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
requestClipboardPermissions: async () => false,
}),
- // registerDeclarativeRedirect: () => false,
- registerTalerHeaderListener: () => false,
- containsTalerHeaderListener: () => false,
getWalletWebExVersion: () => ({
version: "none",
}),
diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts
index 0bbe805cf..3d67423fd 100644
--- a/packages/taler-wallet-webextension/src/platform/firefox.ts
+++ b/packages/taler-wallet-webextension/src/platform/firefox.ts
@@ -26,9 +26,6 @@ import chromePlatform, {
containsClipboardPermissions as chromeClipContains,
removeClipboardPermissions as chromeClipRemove,
requestClipboardPermissions as chromeClipRequest,
- containsHostPermissions as chromeHostContains,
- requestHostPermissions as chromeHostRequest,
- removeHostPermissions as chromeHostRemove,
} from "./chrome.js";
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
@@ -47,16 +44,8 @@ function isFirefox(): boolean {
return true;
}
-function addPermissionsListener(callback: (p: Permissions) => void): void {
- // throw Error("addPermissionListener is not supported for Firefox");
-}
-
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
- addPermissionsListener,
- containsHostPermissions: chromeHostContains,
- requestHostPermissions: chromeHostRequest,
- removeHostPermissions: chromeHostRemove,
containsClipboardPermissions: chromeClipContains,
removeClipboardPermissions: chromeClipRemove,
requestClipboardPermissions: chromeClipRequest,
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 23614e290..93770312e 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -105,7 +105,8 @@ function useComponentState({
if (state.hasError) {
return {
status: "error",
- error: alertFromError(i18n.str`Could not load the balance`, state),
+ error: alertFromError( i18n,
+ i18n.str`Could not load the balance`, state),
};
}
if (addingAction) {
@@ -153,7 +154,10 @@ export function BalanceView(state: State.Balances): VNode {
const { i18n } = useTranslationContext();
const currencyWithNonZeroAmount = state.balances
.filter((b) => !Amounts.isZero(b.available))
- .map((b) => b.available.split(":")[0]);
+ .map((b) => {
+ b.flags
+ return b.available.split(":")[0]
+ });
if (state.balances.length === 0) {
return (
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
index 8d0e6876e..c698066e7 100644
--- a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -31,7 +31,8 @@ export function NoBalanceHelp({
goToWalletManualWithdraw: ButtonHandler;
}): VNode {
const { i18n } = useTranslationContext();
- return (
+ return (<Fragment>
+
<Paper class={margin}>
<Alert title={i18n.str`Your wallet is empty.`} severity="info">
<Button
@@ -44,5 +45,9 @@ export function NoBalanceHelp({
</Button>
</Alert>
</Paper>
+ <a target="_bank" rel="noreferrer" href="https://demo.taler.net/" style={{ display: "block" }}>
+ <i18n.Translate>Try the demo bank and withdraw test money.</i18n.Translate> »
+ </a>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
index a5b31b387..0388664b3 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
@@ -34,10 +34,6 @@ export const WithdrawalAction = tests.createExample(TestedComponent, {
url: "taler://withdraw/something",
});
-export const TipAction = tests.createExample(TestedComponent, {
- url: "taler://tip/something",
-});
-
export const NotifyAction = tests.createExample(TestedComponent, {
url: "taler://notify-reserve/something",
});
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
index e120334e8..21373c7cd 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -65,26 +65,26 @@ function ContentByUriType({
</Button>
</div>
);
- case TalerUriAction.Reward:
+
+ case TalerUriAction.Refund:
return (
<div>
<p>
- <i18n.Translate>This page has a tip action.</i18n.Translate>
+ <i18n.Translate>This page has a refund action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
- <i18n.Translate>Open tip page</i18n.Translate>
+ <i18n.Translate>Open refund page</i18n.Translate>
</Button>
</div>
);
-
- case TalerUriAction.Refund:
+ case TalerUriAction.AddExchange:
return (
<div>
<p>
- <i18n.Translate>This page has a refund action.</i18n.Translate>
+ <i18n.Translate>This page has a add exchange action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
- <i18n.Translate>Open refund page</i18n.Translate>
+ <i18n.Translate>Open add exchange page</i18n.Translate>
</Button>
</div>
);
@@ -93,8 +93,6 @@ function ContentByUriType({
case TalerUriAction.PayPull:
case TalerUriAction.PayPush:
case TalerUriAction.Restore:
- case TalerUriAction.Auditor:
- case TalerUriAction.Exchange:
return null;
default: {
const error: never = uri;
diff --git a/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
new file mode 100644
index 000000000..d880cbf0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" height="24" width="24">
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
+</svg>
+
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
index d1b1dc374..3b7cbcbb7 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -15,6 +15,7 @@
*/
import { CoreApiResponse, TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
+import type { MessageFromBackend } from "./platform/api.js";
/**
* This will modify all the pages that the user load when navigating with Web Extension enabled
@@ -46,6 +47,9 @@ const suffixIsNotXMLorPDF =
const rootElementIsHTML =
document.documentElement.nodeName &&
document.documentElement.nodeName.toLowerCase() === "html";
+// const pageAcceptsTalerSupport = document.head.querySelector(
+// "meta[name=taler-support]",
+// );
@@ -67,6 +71,7 @@ function convertURIToWebExtensionPath(uri: string) {
const shouldNotInject =
!documentDocTypeIsHTML ||
!suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport ||
!rootElementIsHTML;
const logger = {
@@ -93,16 +98,22 @@ function redirectToTalerActionHandler(element: HTMLMetaElement) {
return;
}
- location.href = convertURIToWebExtensionPath(uri)
+ const walletPage = convertURIToWebExtensionPath(uri)
+ window.location.replace(walletPage)
}
-function injectTalerSupportScript(head: HTMLHeadElement) {
+function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
const meta = head.querySelector("meta[name=taler-support]")
+ if (!meta) return;
+ const content = meta.getAttribute("content");
+ if (!content) return;
+ const features = content.split(",")
- const debugEnabled = meta?.getAttribute("debug") === "true";
+ const debugEnabled = meta.getAttribute("debug") === "true";
+ const hijackEnabled = features.indexOf("uri") !== -1
+ const talerApiEnabled = features.indexOf("api") !== -1 && trusted
const scriptTag = document.createElement("script");
-
scriptTag.setAttribute("async", "false");
const url = new URL(
chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
@@ -111,6 +122,12 @@ function injectTalerSupportScript(head: HTMLHeadElement) {
if (debugEnabled) {
url.searchParams.set("debug", "true");
}
+ if (talerApiEnabled) {
+ url.searchParams.set("api", "true");
+ }
+ if (hijackEnabled) {
+ url.searchParams.set("hijack", "true");
+ }
scriptTag.src = url.href;
try {
@@ -123,12 +140,14 @@ function injectTalerSupportScript(head: HTMLHeadElement) {
export interface ExtensionOperations {
- isInjectionEnabled: {
+ isAutoOpenEnabled: {
request: void;
response: boolean;
};
- isAutoOpenEnabled: {
- request: void;
+ isDomainTrusted: {
+ request: {
+ domain: string;
+ };
response: boolean;
};
}
@@ -178,7 +197,11 @@ async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
let timedout = false;
const timerId = setTimeout(() => {
timedout = true;
- reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {}))
+ reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }))
}, 20 * 1000); //five seconds
try {
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
@@ -200,48 +223,89 @@ async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
});
}
+let notificationPort: chrome.runtime.Port | undefined;
+function listenToWalletBackground(listener: (m: any) => void): () => void {
+ if (notificationPort === undefined) {
+ notificationPort = chrome.runtime.connect({ name: "notifications" });
+ }
+ notificationPort.onMessage.addListener(listener);
+ function removeListener(): void {
+ if (notificationPort !== undefined) {
+ notificationPort.onMessage.removeListener(listener);
+ }
+ }
+ return removeListener;
+}
+
+const loaderSettings = {
+ isAutoOpenEnabled: false,
+ isDomainTrusted: false,
+}
+
function start(
- onTalerMetaTagFound: (listener:(el: HTMLMetaElement)=>void) => void,
- onHeadReady: (listener:(el: HTMLHeadElement)=>void) => void
+ onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => void) => void,
+ onHeadReady: (listener: (el: HTMLHeadElement) => void) => void
) {
- // do not run everywhere, this is just expected to run on html
- // sites
+ // do not run everywhere, this is just expected to run on site
+ // that are aware of taler
if (shouldNotInject) return;
- const isAutoOpenEnabled_promise = callBackground("isAutoOpenEnabled", undefined)
- const isInjectionEnabled_promise = callBackground("isInjectionEnabled", undefined)
+ const isAutoOpenEnabled_promise = callBackground("isAutoOpenEnabled", undefined).then(result => {
+ loaderSettings.isAutoOpenEnabled = result;
+ return result;
+ })
+ const isDomainTrusted_promise = callBackground("isDomainTrusted", {
+ domain: window.location.origin
+ }).then(result => {
+ loaderSettings.isDomainTrusted = result;
+ return result;
+ })
- onTalerMetaTagFound(async (el)=> {
- const enabled = await isAutoOpenEnabled_promise;
- if (!enabled) return;
+ onTalerMetaTagFound(async (el) => {
+ await isAutoOpenEnabled_promise;
+ if (!loaderSettings.isAutoOpenEnabled) {
+ return;
+ }
redirectToTalerActionHandler(el)
})
onHeadReady(async (el) => {
- const enabled = await isInjectionEnabled_promise;
- if (!enabled) return;
- injectTalerSupportScript(el)
+ const trusted = await isDomainTrusted_promise
+ injectTalerSupportScript(el, trusted)
+ })
+
+ listenToWalletBackground((e: MessageFromBackend) => {
+ if (e.type === "web-extension" && e.notification.type === "settings-change") {
+ const settings = e.notification.currentValue
+ loaderSettings.isAutoOpenEnabled = settings.autoOpen
+ }
})
}
+function isCorrectMetaElement(el: HTMLMetaElement): boolean {
+ const name = el.getAttribute("name")
+ if (!name) return false;
+ if (name !== "taler-uri") return false;
+ const uri = el.getAttribute("content");
+ if (!uri) return false;
+ return true
+}
+
/**
* Tries to find taler meta tag ASAP and report
* @param notify
* @returns
*/
-function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
+function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
if (document.head) {
const element = document.head.querySelector("meta[name=taler-uri]")
if (!element) return;
if (!(element instanceof HTMLMetaElement)) return;
- const name = element.getAttribute("name")
- if (!name) return;
- if (name !== "taler-uri") return;
- const uri = element.getAttribute("content");
- if (!uri) return;
- notify(element)
+ if (isCorrectMetaElement(element)) {
+ notify(element)
+ }
return;
}
const obs = new MutationObserver(async function (mutations) {
@@ -250,13 +314,10 @@ function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
if (mut.type === "childList") {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLMetaElement) {
- const name = added.getAttribute("name")
- if (!name) return;
- if (name !== "taler-uri") return;
- const uri = added.getAttribute("content");
- if (!uri) return;
- notify(added)
- obs.disconnect()
+ if (isCorrectMetaElement(added)) {
+ notify(added)
+ obs.disconnect()
+ }
}
});
}
@@ -279,7 +340,7 @@ function onTalerMetaTag(notify: (el: HTMLMetaElement) => void) {
* @param notify
* @returns
*/
-function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
+function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
if (document.head) {
notify(document.head)
return;
@@ -290,7 +351,6 @@ function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
if (mut.type === "childList") {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLHeadElement) {
-
notify(added)
obs.disconnect()
}
@@ -309,4 +369,4 @@ function onHeaderReady(notify: (el: HTMLHeadElement) => void) {
})
}
-start(onTalerMetaTag, onHeaderReady);
+start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
index b70ca2899..8b15380f9 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -20,173 +20,181 @@
* This script will be loaded and run in every page while the
* user us navigating. It must be short, simple and safe.
*/
+(() => {
+ const logger = {
+ debug: (...msg: any[]) => { },
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+ };
-const logger = {
- debug: (...msg: any[]) => { },
- info: (...msg: any[]) =>
- console.log(`${new Date().toISOString()} TALER`, ...msg),
- error: (...msg: any[]) =>
- console.error(`${new Date().toISOString()} TALER`, ...msg),
-};
-
-const documentDocTypeIsHTML =
- window.document.doctype && window.document.doctype.name === "html";
-const suffixIsNotXMLorPDF =
- !window.location.pathname.endsWith(".xml") &&
- !window.location.pathname.endsWith(".pdf");
-const rootElementIsHTML =
- document.documentElement.nodeName &&
- document.documentElement.nodeName.toLowerCase() === "html";
-const pageAcceptsTalerSupport = document.head.querySelector(
- "meta[name=taler-support]",
-);
-
-// this is also checked by the loader
-// but a double check will prevent running and breaking user navigation
-// if loaded from other location
-const shouldNotRun =
- !documentDocTypeIsHTML ||
- !suffixIsNotXMLorPDF ||
- // !pageAcceptsTalerSupport || FIXME: removing this before release for testing
- !rootElementIsHTML;
-
-interface Info {
- extensionId: string;
- protocol: string;
- hostname: string;
-}
-interface API {
- convertURIToWebExtensionPath: (uri: string) => string | undefined;
- anchorOnClick: (ev: MouseEvent) => void;
- registerProtocolHandler: () => void;
-}
-interface TalerSupport {
- info: Readonly<Info>;
- __internal: API;
-}
-
-function buildApi(config: Readonly<Info>): API {
- /**
- * Takes an anchor href that starts with taler:// and
- * returns the path to the web-extension page
- */
- function convertURIToWebExtensionPath(uri: string): string | undefined {
- if (!validateTalerUri(uri)) {
- logger.error(`taler:// URI is invalid: ${uri}`);
- return undefined;
- }
- const host = `${config.protocol}//${config.hostname}`;
- const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
- return `${host}/${path}`;
+ const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+ const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+ const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+ const pageAcceptsTalerSupport = document.head.querySelector(
+ "meta[name=taler-support]",
+ );
+
+ // this is also checked by the loader
+ // but a double check will prevent running and breaking user navigation
+ // if loaded from other location
+ const shouldNotRun =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ !pageAcceptsTalerSupport ||
+ !rootElementIsHTML;
+
+ interface Info {
+ extensionId: string;
+ protocol: string;
+ hostname: string;
+ }
+ interface API {
+ convertURIToWebExtensionPath: (uri: string) => string | undefined;
+ anchorOnClick: (ev: MouseEvent) => void;
+ registerProtocolHandler: () => void;
+ }
+ interface TalerSupport {
+ info: Readonly<Info>;
+ __internal: API;
}
- function anchorOnClick(ev: MouseEvent) {
- if (!(ev.currentTarget instanceof Element)) {
- logger.debug(`onclick: registered in a link that is not an HTML element`);
- return;
+ function buildApi(config: Readonly<Info>): API {
+ /**
+ * Takes an anchor href that starts with taler:// and
+ * returns the path to the web-extension page
+ */
+ function convertURIToWebExtensionPath(uri: string): string | undefined {
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return undefined;
+ }
+ const host = `${config.protocol}//${config.hostname}`;
+ const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
+ return `${host}/${path}`;
}
- const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
- if (!hrefAttr) {
- logger.debug(`onclick: link didn't have href with taler:// uri`);
- return;
+
+ function anchorOnClick(ev: MouseEvent) {
+ if (!(ev.currentTarget instanceof Element)) {
+ logger.debug(`onclick: registered in a link that is not an HTML element`);
+ return;
+ }
+ const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
+ if (!hrefAttr) {
+ logger.debug(`onclick: link didn't have href with taler:// uri`);
+ return;
+ }
+ const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
+ const windowTarget =
+ targetAttr && targetAttr.value ? targetAttr.value : "_self";
+ const page = convertURIToWebExtensionPath(hrefAttr.value);
+ if (!page) {
+ logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
+ return;
+ }
+ // we can use window.open, but maybe some browser will block it?
+ window.open(page, windowTarget);
+ ev.preventDefault();
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ return false;
}
- const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
- const windowTarget =
- targetAttr && targetAttr.value ? targetAttr.value : "_self";
- const page = convertURIToWebExtensionPath(hrefAttr.value);
- if (!page) {
- logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
- return;
+
+ function overrideAllAnchor(root: HTMLElement) {
+ const allAnchors = root.querySelectorAll("a[href^=taler]");
+ logger.debug(`registering taler protocol in ${allAnchors.length} links`);
+ allAnchors.forEach((link) => {
+ if (link instanceof HTMLElement) {
+ link.addEventListener("click", anchorOnClick);
+ }
+ });
}
- // we can use window.open, but maybe some browser will block it?
- window.open(page, windowTarget);
- ev.preventDefault();
- ev.stopPropagation();
- ev.stopImmediatePropagation();
- return false;
- }
- function overrideAllAnchor(root: HTMLElement) {
- const allAnchors = root.querySelectorAll("a[href^=taler]");
- logger.debug(`registering taler protocol in ${allAnchors.length} links`);
- allAnchors.forEach((link) => {
- if (link instanceof HTMLElement) {
- link.addEventListener("click", anchorOnClick);
- }
- });
- }
+ function checkForNewAnchors(
+ mutations: MutationRecord[],
+ observer: MutationObserver,
+ ) {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLElement) {
+ logger.debug(`new element`, added);
+ overrideAllAnchor(added);
+ }
+ });
+ }
+ });
+ }
- function checkForNewAnchors(
- mutations: MutationRecord[],
- observer: MutationObserver,
- ) {
- mutations.forEach((mut) => {
- if (mut.type === "childList") {
- mut.addedNodes.forEach((added) => {
- if (added instanceof HTMLElement) {
- logger.debug(`new element`, added);
- overrideAllAnchor(added);
- }
- });
- }
- });
+ /**
+ * Check of every anchor and observes for new one.
+ * Register the anchor handler when found
+ */
+ function registerProtocolHandler() {
+ if (document.body) overrideAllAnchor(document.body)
+ new MutationObserver(checkForNewAnchors).observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ });
+ }
+
+ return {
+ convertURIToWebExtensionPath,
+ anchorOnClick,
+ registerProtocolHandler,
+ };
}
- /**
- * Check of every anchor and observes for new one.
- * Register the anchor handler when found
- */
- function registerProtocolHandler() {
- overrideAllAnchor(document.body)
- new MutationObserver(checkForNewAnchors).observe(document, {
- childList: true,
- subtree: true,
- attributes: false,
+ function start() {
+ if (shouldNotRun) return;
+ if (!(document.currentScript instanceof HTMLScriptElement)) return;
+
+ const url = new URL(document.currentScript.src);
+ const { protocol, searchParams, hostname } = url;
+ const extensionId = searchParams.get("id") ?? "";
+ const debugEnabled = searchParams.get("debug") === "true";
+ const apiEnabled = searchParams.get("api") === "true";
+ const hijackEnabled = searchParams.get("hijack") === "true";
+
+ const info: Info = Object.freeze({
+ extensionId,
+ protocol,
+ hostname,
});
- }
- return {
- convertURIToWebExtensionPath,
- anchorOnClick,
- registerProtocolHandler,
- };
-}
-
-function start() {
- if (shouldNotRun) return;
- // FIXME: we can remove this if the script caller send information we need
- if (!(document.currentScript instanceof HTMLScriptElement)) return;
-
- const url = new URL(document.currentScript.src);
- const { protocol, searchParams, hostname } = url;
- const extensionId = searchParams.get("id") ?? "";
- const debugEnabled = searchParams.get("debug") === "true";
- if (debugEnabled) {
- logger.debug = logger.info;
- }
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
- const info: Info = Object.freeze({
- extensionId,
- protocol,
- hostname,
- });
- const taler: TalerSupport = {
- info,
- __internal: buildApi(info),
- };
+ const taler: TalerSupport = {
+ info,
+ __internal: buildApi(info),
+ };
+
+ if (apiEnabled) {
+ //@ts-ignore
+ window.taler = taler;
+ }
- //@ts-ignore
- window.taler = taler;
+ if (hijackEnabled) {
+ taler.__internal.registerProtocolHandler();
+ }
+ }
- //default behavior: register on install
- taler.__internal.registerProtocolHandler();
-}
+ // utils functions
+ function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+ }
-// utils functions
-function validateTalerUri(uri: string): boolean {
- return (
- !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
- );
-}
+ start();
+})()
-start();
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index e66693f53..452cc578e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { NotificationType, TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+import { NotificationType, TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient, WalletNotification } from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
WalletCoreOpKeys,
@@ -46,7 +46,7 @@ interface MockHandler {
getCallingQueueState(): "empty" | string;
- notifyEventFromWallet(event: NotificationType): void;
+ notifyEventFromWallet(notif: WalletNotification): void;
}
type CallRecord = WalletCallRecord | BackgroundCallRecord;
@@ -65,7 +65,7 @@ interface BackgroundCallRecord {
}
type Subscriptions = {
- [key in NotificationType]?: VoidFunction;
+ [key in NotificationType]?: (d: WalletNotification) => void;
};
export function createWalletApiMock(): {
@@ -115,9 +115,12 @@ export function createWalletApiMock(): {
},
}),
listener: {
+ trigger: () => {
+
+ },
onUpdateNotification(
mTypes: NotificationType[],
- callback: (() => void) | undefined,
+ callback: ((d: WalletNotification) => void) | undefined,
): () => void {
mTypes.forEach((m) => {
subscriptions[m] = callback;
@@ -164,11 +167,11 @@ export function createWalletApiMock(): {
});
return handler;
},
- notifyEventFromWallet(event: NotificationType): void {
- const callback = subscriptions[event];
+ notifyEventFromWallet(event: WalletNotification): void {
+ const callback = subscriptions[event.type];
if (!callback)
throw Error(`Expected to have a subscription for ${event}`);
- return callback();
+ return callback(event);
},
getCallingQueueState() {
return calls.length === 0 ? "empty" : `${calls.length} left`;
@@ -187,7 +190,7 @@ export function createWalletApiMock(): {
bankCore: new TalerCoreBankHttpClient("/"),
bankIntegration: new TalerBankIntegrationHttpClient("/"),
bankWire: new TalerWireGatewayHttpClient("/",""),
- bankRevenue: new TalerRevenueHttpClient("/",""),
+ bankRevenue: new TalerRevenueHttpClient("/"),
}
children = create(ApiContextProvider, { value, children }, children);
children = create(
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts
index ad4eabf15..d83e6f472 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -15,6 +15,7 @@
*/
import { createElement, VNode } from "preact";
+import { useCallback, useMemo } from "preact/hooks";
function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) {
@@ -26,8 +27,7 @@ function getJsonIfOk(r: Response): Promise<any> {
}
throw new Error(
- `Try another server: (${r.status}) ${
- r.statusText || "internal server error"
+ `Try another server: (${r.status}) ${r.statusText || "internal server error"
}`,
);
}
@@ -89,6 +89,7 @@ export function compose<SType extends { status: string }, PType>(
): (p: PType) => VNode {
function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
function TheComponent(): VNode {
+ //if the function is the same, do not compute
const state = stateHook();
if (typeof state === "function") {
@@ -102,7 +103,9 @@ export function compose<SType extends { status: string }, PType>(
}
// TheComponent.name = `${name}`;
- return TheComponent;
+ return useMemo(() => {
+ return TheComponent
+ }, [stateHook]);
}
return (p: PType) => {
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
index e0b79e060..daa6b425d 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
@@ -14,8 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TalerErrorDetail } from "@gnu-taler/taler-util";
-import { SyncTermsOfServiceResponse } from "@gnu-taler/taler-wallet-core";
+import {
+ SyncTermsOfServiceResponse,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
@@ -24,7 +26,7 @@ import {
TextFieldHandler,
ToggleHandler,
} from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ConfirmProviderView, SelectProviderView } from "./views.js";
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
index cf35abac7..75b8e53c0 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -14,11 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { canonicalizeBaseUrl, Codec } from "@gnu-taler/taler-util";
import {
+ canonicalizeBaseUrl,
+ Codec,
codecForSyncTermsOfServiceResponse,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
@@ -98,42 +99,45 @@ function useUrlState<T>(
}
const constHref = href;
- useDebounceEffect(
- 500,
- constHref == undefined
- ? undefined
- : async () => {
- const req = await fetch(constHref).catch((e) => {
- return setState({
- status: "network-error",
- href: constHref,
- });
- });
- if (!req) return;
+ async function checkURL() {
+ if (!constHref) {
+ return;
+ }
+ const req = await fetch(constHref).catch((e) => {
+ return setState({
+ status: "network-error",
+ href: constHref,
+ });
+ });
+ if (!req) return;
+
+ if (req.status >= 400 && req.status < 500) {
+ setState({
+ status: "client-error",
+ code: req.status,
+ });
+ return;
+ }
+ if (req.status > 500) {
+ setState({
+ status: "server-error",
+ code: req.status,
+ });
+ return;
+ }
- if (req.status >= 400 && req.status < 500) {
- setState({
- status: "client-error",
- code: req.status,
- });
- return;
- }
- if (req.status > 500) {
- setState({
- status: "server-error",
- code: req.status,
- });
- return;
- }
+ const json = await req.json();
+ try {
+ const result = codec.decode(json);
+ setState({ status: "ok", result });
+ } catch (e: any) {
+ setState({ status: "parsing-error", json });
+ }
+ }
- const json = await req.json();
- try {
- const result = codec.decode(json);
- setState({ status: "ok", result });
- } catch (e: any) {
- setState({ status: "parsing-error", json });
- }
- },
+ useDebounceEffect(
+ 500,
+ constHref == undefined ? undefined : checkURL,
[host, path],
);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
index 598ca9369..058f4f460 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
@@ -32,7 +32,13 @@ const props: Props = {
onPaymentRequired: nullFunction,
};
describe("AddBackupProvider states", () => {
- it("should start in 'select-provider' state", async () => {
+ /**
+ * FIXME: this test has inconsistent behavior.
+ * it should always expect one state but for some reason
+ * (maybe race condition) it sometime expect 1 update when
+ * it should no update
+ */
+ it.skip("should start in 'select-provider' state", async () => {
const { handler, TestingContext } = createWalletApiMock();
const hookBehavior = await tests.hookBehaveLikeThis(
@@ -45,6 +51,13 @@ describe("AddBackupProvider states", () => {
expect(state.name.value).eq("");
expect(state.url.value).eq("");
},
+ //FIXME: this shouldn't take 2 updates, just
+ // (state) => {
+ // expect(state.status).equal("select-provider");
+ // if (state.status !== "select-provider") return;
+ // expect(state.name.value).eq("");
+ // expect(state.url.value).eq("");
+ // },
],
TestingContext,
);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
index 69f2a6028..94b32c157 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -14,15 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpResponse } from "@gnu-taler/web-util/browser";
+import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { TextFieldHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
-import { ConfirmView, VerifyView } from "./views.js";
-import { ExchangeListItem } from "@gnu-taler/taler-util";
+import { ConfirmAddExchangeView, VerifyView } from "./views.js";
export interface Props {
currency?: string;
@@ -35,6 +34,14 @@ export type State = State.Loading
| State.Confirm
| State.Verify;
+export type CheckExchangeErrors = {
+ "invalid-version": string;
+ "invalid-currency": string;
+ "not-found": void;
+ "already-active": void;
+ "invalid-protocol": void;
+}
+
export namespace State {
export interface Loading {
status: "loading";
@@ -64,8 +71,9 @@ export namespace State {
onAccept: () => Promise<void>;
url: TextFieldHandler,
+ loading: boolean;
knownExchanges: URL[],
- result: HttpResponse<{ currency_specification: { currency: string }, version: string }, unknown> | undefined,
+ result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> | OperationFailWithBody<CheckExchangeErrors> | undefined,
expectedCurrency: string | undefined,
}
}
@@ -73,7 +81,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
- confirm: ConfirmView,
+ confirm: ConfirmAddExchangeView,
verify: VerifyView,
};
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
index 61f4308f4..4a04f762a 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -14,21 +14,37 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useState, useEffect, useCallback } from "preact/hooks";
-import { Props, State } from "./index.js";
-import { ExchangeEntryStatus, TalerCorebankApi, TalerExchangeApi, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { RecursiveState } from "../../utils/index.js";
-import { HttpResponse, useApiContext } from "@gnu-taler/web-util/browser";
-import { alertFromError } from "../../context/alert.js";
import { withSafe } from "../../mui/handlers.js";
+import { RecursiveState } from "../../utils/index.js";
+import { CheckExchangeErrors, Props, State } from "./index.js";
+
+function urlFromInput(str: string): URL {
+ let result: URL;
+ try {
+ result = new URL(str)
+ } catch (original) {
+ try {
+ result = new URL(`https://${str}`)
+ } catch (e) {
+ throw original
+ }
+ }
+ if (!result.pathname.endsWith("/")) {
+ result.pathname = result.pathname + "/";
+ }
+ result.search = "";
+ result.hash = "";
+ return result;
+}
export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState<State> {
- const [verified, setVerified] = useState<
- { url: string; config: { currency_specification: {currency: string}, version: string} } | undefined
- >(undefined);
+ const [verified, setVerified] = useState<string>();
const api = useBackendContext();
const hook = useAsyncAsHook(() =>
@@ -38,20 +54,49 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
const used = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Used);
const preset = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Preset);
-
if (!verified) {
return (): State => {
- const { request } = useApiContext();
- const ccc = useCallback(async (str: string) => {
- const c = canonicalizeBaseUrl(str)
- const found = used.findIndex((e) => e.exchangeBaseUrl === c);
+ const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) {
+ const baseUrl = urlFromInput(str)
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-protocol", undefined)
+ }
+ const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href);
if (found !== -1) {
- throw Error("This exchange is already active")
+ return opKnownFailureWithBody<CheckExchangeErrors>("already-active", undefined);
+ }
+
+ /**
+ * FIXME: For some reason typescript doesn't like the next BrowserFetchHttpLib
+ *
+ * │ src/wallet/AddExchange/state.ts(68,63): error TS2345: Argument of type 'BrowserFetchHttpLib' is not assignable to parameter of ty
+ * │ Types of property 'fetch' are incompatible.
+ * │ Type '(requestUrl: string, options?: HttpRequestOptions | undefined) => Promise<HttpResponse>' is not assignable to type '(ur
+ * │ Types of parameters 'options' and 'opt' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", { wi
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", {
+ * │ Types of property 'cancellationToken' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellation
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellati
+ * │ Types have separate declarations of a private property '_isCancelled'.
+ *
+ */
+ const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any);
+ const config = await api.getConfig()
+ if (config.type === "fail") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined)
}
- const result = await request<{ currency_specification: {currency: string}, version: string}>(c, "/keys")
- return result
+ if (!api.isCompatible(config.body.version)) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
+ }
+ if (currency !== undefined && currency !== config.body.currency) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-currency", config.body.currency)
+ }
+ const keys = await api.getKeys()
+ return keys
}, [used])
- const { result, value: url, update, error: requestError } = useDebounce<HttpResponse<{ currency_specification: {currency: string}, version: string}, unknown>>(ccc, noDebounce ?? false)
+
+ const { result, value: url, loading, update, error: requestError } = useDebounce(checkExchangeBaseUrl_memo, noDebounce ?? false)
const [inputError, setInputError] = useState<string>()
return {
@@ -60,10 +105,11 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
onCancel: onBack,
expectedCurrency: currency,
onAccept: async () => {
- if (!url || !result || !result.ok) return;
- setVerified({ url, config: result.data })
+ if (!result || result.type !== "ok") return;
+ setVerified(result.body.base_url)
},
result,
+ loading,
knownExchanges: preset.map(e => new URL(e.exchangeBaseUrl)),
url: {
value: url ?? "",
@@ -79,7 +125,7 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
async function onConfirm() {
if (!verified) return;
await api.wallet.call(WalletApiOperation.AddExchange, {
- exchangeBaseUrl: canonicalizeBaseUrl(verified.url),
+ exchangeBaseUrl: canonicalizeBaseUrl(verified),
forceUpdate: true,
});
onBack();
@@ -90,7 +136,7 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
error: undefined,
onCancel: onBack,
onConfirm,
- url: verified.url
+ url: verified
};
}
@@ -101,7 +147,7 @@ function useDebounce<T>(
disabled: boolean,
): {
loading: boolean;
- error?: string;
+ error?: Error;
value: string | undefined;
result: T | undefined;
update: (s: string) => void;
@@ -110,9 +156,9 @@ function useDebounce<T>(
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<T | undefined>(undefined);
- const [error, setError] = useState<string | undefined>(undefined);
+ const [error, setError] = useState<Error | undefined>(undefined);
- const [handler, setHandler] = useState<any | undefined>(undefined);
+ const [handler, setHandler] = useState<number | undefined>(undefined);
if (!disabled) {
useEffect(() => {
@@ -126,15 +172,18 @@ function useDebounce<T>(
setResult(result);
setError(undefined);
setLoading(false);
- } catch (e) {
- const errorMessage =
- e instanceof Error ? e.message : `unknown error: ${e}`;
- setError(errorMessage);
+ } catch (er) {
+ if (er instanceof Error) {
+ setError(er);
+ } else {
+ // @ts-expect-error cause still not in typescript
+ setError(new Error('unkown error on debounce', { cause: er }))
+ }
setLoading(false);
setResult(undefined);
}
}, 500);
- setHandler(h);
+ setHandler(h as unknown as number);
}, [value, setHandler, onTrigger]);
}
@@ -143,7 +192,7 @@ function useDebounce<T>(
loading: loading,
result: result,
value: value,
- update: disabled ? onTrigger : setValue ,
+ update: disabled ? onTrigger : setValue,
};
}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
index 4e2610743..f205b6415 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
@@ -19,8 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import * as tests from "@gnu-taler/web-util/testing";
-import { ConfirmView, VerifyView } from "./views.js";
export default {
title: "example",
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
index f17872779..d0e78a94e 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
@@ -19,18 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { expect } from "chai";
-import { createWalletApiMock } from "../../test-utils.js";
-import * as tests from "@gnu-taler/web-util/testing";
-import { Props } from "./index.js";
-import { useComponentState } from "./state.js";
-import { nullFunction } from "../../mui/handlers.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
ExchangeEntryStatus,
ExchangeTosStatus,
ExchangeUpdateStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
const props: Props = {
onBack: nullFunction,
noDebounce: true,
@@ -48,12 +49,20 @@ describe("AddExchange states", () => {
{
exchangeBaseUrl: "http://exchange.local/",
ageRestrictionOptions: [],
- scopeInfo: undefined,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://exchange.local/",
+ },
+ masterPub: "123qwe123",
currency: "ARS",
exchangeEntryStatus: ExchangeEntryStatus.Ephemeral,
tosStatus: ExchangeTosStatus.Pending,
exchangeUpdateStatus: ExchangeUpdateStatus.UnavailableUpdate,
paytoUris: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
},
],
},
@@ -85,116 +94,116 @@ describe("AddExchange states", () => {
expect(handler.getCallingQueueState()).eq("empty");
});
- it("should not be able to add a known exchange", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- handler.addWalletCallResponse(
- WalletApiOperation.ListExchanges,
- {},
- {
- exchanges: [
- {
- exchangeBaseUrl: "http://exchange.local/",
- ageRestrictionOptions: [],
- scopeInfo: undefined,
- currency: "ARS",
- exchangeEntryStatus: ExchangeEntryStatus.Used,
- tosStatus: ExchangeTosStatus.Pending,
- exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
- paytoUris: [],
- },
- ],
- },
- );
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- (state) => {
- expect(state.status).equal("verify");
- if (state.status !== "verify") return;
- expect(state.url.value).eq("");
- expect(state.expectedCurrency).is.undefined;
- expect(state.result).is.undefined;
- },
- (state) => {
- expect(state.status).equal("verify");
- if (state.status !== "verify") return;
- expect(state.url.value).eq("");
- expect(state.expectedCurrency).is.undefined;
- expect(state.result).is.undefined;
- expect(state.error).is.undefined;
- expect(state.url.onInput).is.not.undefined;
- if (!state.url.onInput) return;
- state.url.onInput("http://exchange.local/");
- },
- (state) => {
- expect(state.status).equal("verify");
- if (state.status !== "verify") return;
- expect(state.url.value).eq("");
- expect(state.expectedCurrency).is.undefined;
- expect(state.result).is.undefined;
- expect(state.url.error).eq("This exchange is already active");
- expect(state.url.onInput).is.not.undefined;
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
-
- it("should be able to add a preset exchange", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- handler.addWalletCallResponse(
- WalletApiOperation.ListExchanges,
- {},
- {
- exchanges: [
- {
- exchangeBaseUrl: "http://exchange.local/",
- ageRestrictionOptions: [],
- scopeInfo: undefined,
- currency: "ARS",
- exchangeEntryStatus: ExchangeEntryStatus.Preset,
- tosStatus: ExchangeTosStatus.Pending,
- exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
- paytoUris: [],
- },
- ],
- },
- );
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- (state) => {
- expect(state.status).equal("verify");
- if (state.status !== "verify") return;
- expect(state.url.value).eq("");
- expect(state.expectedCurrency).is.undefined;
- expect(state.result).is.undefined;
- },
- (state) => {
- expect(state.status).equal("verify");
- if (state.status !== "verify") return;
- expect(state.url.value).eq("");
- expect(state.expectedCurrency).is.undefined;
- expect(state.result).is.undefined;
- expect(state.error).is.undefined;
- expect(state.url.onInput).is.not.undefined;
- if (!state.url.onInput) return;
- state.url.onInput("http://exchange.local/");
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
+ // it("should not be able to add a known exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Used,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.url.error).eq("This exchange is already active");
+ // expect(state.url.onInput).is.not.undefined;
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+
+ // it("should be able to add a preset exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Preset,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
index 53a46fe02..f6537bc68 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -16,19 +16,25 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { Input, LightText, SubTitle, Title, WarningBox } from "../../components/styled/index.js";
+import {
+ Input,
+ LightText,
+ SubTitle,
+ Title,
+ WarningBox,
+} from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
-
+import { assertUnreachable } from "@gnu-taler/taler-util";
export function VerifyView({
expectedCurrency,
onCancel,
onAccept,
result,
+ loading,
knownExchanges,
url,
}: State.Verify): VNode {
@@ -53,29 +59,74 @@ export function VerifyView({
</i18n.Translate>
</LightText>
)}
- {result && (
- <LightText>
- <i18n.Translate>
- An exchange has been found! Review the information and click next
- </i18n.Translate>
- </LightText>
- )}
- {result && result.ok && expectedCurrency && expectedCurrency !== result.data.currency_specification.currency && (
- <WarningBox>
- <i18n.Translate>
- This exchange doesn&apos;t match the expected currency
- <b>{expectedCurrency}</b>
- </i18n.Translate>
- </WarningBox>
- )}
- {result && !result.ok && !result.loading && (
- <ErrorMessage
- title={i18n.str`Unable to verify this exchange`}
- description={result.message}
- />
- )}
+ {(() => {
+ if (!result) return;
+ if (result.type == "ok") {
+ return (
+ <LightText>
+ <i18n.Translate>
+ An exchange has been found! Review the information and click
+ next
+ </i18n.Translate>
+ </LightText>
+ );
+ }
+ switch (result.case) {
+ case "already-active": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange is already in your list.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-protocol": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ Only exchange accessible through "http" and "https" are
+ allowed.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-version": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange protocol version is not supported: "
+ {result.body}".
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-currency": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange currency "{result.body}" doesn&apos;t match
+ the expected currency {expectedCurrency}.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "not-found": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ No exchange found in that URL.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ default: {
+ assertUnreachable(result.case);
+ }
+ }
+ })()}
<p>
- <Input invalid={result && !result.ok} >
+ <Input invalid={result && result.type !== "ok"}>
<label>URL</label>
<input
type="text"
@@ -83,36 +134,36 @@ export function VerifyView({
value={url.value}
onInput={(e) => {
if (url.onInput) {
- url.onInput(e.currentTarget.value)
+ url.onInput(e.currentTarget.value);
}
}}
/>
</Input>
- {result && result.loading && (
+ {loading && (
<div>
<i18n.Translate>loading</i18n.Translate>...
</div>
)}
- {result && result.ok && !result.loading && (
+ {result && result.type === "ok" && (
<Fragment>
<Input>
<label>
<i18n.Translate>Version</i18n.Translate>
</label>
- <input type="text" disabled value={result.data.version} />
+ <input type="text" disabled value={result.body.version} />
</Input>
<Input>
<label>
<i18n.Translate>Currency</i18n.Translate>
</label>
- <input type="text" disabled value={result.data.currency_specification.currency} />
+ <input type="text" disabled value={result.body.currency} />
</Input>
</Fragment>
)}
</p>
- {url.error && (
+ {url.value && url.error && (
<ErrorMessage
- title={i18n.str`Can't use this URL`}
+ title={i18n.str`Can't use the URL: "${url.value}"`}
description={url.error}
/>
)}
@@ -123,12 +174,7 @@ export function VerifyView({
</Button>
<Button
variant="contained"
- disabled={
- !result ||
- result.loading ||
- !result.ok ||
- (!!expectedCurrency && expectedCurrency !== result.data.currency_specification.currency)
- }
+ disabled={!result || result.type !== "ok"}
onClick={onAccept}
>
<i18n.Translate>Next</i18n.Translate>
@@ -136,14 +182,22 @@ export function VerifyView({
</footer>
<section>
<ul>
- {knownExchanges.map(ex => {
- return <li><a href="#" onClick={(e) => {
- if (url.onInput) {
- url.onInput(ex.href)
- }
- e.preventDefault()
- }}>
- {ex.href}</a></li>
+ {knownExchanges.map((ex) => {
+ return (
+ <li key={ex.href}>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (url.onInput) {
+ url.onInput(ex.href);
+ }
+ e.preventDefault();
+ }}
+ >
+ {ex.href}
+ </a>
+ </li>
+ );
})}
</ul>
</section>
@@ -151,8 +205,7 @@ export function VerifyView({
);
}
-
-export function ConfirmView({
+export function ConfirmAddExchangeView({
url,
onCancel,
onConfirm,
@@ -173,8 +226,7 @@ export function ConfirmView({
</div>
</section>
-
- <TermsOfService key="terms" exchangeUrl={url} >
+ <TermsOfService key="terms" exchangeUrl={url}>
<footer>
<Button
key="cancel"
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
index fc3a0916c..dd1777fd1 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
@@ -66,8 +66,6 @@ export function AddNewActionView({ onCancel }: Props): VNode {
return <i18n.Translate>Open pay page</i18n.Translate>;
case TalerUriAction.Refund:
return <i18n.Translate>Open refund page</i18n.Translate>;
- case TalerUriAction.Reward:
- return <i18n.Translate>Open tip page</i18n.Translate>;
case TalerUriAction.Withdraw:
return <i18n.Translate>Open withdraw page</i18n.Translate>;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index df0e968b9..893122c0f 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -23,7 +23,9 @@
import {
Amounts,
TalerUri,
+ TalerUriAction,
TranslatedString,
+ parseTalerUri,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
import {
@@ -59,7 +61,6 @@ import { PaymentPage } from "../cta/Payment/index.js";
import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js";
import { RefundPage } from "../cta/Refund/index.js";
-import { TipPage } from "../cta/Reward/index.js";
import { TransferCreatePage } from "../cta/TransferCreate/index.js";
import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import {
@@ -82,6 +83,10 @@ import { QrReaderPage } from "./QrReader.js";
import { SettingsPage } from "./Settings.js";
import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.js";
+import { WalletActivity } from "../components/WalletActivity.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { DevExperimentPage } from "../cta/DevExperiment/index.js";
+import { ConfirmAddExchangeView } from "./AddExchange/views.js";
export function Application(): VNode {
const { i18n } = useTranslationContext();
@@ -173,6 +178,27 @@ export function Application(): VNode {
)}
/>
<Route
+ path={Pages.searchHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ search
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
path={Pages.sendCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance" goToURL={redirectToURL}>
@@ -366,20 +392,6 @@ export function Application(): VNode {
)}
/>
<Route
- path={Pages.ctaTips}
- component={({ talerUri }: { talerUri: string }) => (
- <CallToActionTemplate title={i18n.str`Digital cash tip`}>
- <TipPage
- talerTipUri={decodeURIComponent(talerUri)}
- onCancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- </CallToActionTemplate>
- )}
- />
- <Route
path={Pages.ctaWithdraw}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
@@ -510,7 +522,40 @@ export function Application(): VNode {
</CallToActionTemplate>
)}
/>
-
+ <Route
+ path={Pages.ctaExperiment}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Development experiment`}>
+ <DevExperimentPage
+ talerExperimentUri={decodeURIComponent(talerUri)}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onSuccess={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaAddExchange}
+ component={({ talerUri }: { talerUri: string }) => {
+ const tUri = parseTalerUri(decodeURIComponent(talerUri))
+ const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined
+ if (!baseUrl) {
+ redirectTo(Pages.balanceHistory({}))
+ return <div>
+ invalid url {talerUri}
+ </div>
+ }
+ return <CallToActionTemplate title={i18n.str`Add exchange`}>
+ <ConfirmAddExchangeView
+ url={baseUrl}
+ status="confirm"
+ error={undefined}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onConfirm={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ }}
+ />
{/**
* NOT FOUND
* all redirects should be at the end
@@ -525,6 +570,9 @@ export function Application(): VNode {
component={() => <Redirect to={Pages.balanceHistory({})} />}
/>
</Router>
+ <EnabledBySettings name="showWalletActivity">
+ <WalletActivity />
+ </EnabledBySettings>
</IoCProviderForRuntime>
</TranslationProvider>
);
@@ -541,17 +589,17 @@ function Redirect({ to }: { to: string }): null {
return null;
}
-function matchesRoute(url: string, route: string): boolean {
- type MatcherFunc = (
- url: string,
- route: string,
- opts: any,
- ) => Record<string, string> | false;
+// function matchesRoute(url: string, route: string): boolean {
+// type MatcherFunc = (
+// url: string,
+// route: string,
+// opts: any,
+// ) => Record<string, string> | false;
- const internalPreactMatcher: MatcherFunc = (Router as any).exec;
- const result = internalPreactMatcher(url, route, {});
- return !result ? false : true;
-}
+// const internalPreactMatcher: MatcherFunc = (Router as any).exec;
+// const result = internalPreactMatcher(url, route, {});
+// return !result ? false : true;
+// }
function CallToActionTemplate({
title,
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index ae160a30c..cc7c9af67 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -19,19 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
-import { addDays } from "date-fns";
-import {
- BackupView as TestedComponent,
- ShowRecoveryInfo,
-} from "./BackupPage.js";
-import * as tests from "@gnu-taler/web-util/testing";
import {
AbsoluteTime,
AmountString,
+ ProviderPaymentType,
TalerPreciseTimestamp,
- TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { addDays } from "date-fns";
+import {
+ ShowRecoveryInfo,
+ BackupView as TestedComponent,
+} from "./BackupPage.js";
export default {
title: "backup",
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index 5ae52db6f..8a3710f69 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -16,23 +16,22 @@
import {
AbsoluteTime,
- constructRecoveryUri,
- stringifyRestoreUri,
-} from "@gnu-taler/taler-util";
-import {
ProviderInfo,
ProviderPaymentPaid,
ProviderPaymentStatus,
ProviderPaymentType,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+ stringifyRestoreUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import {
differenceInMonths,
formatDuration,
intervalToDuration,
} from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
import { ErrorAlertView } from "../components/CurrentAlerts.js";
import { Loading } from "../components/Loading.js";
import { QR } from "../components/QR.js";
@@ -48,10 +47,8 @@ import {
} from "../components/styled/index.js";
import { alertFromError } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
-import { Pages } from "../NavigationBar.js";
interface Props {
onAddProvider: () => Promise<void>;
@@ -124,6 +121,7 @@ export function BackupPage({ onAddProvider }: Props): VNode {
return (
<ErrorAlertView
error={alertFromError(
+ i18n,
i18n.str`Could not load backup providers`,
status,
)}
@@ -325,12 +323,12 @@ function daysUntil(d: AbsoluteTime): string {
duration?.years
? "years"
: duration?.months
- ? "months"
- : duration?.days
- ? "days"
- : duration.hours
- ? "hours"
- : "minutes",
+ ? "months"
+ : duration?.days
+ ? "days"
+ : duration.hours
+ ? "hours"
+ : "minutes",
],
});
return `${str}`;
@@ -353,6 +351,6 @@ function getStatusPaidOrder(
return a.paidUntil.t_ms === "never"
? -1
: b.paidUntil.t_ms === "never"
- ? 1
- : a.paidUntil.t_ms - b.paidUntil.t_ms;
+ ? 1
+ : a.paidUntil.t_ms - b.paidUntil.t_ms;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
index 8c773186e..97b2ab517 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -60,8 +60,8 @@ export function useComponentState({
parsed !== undefined
? parsed
: currency !== undefined
- ? Amounts.zeroOfCurrency(currency)
- : undefined;
+ ? Amounts.zeroOfCurrency(currency)
+ : undefined;
// const [accountIdx, setAccountIdx] = useState<number>(0);
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
@@ -83,7 +83,8 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n.str`Could not load balance information`, hook),
+ error: alertFromError(i18n,
+ i18n.str`Could not load balance information`, hook),
};
}
const { accounts, balances } = hook.response;
@@ -169,6 +170,7 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load fee for amount ${amountStr}`,
hook,
),
@@ -193,8 +195,8 @@ export function useComponentState({
const amountError = !isDirty
? undefined
: Amounts.cmp(balance, amount) === -1
- ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
- : undefined;
+ ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
+ : undefined;
const unableToDeposit =
Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
index a5d44e872..d4e270a6c 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -52,22 +52,22 @@ export function useComponentState(props: Props): RecursiveState<State> {
const previous: Contact[] = true
? []
: [
- {
- name: "International Bank",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- {
- name: "Max",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- {
- name: "Alex",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- ];
+ {
+ name: "International Bank",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ {
+ name: "Max",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ {
+ name: "Alex",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ ];
if (!amount) {
return () => {
@@ -87,7 +87,8 @@ export function useComponentState(props: Props): RecursiveState<State> {
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n.str`Could not load exchanges`, hook),
+ error: alertFromError(i18n,
+ i18n.str`Could not load exchanges`, hook),
};
}
const currencies: Record<string, string> = {};
@@ -127,8 +128,8 @@ export function useComponentState(props: Props): RecursiveState<State> {
onClick: invalid
? undefined
: pushAlertOnError(async () => {
- props.goToWalletBankDeposit(currencyAndAmount);
- }),
+ props.goToWalletBankDeposit(currencyAndAmount);
+ }),
},
selectMax: {
onClick: pushAlertOnError(async () => {
@@ -145,8 +146,8 @@ export function useComponentState(props: Props): RecursiveState<State> {
onClick: invalid
? undefined
: pushAlertOnError(async () => {
- props.goToWalletWalletSend(currencyAndAmount);
- }),
+ props.goToWalletWalletSend(currencyAndAmount);
+ }),
},
amountHandler: {
onInput: pushAlertOnError(async (s) => setAmount(s)),
@@ -168,22 +169,22 @@ export function useComponentState(props: Props): RecursiveState<State> {
onClick: invalid
? undefined
: pushAlertOnError(async () => {
- props.goToWalletManualWithdraw(currencyAndAmount);
- }),
+ props.goToWalletManualWithdraw(currencyAndAmount);
+ }),
},
goToBank: {
onClick: invalid
? undefined
: pushAlertOnError(async () => {
- props.goToWalletManualWithdraw(currencyAndAmount);
- }),
+ props.goToWalletManualWithdraw(currencyAndAmount);
+ }),
},
goToWallet: {
onClick: invalid
? undefined
: pushAlertOnError(async () => {
- props.goToWalletWalletInvoice(currencyAndAmount);
- }),
+ props.goToWalletWalletInvoice(currencyAndAmount);
+ }),
},
amountHandler: {
onInput: pushAlertOnError(async (s) => setAmount(s)),
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
index d42a3477d..683378613 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -25,10 +25,11 @@ import {
ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { expect } from "chai";
import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
@@ -36,12 +37,20 @@ import { useComponentState } from "./state.js";
const exchangeArs: ExchangeListItem = {
currency: "ARS",
exchangeBaseUrl: "http://",
- scopeInfo: undefined,
+ masterPub: "123qwe123",
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://",
+ },
tosStatus: ExchangeTosStatus.Accepted,
exchangeEntryStatus: ExchangeEntryStatus.Used,
exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
paytoUris: [],
ageRestrictionOptions: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
};
describe("Destination selection states", () => {
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index f8e2c6707..8a74a20f1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js";
@@ -25,7 +26,6 @@ import {
LinkPrimary,
SvgIcon,
} from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
import { Paper } from "../../mui/Paper.js";
@@ -34,8 +34,6 @@ import arrowIcon from "../../svg/chevron-down.inline.svg";
import bankIcon from "../../svg/ri-bank-line.inline.svg";
import { assertUnreachable } from "../../utils/index.js";
import { Contact, State } from "./index.js";
-import { useEffect } from "preact/hooks";
-import { Checkbox } from "../../components/Checkbox.js";
export function SelectCurrencyView({
currencies,
@@ -171,7 +169,9 @@ const CircleDiv = styled.div`
text-align: center;
text-decoration: none;
text-transform: uppercase;
- transition: background-color 0.15s ease, border-color 0.15s ease,
+ transition:
+ background-color 0.15s ease,
+ border-color 0.15s ease,
color 0.15s ease;
font-size: 16px;
background-color: #86a7bd1a;
@@ -277,6 +277,16 @@ export function ReadyGetView({
</Button>
</Paper>
</Grid>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate>
+ </p>
+ <a href={Pages.qr}>
+ <i18n.Translate>Enter URI here</i18n.Translate>
+ </a>
+ </Paper>
+ </Grid>
</Grid>
</Grid>
</Container>
@@ -303,7 +313,7 @@ export function ReadySendView({
required
handler={amountHandler}
/>
- <EnabledBySettings name="advanceMode">
+ <EnabledBySettings name="advancedMode">
<Button onClick={selectMax.onClick}>
<i18n.Translate>Send all</i18n.Translate>
</Button>
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
index 2ca5305f5..e7c9111fd 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
@@ -19,10 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { PendingTaskType, TaskId } from "@gnu-taler/taler-wallet-core";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
-import { View as TestedComponent } from "./DeveloperPage.js";
-import { AbsoluteTime, PendingIdStr } from "@gnu-taler/taler-util";
+import { DeveloperPage as TestedComponent } from "./DeveloperPage.js";
export default {
title: "developer",
@@ -36,8 +35,8 @@ export const AllOff = tests.createExample(TestedComponent, {
onDownloadDatabase: async () => "this is the content of the database",
operations: [
{
- id: " " as TaskId,
- type: PendingTaskType.ExchangeUpdate,
+ id: " ",
+ type: "exchange-update",
exchangeBaseUrl: "http://exchange.url.",
givesLifeness: false,
lastError: undefined,
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 0a01b8a95..7b6ac8895 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -19,73 +19,39 @@ import {
Amounts,
CoinDumpJson,
CoinStatus,
- ExchangeListItem,
+ ExchangeTosStatus,
LogLevel,
NotificationType,
+ ScopeType,
+ stringifyWithdrawExchange,
} from "@gnu-taler/taler-util";
-import {
- PendingTaskInfo,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { Checkbox } from "../components/Checkbox.js";
import { SelectList } from "../components/SelectList.js";
import { Time } from "../components/Time.js";
-import { NotifyUpdateFadeOut } from "../components/styled/index.js";
+import { ActiveTasksTable } from "../components/WalletActivity.js";
+import {
+ DestructiveText,
+ LinkPrimary,
+ NotifyUpdateFadeOut,
+ SubTitle,
+ SuccessText,
+ WarningText,
+} from "../components/styled/index.js";
+import { useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js";
-export function DeveloperPage(): VNode {
-
- const listenAllEvents = Array.from<NotificationType>({ length: 1 });
-
- const api = useBackendContext();
-
- const response = useAsyncAsHook(async () => {
- const op = await api.wallet.call(
- WalletApiOperation.GetPendingOperations,
- {},
- );
- const c = await api.wallet.call(WalletApiOperation.DumpCoins, {});
- const ex = await api.wallet.call(WalletApiOperation.ListExchanges, {});
- return {
- operations: op.pendingOperations,
- coins: c.coins,
- exchanges: ex.exchanges,
- };
- });
-
- useEffect(() => {
- return api.listener.onUpdateNotification(listenAllEvents, response?.retry);
- });
-
- const nonResponse = { operations: [], coins: [], exchanges: [] };
- const { operations, coins, exchanges } =
- response === undefined
- ? nonResponse
- : response.hasError
- ? nonResponse
- : response.response;
-
- return (
- <View
- operations={operations}
- coins={coins}
- exchanges={exchanges}
- onDownloadDatabase={async () => {
- const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
- return JSON.stringify(db);
- }}
- />
- );
-}
-
type CoinsInfo = CoinDumpJson["coins"];
type CalculatedCoinfInfo = {
// ageKeysCount: number | undefined;
@@ -103,27 +69,21 @@ type SplitedCoinInfo = {
};
export interface Props {
- operations: PendingTaskInfo[];
- coins: CoinsInfo;
- exchanges: ExchangeListItem[];
- onDownloadDatabase: () => Promise<string>;
+ // FIXME: Pending operations don't exist anymore.
}
function hashObjectId(o: any): string {
return JSON.stringify(o);
}
-export function View({
- operations,
- coins,
- onDownloadDatabase,
-}: Props): VNode {
+export function DeveloperPage({}: Props): VNode {
const { i18n } = useTranslationContext();
const [downloadedDatabase, setDownloadedDatabase] = useState<
{ time: Date; content: string } | undefined
>(undefined);
async function onExportDatabase(): Promise<void> {
- const content = await onDownloadDatabase();
+ const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
+ const content = JSON.stringify(db);
setDownloadedDatabase({
time: new Date(),
content,
@@ -133,15 +93,31 @@ export function View({
const fileRef = useRef<HTMLInputElement>(null);
async function onImportDatabase(str: string): Promise<void> {
- return api.wallet.call(WalletApiOperation.ImportDb, {
+ await api.wallet.call(WalletApiOperation.ImportDb, {
dump: JSON.parse(str),
});
}
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
- const hook = useAsyncAsHook(() =>
- api.wallet.call(WalletApiOperation.ListExchanges, {}),
- );
+ const listenAllEvents = Array.from<NotificationType>({ length: 1 });
+ // listenAllEvents.includes = () => true
+
+ const hook = useAsyncAsHook(async () => {
+ const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
+ const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
+ const coins = await api.wallet.call(WalletApiOperation.DumpCoins, {});
+ return { exchanges: list.exchanges, version, coins };
+ });
const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];
+ const coins = hook && !hook.hasError ? hook.response.coins.coins : [];
+
+ useEffect(() => {
+ return api.listener.onUpdateNotification(listenAllEvents, (ev) => {
+ console.log("event", ev);
+ return hook?.retry();
+ });
+ });
const currencies: { [ex: string]: string } = {};
const money_by_exchange = coins.reduce(
@@ -206,30 +182,6 @@ export function View({
<Grid item>
<Button
variant="contained"
- onClick={() => {
- return api.background.call("sum", [1, 2, 3]).then((r) => {
- console.log("SUM", r);
- });
- }}
- >
- <i18n.Translate>sum 123</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={() => {
- return api.background.call("freeze", 4000).then(() => {
- console.log("WAIT");
- });
- }}
- >
- <i18n.Translate>freeze 4000</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
onClick={async () => fileRef?.current?.click()}
>
<i18n.Translate>import database</i18n.Translate>
@@ -258,78 +210,6 @@ export function View({
</Button>
</Grid>
<Grid item>
- <Button variant="contained" onClick={async () => {
- api.background.call("toggleHeaderListener", true)
- }}>
- <i18n.Translate>enable header listener</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button variant="contained" onClick={async () => {
- api.background.call("toggleHeaderListener", false)
- }}>
- <i18n.Translate>disable header listener</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
- navigator.registerProtocolHandler(
- "taler",
- `${window.location.origin}/static/wallet.html#/cta/withdraw?talerWithdrawUri=%s`,
- );
- }}
- >
- <i18n.Translate>Register taler:// handler</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
- const n = navigator as any;
- if ("unregisterProtocolHandler" in n) {
- n.unregisterProtocolHandler(
- "taler",
- `${window.location.origin}/static/wallet.html#/cta/withdraw?talerWithdrawUri=%s`,
- );
- }
- }}
- >
- <i18n.Translate>Remove taler:// handler</i18n.Translate>
- </Button>
- </Grid>{" "}
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
- navigator.registerProtocolHandler(
- "ext+taler",
- `${window.location.origin}/static/wallet.html#/cta/withdraw?talerWithdrawUri=%s`,
- );
- }}
- >
- <i18n.Translate>Register ext+taler:// handler</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
- <Button
- variant="contained"
- onClick={async () => {
- const n = navigator as any;
- if ("unregisterProtocolHandler" in n) {
- n.unregisterProtocolHandler(
- "ext+taler",
- `${window.location.origin}/static/wallet.html#/cta/withdraw?talerWithdrawUri=%s`,
- );
- }
- }}
- >
- <i18n.Translate>Remove ext+taler:// handler</i18n.Translate>
- </Button>
- </Grid>
- <Grid item>
<Button
variant="contained"
onClick={async () => {
@@ -359,6 +239,242 @@ export function View({
</Button>
</Grid>{" "}
</Grid>
+ {downloadedDatabase && (
+ <div>
+ <i18n.Translate>
+ Database exported at{" "}
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ downloadedDatabase.time.getTime(),
+ )}
+ format="yyyy/MM/dd HH:mm:ss"
+ />{" "}
+ <a
+ href={`data:text/plain;charset=utf-8;base64,${toBase64(
+ downloadedDatabase.content,
+ )}`}
+ download={`taler-wallet-database-${format(
+ downloadedDatabase.time,
+ "yyyy/MM/dd_HH:mm",
+ )}.json`}
+ >
+ <i18n.Translate>click here</i18n.Translate>
+ </a>{" "}
+ to download
+ </i18n.Translate>
+ </div>
+ )}
+ <Checkbox
+ label={i18n.str`Inject Taler support in all pages`}
+ name="inject"
+ description={
+ <i18n.Translate>
+ Enabling this option will make `window.taler` be available in all
+ sites
+ </i18n.Translate>
+ }
+ enabled={settings.injectTalerSupport!}
+ onToggle={safely("update support injection", async () => {
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport);
+ })}
+ />
+
+ <SubTitle>
+ <i18n.Translate>Exchange Entries</i18n.Translate>
+ </SubTitle>
+ {!exchangeList || !exchangeList.length ? (
+ <div>
+ <i18n.Translate>No exchange yet</i18n.Translate>
+ </div>
+ ) : (
+ <Fragment>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Currency</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Terms of Service</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Last Update</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Actions</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {exchangeList.map((e, idx) => {
+ function TosStatus(): VNode {
+ switch (e.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ return (
+ <SuccessText>
+ <i18n.Translate>ok</i18n.Translate>
+ </SuccessText>
+ );
+ case ExchangeTosStatus.Pending:
+ return (
+ <WarningText>
+ <i18n.Translate>pending</i18n.Translate>
+ </WarningText>
+ );
+ case ExchangeTosStatus.Proposed:
+ return <i18n.Translate>proposed</i18n.Translate>;
+ default:
+ return (
+ <DestructiveText>
+ <i18n.Translate>
+ unknown (exchange status should be updated)
+ </i18n.Translate>
+ </DestructiveText>
+ );
+ }
+ }
+ const uri = !e.masterPub
+ ? undefined
+ : stringifyWithdrawExchange({
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ });
+ return (
+ <tr key={idx}>
+ <td>
+ <a href={!uri ? undefined : Pages.defaultCta({ uri })}>
+ {e.scopeInfo
+ ? `${e.scopeInfo.currency} (${
+ e.scopeInfo.type === ScopeType.Global
+ ? "global"
+ : "regional"
+ })`
+ : e.currency}
+ </a>
+ </td>
+ <td>
+ <a
+ href={new URL(`/keys`, e.exchangeBaseUrl).href}
+ target="_blank"
+ >
+ {e.exchangeBaseUrl}
+ </a>
+ </td>
+ <td>
+ {e.exchangeEntryStatus} / {e.exchangeUpdateStatus}
+ </td>
+ <td>
+ <TosStatus />
+ </td>
+ <td>
+ {e.lastUpdateTimestamp
+ ? AbsoluteTime.toIsoString(
+ AbsoluteTime.fromPreciseTimestamp(
+ e.lastUpdateTimestamp,
+ ),
+ )
+ : "never"}
+ </td>
+ <td>
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.UpdateExchangeEntry,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ force: true,
+ },
+ );
+ }}
+ >
+ Reload
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ });
+ }}
+ >
+ Delete
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ purge: true,
+ });
+ }}
+ >
+ Purge
+ </button>
+ {e.scopeInfo && e.masterPub && e.currency ? (
+ e.scopeInfo.type === ScopeType.Global ? (
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.RemoveGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+ Make regional
+ </button>
+ ) : e.scopeInfo.type ===
+ ScopeType.Auditor ? undefined : e.scopeInfo.type ===
+ ScopeType.Exchange ? (
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.AddGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+ Make global
+ </button>
+ ) : undefined
+ ) : undefined}
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.SetExchangeTosForgotten,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ },
+ );
+ }}
+ >
+ Forget ToS
+ </button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ )}
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div />
+ <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <i18n.Translate>Add an exchange</i18n.Translate>
+ </LinkPrimary>
+ </div>
+
<Paper style={{ padding: 10, margin: 10 }}>
<h3>Logging</h3>
<div>
@@ -395,28 +511,7 @@ export function View({
Set log level
</Button>
</Paper>
- {downloadedDatabase && (
- <div>
- <i18n.Translate>
- Database exported at <Time
- timestamp={AbsoluteTime.fromMilliseconds(
- downloadedDatabase.time.getTime(),
- )}
- format="yyyy/MM/dd HH:mm:ss"
- /> <a
- href={`data:text/plain;charset=utf-8;base64,${toBase64(
- downloadedDatabase.content,
- )}`}
- download={`taler-wallet-database-${format(
- downloadedDatabase.time,
- "yyyy/MM/dd_HH:mm",
- )}.json`}
- >
- <i18n.Translate>click here</i18n.Translate>
- </a> to download
- </i18n.Translate>
- </div>
- )}
+
<br />
<p>
<i18n.Translate>Coins</i18n.Translate>:
@@ -452,31 +547,9 @@ export function View({
);
})}
<br />
- {operations && operations.length > 0 && (
- <Fragment>
- <p>
- <i18n.Translate>Pending operations</i18n.Translate>
- </p>
- <dl>
- {operations.reverse().map((o) => {
- return (
- <NotifyUpdateFadeOut key={hashObjectId(o)}>
- <dt>
- {o.type}{" "}
- <Time
- timestamp={o.timestampDue}
- format="yy/MM/dd HH:mm:ss"
- />
- </dt>
- <dd>
- <pre>{JSON.stringify(o, undefined, 2)}</pre>
- </dd>
- </NotifyUpdateFadeOut>
- );
- })}
- </dl>
- </Fragment>
- )}
+ <NotifyUpdateFadeOut>
+ <ActiveTasksTable />
+ </NotifyUpdateFadeOut>
</div>
);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
index b1cbbc2b2..d70b62de0 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -16,13 +16,13 @@
import { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
import {
- createPairTimeline,
WalletApiOperation,
+ createPairTimeline,
} from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js";
@@ -90,6 +90,7 @@ export function useComponentState({
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load exchange details info`,
hook,
),
@@ -151,7 +152,7 @@ export function useComponentState({
};
}
- //this may be expensive, useMemo
+ // this may be expensive, useMemo
const coinOperationTimeline: DenomOperationMap<FeeDescription[]> = {
deposit: createPairTimeline(
selected.denomFees.deposit,
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 8b4f64a93..482b8d698 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -35,7 +35,6 @@ import {
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
- TransactionReward,
TransactionType,
TransactionWithdrawal,
WithdrawalType,
@@ -50,17 +49,17 @@ export default {
let count = 0;
const commonTransaction = (): TransactionCommon =>
-({
- amountRaw: "USD:10",
- amountEffective: "USD:9",
- txState: {
- major: TransactionMajorState.Done,
- },
- timestamp: TalerProtocolTimestamp.fromSeconds(
- new Date().getTime() / 1000 - count++ * 60 * 60 * 7,
- ),
- transactionId: String(count),
-} as TransactionCommon);
+ ({
+ amountRaw: "USD:10",
+ amountEffective: "USD:9",
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ timestamp: TalerProtocolTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - count++ * 60 * 60 * 7,
+ ),
+ transactionId: String(count),
+ }) as TransactionCommon;
const exampleData = {
withdraw: {
@@ -112,11 +111,6 @@ const exampleData = {
exchangeBaseUrl: "http://exchange.taler",
refreshReason: RefreshReason.PayMerchant,
} as TransactionRefresh,
- tip: {
- ...commonTransaction(),
- type: TransactionType.Reward,
- merchantBaseUrl: "http://ads.merchant.taler.net/",
- } as TransactionReward,
refund: {
...commonTransaction(),
type: TransactionType.Refund,
@@ -168,15 +162,12 @@ const exampleData = {
} as TransactionPeerPullDebit,
};
-export const NoBalance = tests.createExample(TestedComponent, {
- transactions: [],
- balances: [],
-});
-
export const SomeBalanceWithNoTransactions = tests.createExample(
TestedComponent,
{
- transactions: [],
+ transactionsByDate: {
+ "11/11/11": [],
+ },
balances: [
{
available: "TESTKUDOS:10" as AmountString,
@@ -192,11 +183,14 @@ export const SomeBalanceWithNoTransactions = tests.createExample(
},
},
],
+ balanceIndex: 0,
},
);
export const OneSimpleTransaction = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -212,12 +206,15 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, {
},
},
],
+ balanceIndex: 0,
});
export const TwoTransactionsAndZeroBalance = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw, exampleData.deposit],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw, exampleData.deposit],
+ },
balances: [
{
flags: [],
@@ -233,18 +230,21 @@ export const TwoTransactionsAndZeroBalance = tests.createExample(
},
},
],
+ balanceIndex: 0,
},
);
export const OneTransactionPending = tests.createExample(TestedComponent, {
- transactions: [
- {
- ...exampleData.withdraw,
- txState: {
- major: TransactionMajorState.Pending,
+ transactionsByDate: {
+ "11/11/11": [
+ {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- },
- ],
+ ],
+ },
balances: [
{
flags: [],
@@ -260,26 +260,28 @@ export const OneTransactionPending = tests.createExample(TestedComponent, {
},
},
],
+ balanceIndex: 0,
});
export const SomeTransactions = tests.createExample(TestedComponent, {
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary:
- "this is a long summary that may be cropped because its too long",
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary:
+ "this is a long summary that may be cropped because its too long",
+ },
},
- },
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -295,85 +297,87 @@ export const SomeTransactions = tests.createExample(TestedComponent, {
},
},
],
+ balanceIndex: 0,
});
export const SomeTransactionsInDifferentStates = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://aborted/withdrawal",
- txState: {
- major: TransactionMajorState.Aborted,
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://aborted/withdrawal",
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
},
- },
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://pending/withdrawal",
- txState: {
- major: TransactionMajorState.Pending,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://pending/withdrawal",
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- },
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://failed/withdrawal",
- txState: {
- major: TransactionMajorState.Failed,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://failed/withdrawal",
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "normal payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "normal payment",
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "aborting in progress",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborting in progress",
+ },
+ txState: {
+ major: TransactionMajorState.Aborting,
+ },
},
- txState: {
- major: TransactionMajorState.Aborting,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborted payment",
+ },
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "aborted payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "pending payment",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- txState: {
- major: TransactionMajorState.Aborted,
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "pending payment",
- },
- txState: {
- major: TransactionMajorState.Pending,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "failed payment",
+ },
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "failed payment",
- },
- txState: {
- major: TransactionMajorState.Failed,
- },
- },
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -389,22 +393,24 @@ export const SomeTransactionsInDifferentStates = tests.createExample(
},
},
],
+ balanceIndex: 0,
},
);
export const SomeTransactionsWithTwoCurrencies = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.refresh,
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -433,11 +439,14 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample(
},
},
],
+ balanceIndex: 0,
},
);
export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -505,12 +514,15 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
},
},
],
+ balanceIndex: 0,
});
export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -578,16 +590,19 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
},
},
],
+ balanceIndex: 0,
},
);
export const PeerToPeer = tests.createExample(TestedComponent, {
- transactions: [
- exampleData.pull_credit,
- exampleData.pull_debit,
- exampleData.push_credit,
- exampleData.push_debit,
- ],
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.pull_credit,
+ exampleData.pull_debit,
+ exampleData.push_credit,
+ exampleData.push_debit,
+ ],
+ },
balances: [
{
flags: [],
@@ -603,4 +618,5 @@ export const PeerToPeer = tests.createExample(TestedComponent, {
},
},
],
+ balanceIndex: 0,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index dcc3c43e3..f81e6db9f 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -18,11 +18,13 @@ import {
AbsoluteTime,
Amounts,
NotificationType,
+ ScopeType,
Transaction,
WalletBalance,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { startOfDay } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorAlertView } from "../components/CurrentAlerts.js";
@@ -38,28 +40,44 @@ import {
import { alertFromError, useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
import DownloadIcon from "../svg/download_24px.inline.svg";
import UploadIcon from "../svg/upload_24px.inline.svg";
-import { getDate, startOfDay } from "date-fns";
+import { TextField } from "../mui/TextField.js";
+import { TextFieldHandler } from "../mui/handlers.js";
interface Props {
currency?: string;
+ search?: boolean;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
}
export function HistoryPage({
- currency,
+ currency: _c,
+ search: showSearch,
goToWalletManualWithdraw,
goToWalletDeposit,
}: Props): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
- const state = useAsyncAsHook(async () => ({
- b: await api.wallet.call(WalletApiOperation.GetBalances, {}),
- tx: await api.wallet.call(WalletApiOperation.GetTransactions, {}),
- }));
+ const [balanceIndex, setBalanceIndex] = useState<number>(0);
+ const [search, setSearch] = useState<string>();
+
+ const [settings] = useSettings();
+ const state = useAsyncAsHook(async () => {
+ const b = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ const balance =
+ b.balances.length > 0 ? b.balances[balanceIndex] : undefined;
+ const tx = await api.wallet.call(WalletApiOperation.GetTransactions, {
+ scopeInfo: showSearch ? undefined : balance?.scopeInfo,
+ sort: "descending",
+ includeRefreshes: settings.showRefeshTransactions,
+ search,
+ });
+ return { b, tx };
+ }, [balanceIndex, search]);
useEffect(() => {
return api.listener.onUpdateNotification(
@@ -67,6 +85,7 @@ export function HistoryPage({
state?.retry,
);
});
+ const { pushAlertOnError } = useAlertContext();
if (!state) {
return <Loading />;
@@ -76,6 +95,7 @@ export function HistoryPage({
return (
<ErrorAlertView
error={alertFromError(
+ i18n,
i18n.str`Could not load the list of transactions`,
state,
)}
@@ -83,100 +103,86 @@ export function HistoryPage({
);
}
+ if (!state.response.b.balances.length) {
+ return (
+ <NoBalanceHelp
+ goToWalletManualWithdraw={{
+ onClick: pushAlertOnError(goToWalletManualWithdraw),
+ }}
+ />
+ );
+ }
+
+ const byDate = state.response.tx.transactions.reduce(
+ (rv, x) => {
+ const startDay =
+ x.timestamp.t_s === "never"
+ ? 0
+ : startOfDay(x.timestamp.t_s * 1000).getTime();
+ if (startDay) {
+ if (!rv[startDay]) {
+ rv[startDay] = [];
+ // datesWithTransaction.push(String(startDay));
+ }
+ rv[startDay].push(x);
+ }
+
+ return rv;
+ },
+ {} as { [x: string]: Transaction[] },
+ );
+
+ if (showSearch) {
+ return (
+ <FilteredHistoryView
+ search={{
+ value: search ?? "",
+ onInput: pushAlertOnError(async (d: string) => {
+ setSearch(d);
+ }),
+ }}
+ transactionsByDate={byDate}
+ />
+ );
+ }
+
return (
<HistoryView
+ balanceIndex={balanceIndex}
+ changeBalanceIndex={(b) => setBalanceIndex(b)}
balances={state.response.b.balances}
- defaultCurrency={currency}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
- transactions={[...state.response.tx.transactions].reverse()}
+ transactionsByDate={byDate}
/>
);
}
-const term = 1000 * 60 * 60 * 24;
-function normalizeToDay(x: number): number {
- return Math.round(x / term) * term;
-}
-
export function HistoryView({
- defaultCurrency,
- transactions,
balances,
+ balanceIndex,
+ changeBalanceIndex,
+ transactionsByDate,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
+ balanceIndex: number;
+ changeBalanceIndex: (s: number) => void;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
- defaultCurrency?: string;
- transactions: Transaction[];
+ transactionsByDate: Record<string, Transaction[]>;
balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
- const { pushAlertOnError } = useAlertContext();
-
- const transactionByCurrency = transactions.reduce((prev, cur) => {
- const c = Amounts.parseOrThrow(cur.amountEffective).currency;
- if (!prev[c]) {
- prev[c] = [];
- }
- prev[c].push(cur);
- return prev;
- }, {} as Record<string, Transaction[]>);
- const currencies = balances
- .filter((b) => {
- const av = Amounts.parseOrThrow(b.available);
- return (
- Amounts.isNonZero(av) ||
- (transactionByCurrency[av.currency] &&
- transactionByCurrency[av.currency].length > 0)
- );
- })
- .map((b) => b.available.split(":")[0]);
+ const balance = balances[balanceIndex];
- const defaultCurrencyIndex = currencies.findIndex(
- (c) => c === defaultCurrency,
- );
- const [currencyIndex, setCurrencyIndex] = useState(
- defaultCurrencyIndex === -1 ? 0 : defaultCurrencyIndex,
- );
- const selectedCurrency =
- currencies.length > 0 ? currencies[currencyIndex] : undefined;
-
- const currencyAmount = balances[currencyIndex]
- ? Amounts.jsonifyAmount(balances[currencyIndex].available)
+ const available = balance
+ ? Amounts.jsonifyAmount(balance.available)
: undefined;
- const ts =
- selectedCurrency === undefined
- ? []
- : transactionByCurrency[selectedCurrency] ?? [];
-
- const datesWithTransaction: string[] = [];
- const byDate = ts.reduce((rv, x) => {
- const startDay =
- x.timestamp.t_s === "never" ? 0 : startOfDay(x.timestamp.t_s * 1000).getTime();
- if (startDay) {
- if (!rv[startDay]) {
- rv[startDay] = [];
- datesWithTransaction.push(String(startDay));
- }
- rv[startDay].push(x);
- }
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
- return rv;
- }, {} as { [x: string]: Transaction[] });
-
- if (balances.length === 0 || !selectedCurrency) {
- return (
- <NoBalanceHelp
- goToWalletManualWithdraw={{
- onClick: pushAlertOnError(goToWalletManualWithdraw),
- }}
- />
- );
- }
return (
<Fragment>
<section>
@@ -186,72 +192,151 @@ export function HistoryView({
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
+ marginRight: 20,
}}
>
- <div
- style={{
- width: "fit-content",
- display: "flex",
- }}
- >
- {currencies.length === 1 ? (
- <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
- {selectedCurrency}
- </CenteredText>
- ) : (
- <NiceSelect>
- <select
- style={{
- fontSize: "x-large",
- }}
- value={currencyIndex}
- onChange={(e) => {
- setCurrencyIndex(Number(e.currentTarget.value));
- }}
- >
- {currencies.map((currency, index) => {
- return (
- <option value={index} key={currency}>
- {currency}
- </option>
- );
- })}
- </select>
- </NiceSelect>
- )}
- {currencyAmount && (
- <CenteredBoldText
- style={{
- display: "inline-block",
- fontSize: "x-large",
- margin: 8,
- }}
- >
- {Amounts.stringifyValue(currencyAmount, 2)}
- </CenteredBoldText>
- )}
- </div>
<div>
<Button
tooltip="Transfer money to the wallet"
startIcon={DownloadIcon}
variant="contained"
- onClick={() => goToWalletManualWithdraw(selectedCurrency)}
+ onClick={() =>
+ goToWalletManualWithdraw(balance.scopeInfo.currency)
+ }
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
- {currencyAmount && Amounts.isNonZero(currencyAmount) && (
+ {available && Amounts.isNonZero(available) && (
<Button
tooltip="Transfer money from the wallet"
startIcon={UploadIcon}
variant="outlined"
color="primary"
- onClick={() => goToWalletDeposit(selectedCurrency)}
+ onClick={() => goToWalletDeposit(balance.scopeInfo.currency)}
>
<i18n.Translate>Send</i18n.Translate>
</Button>
)}
</div>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <h3 style={{ marginBottom: 0 }}>Balance</h3>
+ <div
+ style={{
+ width: "fit-content",
+ display: "flex",
+ }}
+ >
+ {balances.length === 1 ? (
+ <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
+ {balance.scopeInfo.currency}
+ </CenteredText>
+ ) : (
+ <NiceSelect style={{ flexDirection: "column" }}>
+ <select
+ style={{
+ fontSize: "x-large",
+ }}
+ value={balanceIndex}
+ onChange={(e) => {
+ changeBalanceIndex(
+ Number.parseInt(e.currentTarget.value, 10),
+ );
+ }}
+ >
+ {balances.map((entry, index) => {
+ return (
+ <option value={index} key={entry.scopeInfo.currency}>
+ {entry.scopeInfo.currency}
+ </option>
+ );
+ })}
+ </select>
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {balance.scopeInfo.type === ScopeType.Exchange ||
+ balance.scopeInfo.type === ScopeType.Auditor
+ ? balance.scopeInfo.url
+ : undefined}
+ </div>
+ </NiceSelect>
+ )}
+ {available && (
+ <CenteredBoldText
+ style={{
+ display: "inline-block",
+ fontSize: "x-large",
+ margin: 8,
+ }}
+ >
+ {Amounts.stringifyValue(available, 2)}
+ </CenteredBoldText>
+ )}
+ </div>
+ </div>
+ </div>
+ </section>
+ {datesWithTransaction.length === 0 ? (
+ <section>
+ <i18n.Translate>
+ Your transaction history is empty for this currency.
+ </i18n.Translate>
+ </section>
+ ) : (
+ <section>
+ {datesWithTransaction.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {transactionsByDate[d].map((tx, i) => (
+ <HistoryItem key={i} tx={tx} />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ )}
+ </Fragment>
+ );
+}
+
+export function FilteredHistoryView({
+ search,
+ transactionsByDate,
+}: {
+ search: TextFieldHandler;
+ transactionsByDate: Record<string, Transaction[]>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
+
+ return (
+ <Fragment>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginRight: 20,
+ }}
+ >
+ <TextField
+ label="Search"
+ variant="filled"
+ error={search.error}
+ required
+ fullWidth
+ value={search.value}
+ onChange={search.onInput}
+ />
</div>
</section>
{datesWithTransaction.length === 0 ? (
@@ -273,7 +358,7 @@ export function HistoryView({
format="dd MMMM yyyy"
/>
</DateSeparator>
- {byDate[d].map((tx, i) => (
+ {transactionsByDate[d].map((tx, i) => (
<HistoryItem key={i} tx={tx} />
))}
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
index 769fe4d10..a7b2fe90f 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
@@ -61,7 +61,10 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n.str`Could not load known bank accounts`, hook),
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load known bank accounts`,
+ hook),
};
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
index b77c456e5..c01797e31 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
@@ -58,6 +58,7 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
@@ -69,6 +70,7 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, {
uri: {
targetType: "bitcoin",
segwitAddrs: [],
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
isKnown: true,
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
@@ -138,6 +140,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
@@ -150,6 +153,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, {
targetType: "bitcoin",
segwitAddrs: [],
isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
params: {},
},
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 4d045ee13..7b80977f3 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -23,13 +23,12 @@ import {
stringifyPaytoUri,
validateIban,
} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { SelectList } from "../../components/SelectList.js";
-import { Input, SubTitle, SvgIcon } from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { SubTitle, SvgIcon } from "../../components/styled/index.js";
import { Button } from "../../mui/Button.js";
import { TextFieldHandler } from "../../mui/handlers.js";
import { TextField } from "../../mui/TextField.js";
@@ -110,6 +109,7 @@ export function ReadyView({
<div style={{ width: "100%", display: "flex" }}>
{Object.entries(accountType.list).map(([key, name], idx) => (
<div
+ key={idx}
style={{
marginLeft: 8,
padding: 8,
@@ -119,7 +119,7 @@ export function ReadyView({
accountType.value === key ? "#0042b2" : "unset",
color: accountType.value === key ? "white" : "unset",
}}
- onClick={(e) => {
+ onClick={() => {
if (accountType.onChange) {
accountType.onChange(key);
}
@@ -130,6 +130,7 @@ export function ReadyView({
))}
</div>
<div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
+ --- {uri.value} ---
<p>
<CustomFieldByAccountType
type={accountType.value as AccountType}
@@ -431,7 +432,7 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ return Object.keys(obj).some((k) => (obj as Record<string,unknown>)[k] !== undefined)
? obj
: undefined;
}
@@ -488,20 +489,21 @@ function TalerBankAddressAccount({
}
//Taken from libeufin and libeufin took it from the ISO20022 XSD schema
-const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
-const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
+// const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
+// const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
const { i18n } = useTranslationContext();
- const [bic, setBic] = useState<string | undefined>(undefined);
+ // const [bic, setBic] = useState<string | undefined>(undefined);
const [iban, setIban] = useState<string | undefined>(undefined);
const [name, setName] = useState<string | undefined>(undefined);
- const errors = undefinedIfEmpty({
- bic: !bic
- ? undefined
- : !bicRegex.test(bic)
- ? i18n.str`Invalid bic`
- : undefined,
+ const bic = ""
+ const errorsFN = (iban:string | undefined, name: string | undefined) => undefinedIfEmpty({
+ // bic: !bic
+ // ? undefined
+ // : !bicRegex.test(bic)
+ // ? i18n.str`Invalid bic`
+ // : undefined,
iban: !iban
? i18n.str`Can't be empty`
: validateIban(iban).type === "invalid"
@@ -509,16 +511,20 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
: undefined,
name: !name ? i18n.str`Can't be empty` : undefined,
});
+ const errors = errorsFN(iban, name)
function sendUpdateIfNoErrors(
bic: string | undefined,
iban: string,
name: string,
): void {
- if (!errors && field.onInput) {
+ if (!field.onInput) return;
+ if (!errorsFN(iban, name)) {
const p = buildPayto("iban", iban, bic);
p.params["receiver-name"] = name;
field.onInput(stringifyPaytoUri(p));
+ } else {
+ field.onInput("")
}
}
return (
@@ -584,7 +590,7 @@ function CustomFieldByAccountType({
type: AccountType;
field: TextFieldHandler;
}): VNode {
- const { i18n } = useTranslationContext();
+ // const { i18n } = useTranslationContext();
const AccountForm = formComponentByAccountType[type];
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
index f19fe260d..3ef8250ac 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
@@ -42,6 +42,7 @@ export function useComponentState(p: Props): State {
return {
status: "error",
error: alertFromError(
+ i18n,
i18n.str`Could not load user attention request`,
hook,
),
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index f81a86b9d..d4ee09b89 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -22,10 +22,9 @@
import {
AbsoluteTime,
AmountString,
+ ProviderPaymentType,
TalerPreciseTimestamp,
- TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
import * as tests from "@gnu-taler/web-util/testing";
import { ProviderView as TestedComponent } from "./ProviderDetailPage.js";
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index 19ae39106..d628b68e8 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -15,22 +15,22 @@
*/
import * as utils from "@gnu-taler/taler-util";
-import { AbsoluteTime } from "@gnu-taler/taler-util";
import {
+ AbsoluteTime,
ProviderInfo,
ProviderPaymentStatus,
ProviderPaymentType,
- WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
-import { Fragment, h, VNode } from "preact";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { ErrorAlertView } from "../components/CurrentAlerts.js";
import { ErrorMessage } from "../components/ErrorMessage.js";
import { Loading } from "../components/Loading.js";
-import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
import { Time } from "../components/Time.js";
+import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
import { alertFromError } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
@@ -68,6 +68,7 @@ export function ProviderDetailPage({
return (
<ErrorAlertView
error={alertFromError(
+ i18n,
i18n.str`There was an error loading the provider detail for &quot;${providerURL}&quot;`,
state,
)}
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index 999223fd8..a01ea6967 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -15,16 +15,19 @@
*/
import {
+ assertUnreachable,
parseTalerUri,
TalerUri,
+ TalerUriAction,
TranslatedString,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { css } from "@linaria/core";
import { styled } from "@linaria/react";
import jsQR, * as pr from "jsqr";
-import { Fragment, h, VNode } from "preact";
+import { h, VNode } from "preact";
import { useRef, useState } from "preact/hooks";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Alert } from "../mui/Alert.js";
import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
@@ -182,7 +185,7 @@ async function createCanvasFromFile(
canvas.width = img.width;
canvas.height = img.height;
return new Promise<string | undefined>((ok, bad) => {
- img.addEventListener("load", (e) => {
+ img.addEventListener("load", () => {
try {
const code = drawIntoCanvasAndGetQR(img, canvas);
ok(code);
@@ -194,7 +197,7 @@ async function createCanvasFromFile(
}
async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
- return new Promise((ok, bad) => {
+ return new Promise((ok, _bad) => {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
return ok();
}
@@ -211,8 +214,25 @@ export function QrReaderPage({ onDetected }: Props): VNode {
const { i18n } = useTranslationContext();
+ function onChangeDetect(str: string) {
+ if (str) {
+ const uri = parseTalerUri(str);
+ if (!uri) {
+ setError(
+ i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+ );
+ } else {
+ onDetected(uri);
+ setError(undefined);
+ }
+ } else {
+ setError(undefined);
+ }
+ setValue(str);
+ }
+
function onChange(str: string) {
- if (!!str) {
+ if (str) {
if (!parseTalerUri(str)) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
@@ -244,7 +264,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
try {
const code = await createCanvasFromVideo(video, canvasRef.current);
if (code) {
- onChange(code);
+ onChangeDetect(code);
setShow("canvas");
}
stream.getTracks().forEach((e) => {
@@ -264,7 +284,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
try {
const code = await createCanvasFromFile(fileContent, canvasRef.current);
if (code) {
- onChange(code);
+ onChangeDetect(code);
setShow("canvas");
} else {
setError(i18n.str`Could not found a QR code in the file`);
@@ -273,8 +293,8 @@ export function QrReaderPage({ onDetected }: Props): VNode {
setError(i18n.str`something unexpected happen: ${error}`);
}
}
+ const uri = parseTalerUri(value);
- const active = value === "";
return (
<Container>
<section>
@@ -283,59 +303,75 @@ export function QrReaderPage({ onDetected }: Props): VNode {
Scan a QR code or enter taler:// URI below
</i18n.Translate>
</h1>
-
- <p>
- <TextField
- label="Taler URI"
- variant="standard"
- fullWidth
- value={value}
- onChange={onChange}
- />
- </p>
+ <div style={{ justifyContent: "space-between", display: "flex" }}>
+ <div style={{ width: "75%" }}>
+ <TextField
+ label="Taler URI"
+ variant="filled"
+ fullWidth
+ value={value}
+ onChange={onChange}
+ />
+ </div>
+ {uri && (
+ <Button
+ disabled={!!error}
+ variant="contained"
+ color="success"
+ onClick={async () => {
+ if (uri) onDetected(uri);
+ }}
+ >
+ {(function (talerUri: TalerUri): VNode {
+ switch (talerUri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return (
+ <i18n.Translate>Withdrawal from bank</i18n.Translate>
+ );
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Claim refund</i18n.Translate>;
+ case TalerUriAction.PayPull:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.PayPush:
+ return <i18n.Translate>Accept payment</i18n.Translate>;
+ case TalerUriAction.PayTemplate:
+ return <i18n.Translate>Complete order</i18n.Translate>;
+ case TalerUriAction.Restore:
+ return <i18n.Translate>Restore wallet</i18n.Translate>;
+ case TalerUriAction.DevExperiment:
+ return <i18n.Translate>Enable experiment</i18n.Translate>;
+ case TalerUriAction.WithdrawExchange:
+ return (
+ <i18n.Translate>Withdraw from exchange</i18n.Translate>
+ );
+ case TalerUriAction.AddExchange:
+ return <i18n.Translate>Add exchange</i18n.Translate>;
+ default: {
+ assertUnreachable(talerUri);
+ }
+ }
+ })(uri)}
+ </Button>
+ )}
+ </div>
<Grid container justifyContent="space-around" columns={2}>
<Grid item xs={2}>
<p>{error && <Alert severity="error">{error}</Alert>}</p>
</Grid>
- <Grid item xs={1}>
- {!active && (
- <Button
- variant="contained"
- onClick={async () => {
- setShow("nothing");
- onChange("");
- }}
- color="error"
- >
- <i18n.Translate>Clear</i18n.Translate>
- </Button>
- )}
- </Grid>
- <Grid item xs={1}>
- {value && (
- <Button
- disabled={!!error}
- variant="contained"
- color="success"
- onClick={async () => {
- const uri = parseTalerUri(value);
- if (uri) onDetected(uri);
- }}
- >
- <i18n.Translate>Open</i18n.Translate>
- </Button>
- )}
- </Grid>
- <Grid item xs={1}>
- <InputFile onChange={onFileRead}>Read QR from file</InputFile>
- </Grid>
- <Grid item xs={1}>
+ <Grid item xs={2}>
<p>
<Button variant="contained" onClick={startVideo}>
Use Camera
</Button>
</p>
</Grid>
+ <EnabledBySettings name="advancedMode">
+ <Grid item xs={2}>
+ <InputFile onChange={onFileRead}>Read QR from file</InputFile>
+ </Grid>
+ </EnabledBySettings>
</Grid>
</section>
<div>
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
deleted file mode 100644
index 2fcf580ed..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { parsePaytoUri } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
-import { ReserveCreated as TestedComponent } from "./ReserveCreated.js";
-
-export default {
- title: "reserve created",
- component: TestedComponent,
- argTypes: {},
-};
-
-export const TalerBank = tests.createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- accounts: []
-});
-
-export const IBAN = tests.createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- accounts: []
-});
-
-export const WithReceiverName = tests.createExample(TestedComponent, {
- reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- paytoURI: parsePaytoUri(
- "payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG&receiver=Sebastian",
- ),
- amount: {
- currency: "USD",
- value: 10,
- fraction: 0,
- },
- accounts: []
-});
-
-export const Bitcoin = tests.createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- accounts: []
-});
-
-export const BitcoinRegTest = tests.createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- accounts: []
-});
-export const BitcoinTest = tests.createExample(TestedComponent, {
- reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- paytoURI: parsePaytoUri(
- "payto://bitcoin/tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
- ),
- amount: {
- currency: "BTC",
- value: 0,
- fraction: 14000000,
- },
- accounts: []
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
deleted file mode 100644
index 144413541..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { AmountJson, PaytoUri, WithdrawalExchangeAccountDetails, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { Amount } from "../components/Amount.js";
-import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
-import { CopyButton } from "../components/CopyButton.js";
-import { ErrorMessage } from "../components/ErrorMessage.js";
-import { QR } from "../components/QR.js";
-import { Title, WarningBox } from "../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Button } from "../mui/Button.js";
-export interface Props {
- reservePub: string;
- paytoURI: PaytoUri | undefined;
- accounts: WithdrawalExchangeAccountDetails[];
- amount: AmountJson;
- onCancel: () => Promise<void>;
-}
-
-export function ReserveCreated({
- reservePub,
- paytoURI,
- onCancel,
- accounts,
- amount,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- if (!paytoURI) {
- return (
- <ErrorMessage
- title={i18n.str`Could not parse the payto URI`}
- description={i18n.str`Please check the uri`}
- />
- );
- }
- return (
- <Fragment>
- <section>
- <Title>
- <i18n.Translate>Exchange is ready for withdrawal</i18n.Translate>
- </Title>
- <p>
- <i18n.Translate>
- To complete the process you need to wire{` `}
- <b>{<Amount value={amount} />}</b> to the exchange bank account
- </i18n.Translate>
- </p>
- </section>
- <BankDetailsByPaytoType
- amount={amount}
- accounts={accounts}
- subject={reservePub}
- />
- <section>
- <p>
- <i18n.Translate>
- Alternative, you can also scan this QR code or open{" "}
- <a href={stringifyPaytoUri(paytoURI)}>this link</a> if you have a
- banking app installed that supports RFC 8905
- </i18n.Translate>
- </p>
- <QR text={stringifyPaytoUri(paytoURI)} />
- </section>
- <footer>
- <div />
- <Button variant="contained" color="error" onClick={onCancel}>
- <i18n.Translate>Cancel withdrawal</i18n.Translate>
- </Button>
- </footer>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
index bbf5bf0c8..cd43c4526 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -37,11 +37,13 @@ const version = {
merchant: "2:0:1",
bank: "0:0:0",
hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
- version: "0.9.0-dev.1",
+ version: "1:2:3",
devMode: false,
bankConversionApiRange: "0:0:0",
bankIntegrationApiRange: "0:0:0",
corebankApiRange: "0:0:0",
+ implementationGitHash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
+ implementationSemver: "0.9.0-dev.1",
} satisfies WalletCoreVersion,
webexVersion: {
version: "0.9.0.13",
@@ -53,7 +55,6 @@ export const AllOff = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
@@ -63,7 +64,6 @@ export const OneChecked = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
@@ -73,22 +73,8 @@ export const WithOneExchange = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
- knownExchanges: [
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange.taler",
- tos: {
- currentVersion: "1",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
- } as any, //TODO: complete with auditors, wireInfo and denominations
- ],
...version,
});
@@ -98,49 +84,8 @@ export const WithExchangeInDifferentState = tests.createExample(
deviceName: "this-is-the-device-name",
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
- injectTalerToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
- knownExchanges: [
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange1.taler",
- tos: {
- currentVersion: "1",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: [
- "payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator",
- ],
- },
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange2.taler",
- tos: {
- currentVersion: "2",
- acceptedVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: [
- "payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator",
- ],
- } as any, //TODO: complete with auditors, wireInfo and denominations
- {
- currency: "USD",
- exchangeBaseUrl: "http://exchange3.taler",
- tos: {
- currentVersion: "1",
- content: "content of tos",
- contentType: "text/plain",
- },
- paytoUris: [
- "payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator",
- ],
- },
- ],
...version,
},
);
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index b27413a96..0d0a31a2d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -15,28 +15,21 @@
*/
import {
- ExchangeListItem,
- ExchangeTosStatus,
LibtoolVersion,
TranslatedString,
- WalletCoreVersion,
+ WalletCoreVersion
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { Pages } from "../NavigationBar.js";
import { Checkbox } from "../components/Checkbox.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Part } from "../components/Part.js";
import { SelectList } from "../components/SelectList.js";
import {
- DestructiveText,
Input,
- LinkPrimary,
SubTitle,
- SuccessText,
- WarningBox,
- WarningText,
+ WarningBox
} from "../components/styled/index.js";
import { useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
@@ -57,19 +50,15 @@ export function SettingsPage(): VNode {
const webex = platform.getWalletWebExVersion();
const api = useBackendContext();
- const exchangesHook = useAsyncAsHook(async () => {
- const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
+ const hook = useAsyncAsHook(async () => {
const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
- return { exchanges: list.exchanges, version };
+ return { version };
});
- const { exchanges, version } =
- !exchangesHook || exchangesHook.hasError
- ? { exchanges: [], version: undefined }
- : exchangesHook.response;
+
+ const version = hook && !hook.hasError ? hook.response.version : undefined
return (
<SettingsView
- knownExchanges={exchanges}
deviceName={name}
setDeviceName={update}
autoOpenToggle={{
@@ -80,19 +69,11 @@ export function SettingsPage(): VNode {
}),
},
}}
- injectTalerToggle={{
- value: settings.injectTalerSupport,
- button: {
- onClick: safely("update support injection", async () => {
- updateSettings("injectTalerSupport", !settings.injectTalerSupport);
- }),
- },
- }}
advanceToggle={{
- value: settings.advanceMode,
+ value: settings.advancedMode,
button: {
onClick: safely("update advance mode", async () => {
- updateSettings("advanceMode", !settings.advanceMode);
+ updateSettings("advancedMode", !settings.advancedMode);
}),
},
}}
@@ -117,10 +98,8 @@ export interface ViewProps {
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
autoOpenToggle: ToggleHandler;
- injectTalerToggle: ToggleHandler;
advanceToggle: ToggleHandler;
langToggle: ToggleHandler;
- knownExchanges: Array<ExchangeListItem>;
coreVersion: WalletCoreVersion | undefined;
webexVersion: {
version: string;
@@ -129,9 +108,7 @@ export interface ViewProps {
}
export function SettingsView({
- knownExchanges,
autoOpenToggle,
- injectTalerToggle,
advanceToggle,
langToggle,
coreVersion,
@@ -139,123 +116,84 @@ export function SettingsView({
}: ViewProps): VNode {
const { i18n, lang, supportedLang, changeLanguage } = useTranslationContext();
+ const api = useBackendContext();
+
return (
<Fragment>
<section>
<SubTitle>
- <i18n.Translate>Trust</i18n.Translate>
+ <i18n.Translate>Navigator</i18n.Translate>
+ </SubTitle>
+ <Checkbox
+ label={i18n.str`Automatically open wallet`}
+ name="autoOpen"
+ description={
+ <i18n.Translate>
+ Open the wallet when a payment action is found.
+ </i18n.Translate>
+ }
+ enabled={autoOpenToggle.value!}
+ onToggle={autoOpenToggle.button.onClick!}
+ />
+
+ <SubTitle>
+ <i18n.Translate>Version Info</i18n.Translate>
</SubTitle>
- {!knownExchanges || !knownExchanges.length ? (
- <div>
- <i18n.Translate>No exchange yet</i18n.Translate>
- </div>
- ) : (
+ <Part
+ title={i18n.str`Web Extension`}
+ text={
+ <span>
+ {webexVersion.version}{" "}
+ <EnabledBySettings name="advancedMode">
+ {webexVersion.hash}
+ </EnabledBySettings>
+ </span>
+ }
+ />
+ {coreVersion && (
<Fragment>
- <table>
- <thead>
- <tr>
- <th>
- <i18n.Translate>Currency</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>URL</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Term of Service</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody>
- {knownExchanges.map((e, idx) => {
- function Status(): VNode {
- switch (e.tosStatus) {
- case ExchangeTosStatus.Accepted:
- return (
- <SuccessText>
- <i18n.Translate>ok</i18n.Translate>
- </SuccessText>
- );
- case ExchangeTosStatus.Pending:
- return (
- <WarningText>
- <i18n.Translate>pending</i18n.Translate>
- </WarningText>
- );
- case ExchangeTosStatus.Proposed:
- return (
- <i18n.Translate>proposed</i18n.Translate>
- );
- default:
- return (
- <DestructiveText>
- <i18n.Translate>
- unknown (exchange status should be updated)
- </i18n.Translate>
- </DestructiveText>
- );
- }
- }
- return (
- <tr key={idx}>
- <td>{e.currency}</td>
- <td>
- <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
- </td>
- <td>
- <Status />
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ {LibtoolVersion.compare(
+ coreVersion.version,
+ WALLET_CORE_SUPPORTED_VERSION,
+ )?.compatible ? undefined : (
+ <WarningBox>
+ <i18n.Translate>
+ The version of wallet core is not supported. (supported
+ version: {WALLET_CORE_SUPPORTED_VERSION}, wallet version: {coreVersion.version})
+ </i18n.Translate>
+ </WarningBox>
+ )}
+ <EnabledBySettings name="advancedMode">
+ <Part
+ title={i18n.str`Exchange compatibility`}
+ text={<span>{coreVersion.exchange}</span>}
+ />
+ <Part
+ title={i18n.str`Merchant compatibility`}
+ text={<span>{coreVersion.merchant}</span>}
+ />
+ <Part
+ title={i18n.str`Bank compatibility`}
+ text={<span>{coreVersion.bank}</span>}
+ />
+ <Part
+ title={i18n.str`Wallet Core compatibility`}
+ text={<span>{coreVersion.version}</span>}
+ />
+ </EnabledBySettings>
</Fragment>
)}
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div />
- <LinkPrimary href={Pages.settingsExchangeAdd({})}>
- <i18n.Translate>Add an exchange</i18n.Translate>
- </LinkPrimary>
- </div>
-
- {coreVersion && (<Fragment>
- {LibtoolVersion.compare(coreVersion.version, WALLET_CORE_SUPPORTED_VERSION)?.compatible ? undefined :
- <WarningBox>
- <i18n.Translate>
- The version of wallet core is not supported. (supported version: {WALLET_CORE_SUPPORTED_VERSION})
- </i18n.Translate>
- </WarningBox>}
- <EnabledBySettings name="advanceMode">
- <Part
- title={i18n.str`Exchange compatibility`}
- text={<span>{coreVersion.exchange}</span>}
- />
- <Part
- title={i18n.str`Merchant compatibility`}
- text={<span>{coreVersion.merchant}</span>}
- />
- <Part
- title={i18n.str`Bank compatibility`}
- text={<span>{coreVersion.bank}</span>}
- />
- <Part
- title={i18n.str`Wallet Core compatibility`}
- text={<span>{coreVersion.version}</span>}
- />
- </EnabledBySettings>
- </Fragment>
- )}
<SubTitle>
- <i18n.Translate>Advance mode</i18n.Translate>
+ <i18n.Translate>Settings</i18n.Translate>
</SubTitle>
<Checkbox
- label={i18n.str`Enable advance mode`}
+ label={i18n.str`Enable developer mode`}
name="devMode"
description={i18n.str`Show more information and options in the UI`}
enabled={advanceToggle.value!}
onToggle={advanceToggle.button.onClick!}
/>
- <EnabledBySettings name="advanceMode">
+ <EnabledBySettings name="advancedMode">
<AdvanceSettings />
</EnabledBySettings>
<EnabledBySettings name="langSelector">
@@ -272,47 +210,6 @@ export function SettingsView({
/>
</Input>
</EnabledBySettings>
- <SubTitle>
- <i18n.Translate>Navigator</i18n.Translate>
- </SubTitle>
- <Checkbox
- label={i18n.str`Inject Taler support in all pages`}
- name="inject"
- description={
- <i18n.Translate>
- Disabling this option will make some web application not able to
- trigger the wallet when clicking links but you will be able to
- open the wallet using the keyboard shortcut
- </i18n.Translate>
- }
- enabled={injectTalerToggle.value!}
- onToggle={injectTalerToggle.button.onClick!}
- />
- <Checkbox
- label={i18n.str`Automatically open wallet`}
- name="autoOpen"
- description={
- <i18n.Translate>
- Open the wallet when a payment action is found.
- </i18n.Translate>
- }
- enabled={autoOpenToggle.value!}
- onToggle={autoOpenToggle.button.onClick!}
- />
- <SubTitle>
- <i18n.Translate>Version</i18n.Translate>
- </SubTitle>
- <Part
- title={i18n.str`Web Extension`}
- text={
- <span>
- {webexVersion.version}{" "}
- <EnabledBySettings name="advanceMode">
- {webexVersion.hash}
- </EnabledBySettings>
- </span>
- }
- />
</section>
</Fragment>
);
@@ -324,6 +221,7 @@ type Options = {
};
function AdvanceSettings(): VNode {
const [settings, updateSettings] = useSettings();
+ const api = useBackendContext();
const { i18n } = useTranslationContext();
const o: Options = {
backup: {
@@ -334,6 +232,10 @@ function AdvanceSettings(): VNode {
label: i18n.str`Show suspend/resume transaction`,
description: i18n.str`Prevent transaction from doing network request.`,
},
+ showRefeshTransactions: {
+ label: i18n.str`Show refresh transaction type in the transaction list`,
+ description: i18n.str`Refresh transaction will be hidden by default if the refresh operation doesn't have fee.`,
+ },
extendedAccountTypes: {
label: i18n.str`Show more account types on deposit`,
description: i18n.str`Extends the UI to more payment target types.`,
@@ -350,6 +252,18 @@ function AdvanceSettings(): VNode {
label: i18n.str`Lang selector`,
description: i18n.str`Allows to manually change the language of the UI. Otherwise it will be automatically selected by your browser configuration.`,
},
+ showExchangeManagement: {
+ label: i18n.str`Edit exchange management`,
+ description: i18n.str`Allows to see the list of exchange, remove, add and switch before withdrawal.`,
+ },
+ selectTosFormat: {
+ label: i18n.str`Select terms of service format`,
+ description: i18n.str`Allows to render the terms of service on different format selected by the user.`,
+ },
+ showWalletActivity: {
+ label: i18n.str`Show wallet activity`,
+ description: i18n.str`Show the wallet notification and observability event in the UI.`,
+ },
};
return (
<Fragment>
@@ -360,10 +274,12 @@ function AdvanceSettings(): VNode {
<Checkbox
label={label}
name={name}
+ key={name}
description={description}
enabled={settings[settingsName]}
onToggle={async () => {
updateSettings(settingsName, !settings[settingsName]);
+ await api.background.call("reinitWallet", undefined);
}}
/>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index c17d15b01..194f0e0bb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -38,11 +38,10 @@ import {
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
- TransactionReward,
TransactionType,
TransactionWithdrawal,
WithdrawalDetails,
- WithdrawalType,
+ WithdrawalType
} from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import beer from "../../static-dev/beer.png";
@@ -137,17 +136,6 @@ const exampleData = {
exchangeBaseUrl: "http://exchange.taler",
refreshReason: RefreshReason.Manual,
} as TransactionRefresh,
- tip: {
- ...commonTransaction,
- type: TransactionType.Reward,
- // merchant: {
- // name: "the merchant",
- // logo: merchantIcon,
- // website: "https://www.themerchant.taler",
- // email: "contact@merchant.taler",
- // },
- merchantBaseUrl: "http://merchant.taler",
- } as TransactionReward,
refund: {
...commonTransaction,
type: TransactionType.Refund,
@@ -584,26 +572,6 @@ export const RefreshError = tests.createExample(TestedComponent, {
},
});
-export const Tip = tests.createExample(TestedComponent, {
- transaction: exampleData.tip,
-});
-
-export const TipError = tests.createExample(TestedComponent, {
- transaction: {
- ...exampleData.tip,
- error: transactionError,
- },
-});
-
-export const TipPending = tests.createExample(TestedComponent, {
- transaction: {
- ...exampleData.tip,
- txState: {
- major: TransactionMajorState.Pending,
- },
- },
-});
-
export const Refund = tests.createExample(TestedComponent, {
transaction: exampleData.refund,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index e7ab65722..1f0293352 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -19,6 +19,7 @@ import {
AmountJson,
Amounts,
AmountString,
+ DenomLossEventType,
MerchantInfo,
NotificationType,
OrderShortInfo,
@@ -37,7 +38,7 @@ import {
TransactionType,
TransactionWithdrawal,
TranslatedString,
- WithdrawalType
+ WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -62,7 +63,7 @@ import {
SmallLightText,
SubTitle,
SvgIcon,
- WarningBox
+ WarningBox,
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
import { alertFromError, useAlertContext } from "../context/alert.js";
@@ -107,6 +108,7 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
return (
<ErrorAlertView
error={alertFromError(
+ i18n,
i18n.str`Could not load transaction information`,
state,
)}
@@ -229,65 +231,75 @@ function TransactionTemplate({
<Fragment>
<section style={{ padding: 8, textAlign: "center" }}>
{transaction?.error &&
- // FIXME: wallet core should stop sending this error on KYC
- transaction.error.code !==
+ // FIXME: wallet core should stop sending this error on KYC
+ transaction.error.code !==
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
<ErrorAlertView
error={alertFromError(
- i18n.str`There was an error trying to complete the transaction`,
+ i18n,
+ i18n.str`There was an error trying to complete the transaction.`,
transaction.error,
)}
/>
) : undefined}
- {transaction.txState.minor === TransactionMinorState.KycRequired && (
- <AlertView
- alert={{
- type: "warning",
- message: i18n.str`KYC check required for the transaction to complete`,
- description:
- transaction.kycUrl && typeof transaction.kycUrl === "string" ? (
- <div>
- <i18n.Translate>
- Follow this link to the{` `}
- <a rel="noreferrer" target="_bank" href={transaction.kycUrl}>KYC verifier</a>
- </i18n.Translate>
- </div>
- ) : (
- i18n.str`No more information has been provided`
- ),
- }}
- />
- )}
- {transaction.txState.minor === TransactionMinorState.AmlRequired && (
- <WarningBox>
- <i18n.Translate>
- The transaction has been blocked since the account required an AML
- check
- </i18n.Translate>
- </WarningBox>
- )}
- {transaction.txState.major === TransactionMajorState.Pending && (
- <WarningBox>
- <div style={{ justifyContent: "center", lineHeight: "25px" }}>
- <i18n.Translate>This transaction is not completed</i18n.Translate>
- <Link onClick={onRetry}>
- <SvgIcon
- title={i18n.str`Retry`}
- dangerouslySetInnerHTML={{ __html: refreshIcon }}
- color="black"
- />
- </Link>
- </div>
- </WarningBox>
- )}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`KYC check required for the transaction to complete.`,
+ description:
+ transaction.kycUrl &&
+ typeof transaction.kycUrl === "string" ? (
+ <div>
+ <i18n.Translate>
+ Follow this link to the{` `}
+ <a
+ rel="noreferrer"
+ target="_bank"
+ href={transaction.kycUrl}
+ >
+ KYC verifier.
+ </a>
+ </i18n.Translate>
+ </div>
+ ) : (
+ i18n.str`No additional information has been provided.`
+ ),
+ }}
+ />
+ ) : transaction.txState.minor ===
+ TransactionMinorState.AmlRequired ? (
+ <WarningBox>
+ <i18n.Translate>
+ The transaction has been blocked since the account required an
+ AML check.
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This transaction is not completed
+ </i18n.Translate>
+ <Link onClick={onRetry} style={{ padding: 0 }}>
+ <SvgIcon
+ title={i18n.str`Retry`}
+ dangerouslySetInnerHTML={{ __html: refreshIcon }}
+ color="black"
+ />
+ </Link>
+ </div>
+ </WarningBox>
+ ))}
{transaction.txState.major === TransactionMajorState.Aborted && (
<InfoBox>
- <i18n.Translate>This transaction was aborted</i18n.Translate>
+ <i18n.Translate>This transaction was aborted.</i18n.Translate>
</InfoBox>
)}
{transaction.txState.major === TransactionMajorState.Failed && (
<ErrorBox>
- <i18n.Translate>This transaction failed</i18n.Translate>
+ <i18n.Translate>This transaction failed.</i18n.Translate>
</ErrorBox>
)}
{confirmBeforeForget ? (
@@ -418,7 +430,7 @@ export function TransactionView({
transaction,
onDelete,
onAbort,
- onBack,
+ // onBack,
onResume,
onSuspend,
onRetry,
@@ -435,8 +447,13 @@ export function TransactionView({
transaction.type === TransactionType.Withdrawal ||
transaction.type === TransactionType.InternalWithdrawal
) {
- const conversion = transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer ?
- transaction.withdrawalDetails.exchangeCreditAccountDetails ?? [] : []
+ // const conversion =
+ // transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ // : [];
+ const blockedByKycOrAml =
+ transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor === TransactionMinorState.AmlRequired;
return (
<TransactionTemplate
transaction={transaction}
@@ -456,22 +473,45 @@ export function TransactionView({
{transaction.exchangeBaseUrl}
</Header>
- {transaction.txState.major !==
- TransactionMajorState.Pending ? undefined :
- transaction.txState.minor === TransactionMinorState.KycRequired ||
- transaction.txState.minor === TransactionMinorState.AmlRequired ? undefined :
- transaction
- .withdrawalDetails.type === WithdrawalType.ManualTransfer ? (
- //manual withdrawal
- <BankDetailsByPaytoType
- amount={raw}
- accounts={transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []}
- subject={transaction.withdrawalDetails.reservePub}
- />
- ) : (
- //integrated bank withdrawal
- <ShowWithdrawalDetailForBankIntegrated transaction={transaction} />
- )}
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
+ <Fragment>
+ <InfoBox>
+ {transaction.withdrawalDetails.exchangeCreditAccountDetails
+ .length > 1 ? (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Select one of the
+ accounts and use the information below to complete the
+ operation by making a wire transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ ) : (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Use the
+ information below to complete the operation by making a wire
+ transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ )}
+ </InfoBox>
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ </Fragment>
+ ) : (
+ //integrated bank withdrawal
+ <ShowWithdrawalDetailForBankIntegrated transaction={transaction} />
+ )}
<Part
title={i18n.str`Details`}
text={
@@ -550,6 +590,7 @@ export function TransactionView({
format="dd MMMM yyyy"
/>
}
+ .
</i18n.Translate>
</td>
</tr>
@@ -618,11 +659,11 @@ export function TransactionView({
price={getAmountWithFee(effective, raw, "debit")}
effectiveRefund={effectiveRefund}
info={transaction.info}
- proposalId={transaction.proposalId}
/>
}
kind="neutral"
/>
+ <ShowFullContractTermPopup transactionId={transaction.transactionId} />
</TransactionTemplate>
);
}
@@ -664,7 +705,7 @@ export function TransactionView({
/>
{!shouldBeWired ? (
<Part
- title={i18n.str`Wire transfer deadline`}
+ title={i18n.str`Wire transfer deadline.`}
text={
<Time timestamp={wireTime} format="dd MMMM yyyy 'at' HH:mm" />
}
@@ -674,7 +715,7 @@ export function TransactionView({
<AlertView
alert={{
type: "warning",
- message: i18n.str`Wire transfer is not initiated`,
+ message: i18n.str`Wire transfer is not initiated.`,
description: i18n.str` `,
}}
/>
@@ -683,7 +724,7 @@ export function TransactionView({
<AlertView
alert={{
type: "success",
- message: i18n.str`Wire transfer completed`,
+ message: i18n.str`Wire transfer completed.`,
description: i18n.str` `,
}}
/>
@@ -701,7 +742,7 @@ export function TransactionView({
<AlertView
alert={{
type: "info",
- message: i18n.str`Wire transfer in progress`,
+ message: i18n.str`Wire transfer in progress.`,
description: i18n.str` `,
}}
/>
@@ -741,40 +782,6 @@ export function TransactionView({
);
}
- if (transaction.type === TransactionType.Reward) {
- return (
- <TransactionTemplate
- transaction={transaction}
- onDelete={onDelete}
- onRetry={onRetry}
- onAbort={onAbort}
- onResume={onResume}
- onSuspend={onSuspend}
- onCancel={onCancel}
- >
- <Header
- timestamp={transaction.timestamp}
- type={i18n.str`Tip`}
- total={effective}
- kind="positive"
- >
- {transaction.merchantBaseUrl}
- </Header>
- {/* <Part
- title={i18n.str`Merchant`}
- text={<MerchantDetails merchant={transaction.merchant} />}
- kind="neutral"
- /> */}
- <Part
- title={i18n.str`Details`}
- text={
- <TipDetails amount={getAmountWithFee(effective, raw, "credit")} />
- }
- />
- </TransactionTemplate>
- );
- }
-
if (transaction.type === TransactionType.Refund) {
return (
<TransactionTemplate
@@ -869,6 +876,7 @@ export function TransactionView({
/>
{transaction.txState.major === TransactionMajorState.Pending &&
transaction.txState.minor === TransactionMinorState.Ready &&
+ transaction.talerUri &&
!transaction.error && (
<Part
title={i18n.str`URI`}
@@ -1027,6 +1035,113 @@ export function TransactionView({
</TransactionTemplate>
);
}
+
+ if (transaction.type === TransactionType.DenomLoss) {
+ switch (transaction.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination expired.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination vanished.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination is unoffered.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ default: {
+ assertUnreachable(transaction.lossEventType);
+ }
+ }
+ }
+ if (transaction.type === TransactionType.Recoup) {
+ throw Error("recoup transaction not implemented");
+ }
assertUnreachable(transaction);
}
@@ -1070,127 +1185,6 @@ export function MerchantDetails({
);
}
-// function DeliveryDetails({
-// date,
-// location,
-// }: {
-// date: TalerProtocolTimestamp | undefined;
-// location: Location | undefined;
-// }): VNode {
-// const { i18n } = useTranslationContext();
-// return (
-// <PurchaseDetailsTable>
-// {location && (
-// <Fragment>
-// {location.country && (
-// <tr>
-// <td>
-// <i18n.Translate>Country</i18n.Translate>
-// </td>
-// <td>{location.country}</td>
-// </tr>
-// )}
-// {location.address_lines && (
-// <tr>
-// <td>
-// <i18n.Translate>Address lines</i18n.Translate>
-// </td>
-// <td>{location.address_lines}</td>
-// </tr>
-// )}
-// {location.building_number && (
-// <tr>
-// <td>
-// <i18n.Translate>Building number</i18n.Translate>
-// </td>
-// <td>{location.building_number}</td>
-// </tr>
-// )}
-// {location.building_name && (
-// <tr>
-// <td>
-// <i18n.Translate>Building name</i18n.Translate>
-// </td>
-// <td>{location.building_name}</td>
-// </tr>
-// )}
-// {location.street && (
-// <tr>
-// <td>
-// <i18n.Translate>Street</i18n.Translate>
-// </td>
-// <td>{location.street}</td>
-// </tr>
-// )}
-// {location.post_code && (
-// <tr>
-// <td>
-// <i18n.Translate>Post code</i18n.Translate>
-// </td>
-// <td>{location.post_code}</td>
-// </tr>
-// )}
-// {location.town_location && (
-// <tr>
-// <td>
-// <i18n.Translate>Town location</i18n.Translate>
-// </td>
-// <td>{location.town_location}</td>
-// </tr>
-// )}
-// {location.town && (
-// <tr>
-// <td>
-// <i18n.Translate>Town</i18n.Translate>
-// </td>
-// <td>{location.town}</td>
-// </tr>
-// )}
-// {location.district && (
-// <tr>
-// <td>
-// <i18n.Translate>District</i18n.Translate>
-// </td>
-// <td>{location.district}</td>
-// </tr>
-// )}
-// {location.country_subdivision && (
-// <tr>
-// <td>
-// <i18n.Translate>Country subdivision</i18n.Translate>
-// </td>
-// <td>{location.country_subdivision}</td>
-// </tr>
-// )}
-// </Fragment>
-// )}
-
-// {!location || !date ? undefined : (
-// <tr>
-// <td colSpan={2}>
-// <hr />
-// </td>
-// </tr>
-// )}
-// {date && (
-// <Fragment>
-// <tr>
-// <td>
-// <i18n.Translate>Date</i18n.Translate>
-// </td>
-// <td>
-// <Time
-// timestamp={AbsoluteTime.fromProtocolTimestamp(date)}
-// format="dd MMMM yyyy, HH:mm"
-// />
-// </td>
-// </tr>
-// </Fragment>
-// )}
-// </PurchaseDetailsTable>
-// );
-// }
-
export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
return (
<div>
@@ -1250,28 +1244,30 @@ export function InvoiceCreationDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1295,28 +1291,30 @@ export function InvoicePaymentDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.value} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1340,28 +1338,30 @@ export function TransferCreationDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Transfer</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1385,43 +1385,46 @@ export function TransferPickupDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
-export function WithdrawDetails({ conversion, amount }: { conversion?: AmountJson, amount: AmountWithFee }): VNode {
- const { i18n } = useTranslationContext();
-
- const maxFrac = [amount.fee, amount.fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
- const total = Amounts.add(amount.value, amount.fee).amount;
+export function WithdrawDetails({
+ conversion,
+ amount,
+}: {
+ conversion?: AmountJson;
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
return (
<PurchaseDetailsTable>
- {conversion ?
+ {conversion ? (
<Fragment>
<tr>
<td>
@@ -1431,7 +1434,8 @@ export function WithdrawDetails({ conversion, amount }: { conversion?: AmountJso
<Amount value={conversion} maxFracSize={amount.maxFrac} />
</td>
</tr>
- {conversion.fraction === amount.value.fraction && conversion.value === amount.value.value ? undefined :
+ {conversion.fraction === amount.value.fraction &&
+ conversion.value === amount.value.value ? undefined : (
<tr>
<td>
<i18n.Translate>Converted</i18n.Translate>
@@ -1440,9 +1444,10 @@ export function WithdrawDetails({ conversion, amount }: { conversion?: AmountJso
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- }
+ )}
</Fragment>
- : <tr>
+ ) : (
+ <tr>
<td>
<i18n.Translate>Transfer</i18n.Translate>
</td>
@@ -1450,30 +1455,32 @@ export function WithdrawDetails({ conversion, amount }: { conversion?: AmountJso
<Amount value={amount.value} maxFracSize={amount.maxFrac} />
</td>
</tr>
- }
+ )}
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1481,27 +1488,16 @@ export function WithdrawDetails({ conversion, amount }: { conversion?: AmountJso
export function PurchaseDetails({
price,
effectiveRefund,
- info,
- proposalId,
+ info: _info,
}: {
price: AmountWithFee;
effectiveRefund?: AmountJson;
info: OrderShortInfo;
- proposalId: string;
}): VNode {
const { i18n } = useTranslationContext();
const total = Amounts.add(price.value, price.fee).amount;
- // const hasProducts = info.products && info.products.length > 0;
-
- // const hasShipping =
- // info.delivery_date !== undefined || info.delivery_location !== undefined;
-
- const showLargePic = (): void => {
- return;
- };
-
return (
<PurchaseDetailsTable>
<tr>
@@ -1513,69 +1509,72 @@ export function PurchaseDetails({
</td>
</tr>
{Amounts.isNonZero(price.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={price.fee} />
- </td>
- </tr>
- )}
- {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
- <Fragment>
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Subtotal</i18n.Translate>
- </td>
- <td>
- <Amount value={price.total} />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Refunded</i18n.Translate>
- </td>
- <td>
- <Amount value={effectiveRefund} negative />
- </td>
- </tr>
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={Amounts.sub(total, effectiveRefund).amount} />
- </td>
- </tr>
- </Fragment>
- ) : (
<Fragment>
<tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
<td>
- <i18n.Translate>Total</i18n.Translate>
+ <i18n.Translate>Fees</i18n.Translate>
</td>
<td>
- <Amount value={price.value} />
+ <Amount value={price.fee} />
</td>
</tr>
+ {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Subtotal</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.total} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={effectiveRefund} negative />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={Amounts.sub(total, effectiveRefund).amount} />
+ </td>
+ </tr>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.value} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</Fragment>
)}
+
{/* {hasProducts && (
<tr>
<td colSpan={2}>
@@ -1621,11 +1620,6 @@ export function PurchaseDetails({
</td>
</tr>
)} */}
- <tr>
- <td>
- <ShowFullContractTermPopup proposalId={proposalId} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1645,28 +1639,30 @@ function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1682,19 +1678,22 @@ function calculateAmountByWireTransfer(
const allTracking = Object.values(state ?? {});
//group tracking by wtid, sum amounts
- const trackByWtid = allTracking.reduce((prev, cur) => {
- const fee = Amounts.parseOrThrow(cur.wireFee);
- const raw = Amounts.parseOrThrow(cur.amountRaw);
- const total = !prev[cur.wireTransferId]
- ? raw
- : Amounts.add(prev[cur.wireTransferId].total, raw).amount;
-
- prev[cur.wireTransferId] = {
- total,
- fee,
- };
- return prev;
- }, {} as Record<string, { total: AmountJson; fee: AmountJson }>);
+ const trackByWtid = allTracking.reduce(
+ (prev, cur) => {
+ const fee = Amounts.parseOrThrow(cur.wireFee);
+ const raw = Amounts.parseOrThrow(cur.amountRaw);
+ const total = !prev[cur.wireTransferId]
+ ? raw
+ : Amounts.add(prev[cur.wireTransferId].total, raw).amount;
+
+ prev[cur.wireTransferId] = {
+ total,
+ fee,
+ };
+ return prev;
+ },
+ {} as Record<string, { total: AmountJson; fee: AmountJson }>,
+ );
//remove wire fee from total amount
return Object.entries(trackByWtid).map(([id, info]) => ({
@@ -1724,7 +1723,7 @@ function TrackingDepositDetails({
</tr>
{wireTransfers.map((wire) => (
- <tr>
+ <tr key={wire.id}>
<td>{wire.id}</td>
<td>
<Amount value={wire.amount} />
@@ -1734,6 +1733,7 @@ function TrackingDepositDetails({
</PurchaseDetailsTable>
);
}
+
function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
@@ -1749,28 +1749,30 @@ function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1813,47 +1815,6 @@ function RefreshDetails({ amount }: { amount: AmountWithFee }): VNode {
);
}
-function TipDetails({ amount }: { amount: AmountWithFee }): VNode {
- const { i18n } = useTranslationContext();
-
- return (
- <PurchaseDetailsTable>
- <tr>
- <td>
- <i18n.Translate>Tip</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.value} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
-
- {Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
- )}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
- </PurchaseDetailsTable>
- );
-}
-
function Header({
timestamp,
total,
@@ -2001,12 +1962,13 @@ function ShowWithdrawalDetailForBankIntegrated({
if (
transaction.txState.major !== TransactionMajorState.Pending ||
transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
- )
+ ) {
return <Fragment />;
+ }
const raw = Amounts.parseOrThrow(transaction.amountRaw);
return (
<Fragment>
- <EnabledBySettings name="advanceMode">
+ <EnabledBySettings name="advancedMode">
<a
href="#"
onClick={(e) => {
@@ -2014,19 +1976,21 @@ function ShowWithdrawalDetailForBankIntegrated({
setShowDetails(!showDetails);
}}
>
- show details
+ Show details.
</a>
</EnabledBySettings>
{showDetails && (
<BankDetailsByPaytoType
amount={raw}
- accounts={transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
subject={transaction.withdrawalDetails.reservePub}
/>
)}
{!transaction.withdrawalDetails.confirmed &&
- transaction.withdrawalDetails.bankConfirmationUrl ? (
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
<InfoBox>
<div style={{ display: "block" }}>
<i18n.Translate>
@@ -2049,7 +2013,7 @@ function ShowWithdrawalDetailForBankIntegrated({
<InfoBox>
<i18n.Translate>
Bank has confirmed the wire transfer. Waiting for the exchange to
- send the coins
+ send the coins.
</i18n.Translate>
</InfoBox>
)}
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
index e19152be2..6a57fe18a 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
@@ -57,65 +57,41 @@ export function View({
return (
<Fragment>
<Title>
- <i18n.Translate>Browser Extension Installed!</i18n.Translate>
+ <i18n.Translate>GNU Taler Wallet installed!</i18n.Translate>
</Title>
<div>
<p>
<i18n.Translate>
- You can open the GNU Taler Wallet using the combination{" "}
+ You can open the wallet using the combination{" "}
<pre style="font-weight: bold; display: inline;">&lt;ALT+W&gt;</pre>
.
</i18n.Translate>
</p>
- {!platform.isFirefox() && (
- <Fragment>
- <p>
- <i18n.Translate>
- Also pinning the GNU Taler Wallet to your Chrome browser allows
- you to quick access without keyboard:
- </i18n.Translate>
- </p>
- <ol style={{ paddingLeft: 40 }}>
- <li>
- <i18n.Translate>Click the puzzle icon</i18n.Translate>
- </li>
- <li>
- <i18n.Translate>Search for GNU Taler Wallet</i18n.Translate>
- </li>
- <li>
- <i18n.Translate>Click the pin icon</i18n.Translate>
- </li>
- </ol>
- </Fragment>
- )}
- <SubTitle>
- <i18n.Translate>Navigator</i18n.Translate>
- </SubTitle>
- <Checkbox
- label={i18n.str`Inject Taler support in all pages`}
- name="inject"
- description={
+ <Fragment>
+ <p>
<i18n.Translate>
- Disabling this option will make some web application not able to
- trigger the wallet when clicking links but you will be able to
- open the wallet using the keyboard shortcut
+ Also pinning the GNU Taler Wallet to your browser allows
+ you to quick access without keyboard:
</i18n.Translate>
- }
- enabled={permissionToggle.value!}
- onToggle={permissionToggle.button.onClick!}
- />
+ </p>
+ <ol style={{ paddingLeft: 40 }}>
+ <li>
+ <i18n.Translate>Click the puzzle icon</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Search for GNU Taler Wallet</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Click the pin icon</i18n.Translate>
+ </li>
+ </ol>
+ </Fragment>
<SubTitle>
<i18n.Translate>Next Steps</i18n.Translate>
</SubTitle>
<a href="https://demo.taler.net/" style={{ display: "block" }}>
<i18n.Translate>Try the demo</i18n.Translate> »
</a>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
- <i18n.Translate>
- Learn how to top up your wallet balance
- </i18n.Translate>{" "}
- »
- </a>
</div>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
index 989292326..89bb75b29 100644
--- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -24,7 +24,6 @@ export * as a4 from "./DepositPage/stories.js";
export * as a7 from "./History.stories.js";
export * as a8 from "./AddBackupProvider/stories.js";
export * as a10 from "./ProviderDetail.stories.js";
-export * as a11 from "./ReserveCreated.stories.js";
export * as a12 from "./Settings.stories.js";
export * as a13 from "./Transaction.stories.js";
export * as a14 from "./Welcome.stories.js";
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 8fb8211ae..4394a982f 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -31,7 +31,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
- WalletDiagnostics,
+ WalletNotification
} from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
@@ -40,11 +40,13 @@ import {
WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
import {
+ ExtensionNotification,
MessageFromBackend,
MessageFromFrontendBackground,
MessageFromFrontendWallet,
} from "./platform/api.js";
import { platform } from "./platform/foreground.js";
+import { WalletActivityTrack } from "./wxBackend.js";
/**
*
@@ -53,26 +55,32 @@ import { platform } from "./platform/foreground.js";
const logger = new Logger("wxApi");
-export const WALLET_CORE_SUPPORTED_VERSION = "1:0:0"
+export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
export interface ExtendedPermissionsResponse {
newValue: boolean;
}
export interface BackgroundOperations {
- freeze: {
- request: number;
+ resetDb: {
+ request: void;
response: void;
};
- sum: {
- request: number[];
- response: number;
+ runGarbageCollector: {
+ request: void;
+ response: void;
};
- resetDb: {
+ reinitWallet: {
request: void;
response: void;
};
- runGarbageCollector: {
+ getNotifications: {
+ request: {
+ filter: string;
+ };
+ response: WalletActivityTrack[];
+ };
+ clearNotifications: {
request: void;
response: void;
};
@@ -83,16 +91,10 @@ export interface BackgroundOperations {
};
response: void;
};
- containsHeaderListener: {
- request: void;
- response: ExtendedPermissionsResponse;
- };
- toggleHeaderListener: {
- request: boolean;
- response: ExtendedPermissionsResponse;
- };
}
+export type WalletEvent = { notification: WalletNotification, when: AbsoluteTime }
+
export interface BackgroundApiClient {
call<Op extends keyof BackgroundOperations>(
operation: Op,
@@ -101,11 +103,13 @@ export interface BackgroundApiClient {
}
export class BackgroundError<T = any> extends Error {
- public errorDetail: TalerErrorDetail & T;
+ public readonly errorDetail: TalerErrorDetail & T;
+ public readonly cause: Error;
- constructor(title: string, e: TalerErrorDetail & T) {
+ constructor(title: string, e: TalerErrorDetail & T, cause: Error) {
super(title);
this.errorDetail = e;
+ this.cause = cause;
}
hasErrorCode<C extends keyof DetailsMap>(
@@ -138,7 +142,7 @@ class BackgroundApiClientImpl implements BackgroundApiClient {
throw new BackgroundError(operation, {
code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
when: AbsoluteTime.now(),
- });
+ }, error);
}
throw error;
}
@@ -146,6 +150,7 @@ class BackgroundApiClientImpl implements BackgroundApiClient {
throw new BackgroundError(
`Background operation "${operation}" failed`,
response.error,
+ TalerError.fromUncheckedDetail(response.error),
);
}
logger.trace("response", response);
@@ -169,14 +174,20 @@ class WalletApiClientImpl implements WalletCoreApiClient {
payload,
};
response = await platform.sendMessageToBackground(message);
- } catch (e) {
- logger.error("Error calling backend", e);
- throw new Error(`Error contacting backend: ${e}`);
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new BackgroundError(operation, {
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ when: AbsoluteTime.now(),
+ }, error);
+ }
+ throw error;
}
if (response.type === "error") {
throw new BackgroundError(
`Wallet operation "${operation}" failed`,
response.error,
+ TalerError.fromUncheckedDetail(response.error)
);
}
logger.trace("got response", response);
@@ -186,7 +197,7 @@ class WalletApiClientImpl implements WalletCoreApiClient {
function onUpdateNotification(
messageTypes: Array<NotificationType>,
- doCallback: undefined | (() => void),
+ doCallback: undefined | ((n: WalletNotification) => void),
): () => void {
//if no callback, then ignore
if (!doCallback)
@@ -194,9 +205,9 @@ function onUpdateNotification(
return;
};
const onNewMessage = (message: MessageFromBackend): void => {
- const shouldNotify = messageTypes.includes(message.type);
+ const shouldNotify = message.type === "wallet" && messageTypes.includes(message.notification.type);
if (shouldNotify) {
- doCallback();
+ doCallback(message.notification);
}
};
return platform.listenToWalletBackground(onNewMessage);
@@ -206,14 +217,23 @@ export type WxApiType = {
wallet: WalletCoreApiClient;
background: BackgroundApiClient;
listener: {
+ trigger: (d: ExtensionNotification) => void;
onUpdateNotification: typeof onUpdateNotification;
};
};
+function trigger(w: ExtensionNotification) {
+ platform.triggerWalletEvent({
+ type: "web-extension",
+ notification: w,
+ })
+}
+
export const wxApi = {
wallet: new WalletApiClientImpl(),
background: new BackgroundApiClientImpl(),
listener: {
+ trigger,
onUpdateNotification,
},
};
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index a194de0ff..5fa255f5d 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -24,36 +24,40 @@
* Imports.
*/
import {
+ AbsoluteTime,
LogLevel,
Logger,
+ NotificationType,
+ OpenedPromise,
+ SetTimeoutTimerAPI,
+ TalerError,
TalerErrorCode,
+ TalerErrorDetail,
+ TransactionMinorState,
+ WalletNotification,
getErrorDetailFromException,
makeErrorDetail,
+ openPromise,
setGlobalLogLevelFromString,
setLogLevelFromString
} from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
DbAccess,
- OpenedPromise,
- SetTimeoutTimerAPI,
SynchronousCryptoWorkerFactoryPlain,
Wallet,
+ WalletApiOperation,
WalletOperations,
WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
- openPromise,
} from "@gnu-taler/taler-wallet-core";
-import {
- BrowserHttpLib,
- ServiceWorkerHttpLib,
-} from "@gnu-taler/web-util/browser";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
import { MessageFromFrontend, MessageResponse } from "./platform/api.js";
import { platform } from "./platform/background.js";
import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
-import { BackgroundOperations, ExtendedPermissionsResponse } from "./wxApi.js";
+import { BackgroundOperations } from "./wxApi.js";
/**
* Currently active wallet instance. Might be unloaded and
@@ -65,11 +69,6 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version of an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
const logger = new Logger("wxBackend.ts");
@@ -91,6 +90,164 @@ async function resetDb(): Promise<void> {
await reinitWallet();
}
+export type WalletActivityTrack = {
+ id: number;
+ events: (WalletNotification & {when: AbsoluteTime})[];
+ start: AbsoluteTime;
+ type: NotificationType;
+ end: AbsoluteTime;
+ groupId: string;
+};
+
+let counter = 0;
+function getUniqueId(): number {
+ return counter++;
+}
+
+//FIXME: maybe circular buffer
+const activity: WalletActivityTrack[] = [];
+
+function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) {
+ const start = AbsoluteTime.now();
+ const ev = {...n, when:start};
+ switch (n.type) {
+ case NotificationType.BalanceChange: {
+ const groupId = `${n.type}:${n.hintTransactionId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.BackupOperationError: {
+ const groupId = "";
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.TransactionStateTransition: {
+ const groupId = `${n.type}:${n.transactionId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.WithdrawalOperationTransition: {
+ return;
+ }
+ case NotificationType.ExchangeStateTransition: {
+ const groupId = `${n.type}:${n.exchangeBaseUrl}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.Idle: {
+ const groupId = "";
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ const groupId = `${n.type}:${n.taskId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ const groupId = `${n.type}:${n.operation}:${n.requestId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ }
+}
+
+async function getNotifications({
+ filter,
+}: {
+ filter: string;
+}): Promise<WalletActivityTrack[]> {
+ if (!filter) return activity;
+
+ const rg = new RegExp(`.*${filter}.*`);
+ return activity.filter((event) => {
+ return rg.test(event.groupId.toLowerCase());
+ });
+}
+
+async function clearNotifications(): Promise<void> {
+ activity.splice(0, activity.length);
+}
+
async function runGarbageCollector(): Promise<void> {
const dbBeforeGc = currentDatabase;
if (!dbBeforeGc) {
@@ -111,46 +268,30 @@ async function runGarbageCollector(): Promise<void> {
logger.info("imported");
}
-function freeze(time: number): Promise<void> {
- return new Promise((res, rej) => {
- setTimeout(res, time);
- });
-}
-
-async function sum(ns: Array<number>): Promise<number> {
- return ns.reduce((prev, cur) => prev + cur, 0);
-}
-
const extensionHandlers: ExtensionHandlerType = {
- isInjectionEnabled,
isAutoOpenEnabled,
+ isDomainTrusted,
};
-async function isInjectionEnabled(): Promise<boolean> {
+async function isAutoOpenEnabled(): Promise<boolean> {
const settings = await platform.getSettingsFromStorage();
- return settings.injectTalerSupport === true;
+ return settings.autoOpen === true;
}
-async function isAutoOpenEnabled(): Promise<boolean> {
+async function isDomainTrusted(): Promise<boolean> {
const settings = await platform.getSettingsFromStorage();
- return settings.autoOpen === true;
+ return settings.injectTalerSupport === true;
}
const backendHandlers: BackendHandlerType = {
- freeze,
- sum,
resetDb,
runGarbageCollector,
+ getNotifications,
+ clearNotifications,
+ reinitWallet,
setLoggingLevel,
- containsHeaderListener,
- toggleHeaderListener,
};
-async function containsHeaderListener(): Promise<ExtendedPermissionsResponse> {
- const result = platform.containsTalerHeaderListener();
- return { newValue: result };
-}
-
async function setLoggingLevel({
tag,
level,
@@ -165,10 +306,13 @@ async function setLoggingLevel({
setLogLevelFromString(tag, level);
}
}
+let nextMessageIndex = 0;
async function dispatch<
Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
+
switch (req.channel) {
case "background": {
const handler = backendHandlers[req.operation] as (req: any) => any;
@@ -231,19 +375,34 @@ async function dispatch<
case "wallet": {
const w = currentWallet;
if (!w) {
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
+
return {
type: "error",
id: req.id,
operation: req.operation,
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
- {},
- "wallet core not available",
+ { lastError },
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
),
};
}
-
- return await w.handleCoreApiRequest(req.operation, req.id, req.payload);
+ //multiple client can create the same id, send the wallet an unique key
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
+ //return to the client the original id
+ resp.id = req.id;
+ return resp;
}
}
@@ -262,21 +421,24 @@ async function dispatch<
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
// setBadgeText({ text: "" });
- let httpLib: HttpRequestLibrary;
let cryptoWorker;
let timer;
+ const httpFactory = (): HttpRequestLibrary => {
+ return new BrowserFetchHttpLib({
+ // enableThrottling: false,
+ });
+ };
+
if (platform.useServiceWorkerAsBackgroundProcess()) {
- httpLib = new ServiceWorkerHttpLib();
cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
} else {
- httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
@@ -288,37 +450,46 @@ async function reinitWallet(): Promise<void> {
logger.info("Setting up wallet");
const wallet = await Wallet.create(
indexedDB as any,
- httpLib as any,
+ httpFactory as any,
timer,
cryptoWorker,
- {
- features: {
- allowHttp: settings.walletAllowHttp,
- },
- },
);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {});
+ await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ testing: {
+ emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
+ },
+ features: {
+ allowHttp: settings.walletAllowHttp,
+ },
+ },
+ });
} catch (e) {
logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
wallet.addNotificationListener((message) => {
- logger.info("wallet -> ui", message);
- platform.sendMessageToAllChannels(message);
- });
+ if (settings.showWalletActivity) {
+ addNewWalletActivityNotification(activity, message);
+ }
- platform.keepAlive(() => {
- return wallet.runTaskLoop().catch((e) => {
- logger.error("error during wallet task loop", e);
+ processWalletNotification(message);
+
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message,
});
});
+
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
+ updateIconBasedOnBalance();
return walletInit.resolve();
}
@@ -355,66 +526,47 @@ export async function wxMain(): Promise<void> {
} catch (e) {
console.error(e);
}
+}
- // platform.registerDeclarativeRedirect();
- // if (false) {
- /**
- * this is not working reliable on chrome, just
- * intercepts queries after the user clicks the popups
- * which doesn't make sense, keeping it to make more tests
- */
-
- logger.trace("check taler header listener");
- const enabled = platform.containsTalerHeaderListener()
- if (!enabled) {
- logger.info("header listener on")
- const perm = await platform.getPermissionsApi().containsHostPermissions()
- if (perm) {
- logger.info("header listener allowed")
- try {
- platform.registerTalerHeaderListener();
- } catch (e) {
- logger.error("could not register header listener", e);
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags));
+ showAlert = true;
+ break;
}
- } else {
- logger.info("header listener requested")
- await platform.getPermissionsApi().requestHostPermissions()
}
- }
- // On platforms that support it, also listen to external
- // modification of permissions.
- platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
- logger.info(`permission added: ${perm}`,)
- if (lastError) {
- logger.error(
- `there was a problem trying to get permission ${perm}`,
- lastError,
- );
- return;
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
}
- platform.registerTalerHeaderListener();
- });
-
- // }
+ }
}
-
-async function toggleHeaderListener(
- newVal: boolean,
-): Promise<ExtendedPermissionsResponse> {
- logger.trace("new extended permissions value", newVal);
- if (newVal) {
- try {
- platform.registerTalerHeaderListener();
- return { newValue: true };
- } catch (e) {
- logger.error("FAIL to toggle",e)
- }
- return { newValue: false }
+/**
+ * All the actions triggered by notification that need to be
+ * run in the background.
+ *
+ * @param message
+ */
+async function processWalletNotification(message: WalletNotification) {
+ if (
+ message.type === NotificationType.TransactionStateTransition &&
+ (message.newTxState.minor === TransactionMinorState.KycRequired ||
+ message.oldTxState.minor === TransactionMinorState.KycRequired ||
+ message.newTxState.minor === TransactionMinorState.AmlRequired ||
+ message.oldTxState.minor === TransactionMinorState.AmlRequired ||
+ message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
+ message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
+ ) {
+ await updateIconBasedOnBalance();
}
-
- const rem = await platform.getPermissionsApi().removeHostPermissions();
- logger.trace("permissions removed:", rem);
- return { newValue: false };
}