summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension')
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json3
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx61
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx200
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx68
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1539
-rw-r--r--packages/taler-wallet-webextension/src/context/alert.ts71
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts73
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx33
-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/views.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx44
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts12
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts44
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts3
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po16
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ru.po1977
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts51
-rw-r--r--packages/taler-wallet-webextension/src/platform/background.ts3
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts81
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts2
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx11
-rw-r--r--packages/taler-wallet-webextension/src/svg/search_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts11
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts7
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx115
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx82
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts7
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx257
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx257
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx136
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx855
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts253
51 files changed, 4970 insertions, 1782 deletions
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 0df768182..32bd5267f 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.9.4.35",
+ "version": "0.10.7",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.9.4"
+ "version_name": "0.10.7"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 3970ba32c..bf063d76e 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.9.4",
+ "version": "0.10.7",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
@@ -41,7 +41,6 @@
"@typescript-eslint/parser": "^6.19.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.2",
-
"@babel/preset-react": "^7.22.3",
"@babel/preset-typescript": "7.18.6",
"@gnu-taler/pogen": "workspace:*",
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 78a526997..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",
),
@@ -127,6 +135,8 @@ export const Pages = {
ctaRefund: "/cta/refund",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
"/cta/invoice/create/:amount?",
),
@@ -151,7 +161,8 @@ const talerUriActionToPageName: {
[TalerUriAction.Restore]: "ctaRecovery",
[TalerUriAction.PayTemplate]: "ctaPayTemplate",
[TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
- [TalerUriAction.DevExperiment]: undefined,
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
};
export function getPathnameForTalerURI(talerUri: string): string | undefined {
@@ -265,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 b8bcaa391..6dd577b88 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -15,8 +15,10 @@
*/
import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index.js";
+import { Fragment, VNode, h } from "preact";
+import {
+ TableWithRoundRows as TableWithRoundedRows
+} from "./styled/index.js";
export function BalanceTable({
balances,
@@ -26,32 +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)}
- <div style={{ fontSize: "small", color: "grey" }}>
- {entry.scopeInfo.type === ScopeType.Exchange || entry.scopeInfo.type === ScopeType.Auditor ? entry.scopeInfo.url : undefined}
- </div>
- </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 bf77174df..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,72 +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"] = 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>
-
+ 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>
- </div>
- {children}
+ {children}
- {accounts.length > 1 ?
- <Fragment>
- {accounts.map((ac, acIdx) => {
- return <Button variant={acIdx === index ? "contained" : "outlined"}
- onClick={async () => {
- setIndex(acIdx)
- }}
- >
- <i18n.Translate>Account #{acIdx+1} ({ac.currencySpecification?.name ?? amount.currency})</i18n.Translate>
- </Button>
- })}
+ {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>
+ );
+ })}
- {/* <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>
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
}
if (payto.isKnown && payto.targetType === "bitcoin") {
@@ -157,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} />
@@ -172,30 +194,68 @@ export function BankDetailsByPaytoType({
</Fragment>
) : undefined;
- const receiver = payto.params["receiver-name"] || payto.params["receiver"] || undefined;
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
return (
<Frame title={i18n.str`Bank transfer details`}>
<table>
<tbody>
- {accountPart}
+ <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 />
- <Row
- name={i18n.str`Amount`}
- value={<Amount value={altCurrency ? selectedAccount.transferAmount! : amount} hideCurrency />}
- />
-
+ <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={altCurrency ? selectedAccount.transferAmount! : amount}
+ hideCurrency
+ />
+ }
+ />
+
+ <tr>
+ <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
+ 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>
@@ -204,22 +264,20 @@ export function BankDetailsByPaytoType({
</tr>
<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>
- <td width="100%" style={{ wordBreak: "break-all" }}>
- {stringifyPaytoUri(payto)}
+ <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)} />
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index 4b44365ea..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,10 +136,6 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
- case TransactionType.Reward:
- return (
- <div>not supported</div>
- );
case TransactionType.Refresh:
return (
<Layout
@@ -155,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}
@@ -172,6 +173,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
+ }
case TransactionType.PeerPullCredit:
return (
<Layout
@@ -240,6 +242,56 @@ 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: {
@@ -256,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 5553c72df..f8c0f1651 100644
--- a/packages/taler-wallet-webextension/src/components/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -52,7 +52,7 @@ const Body = styled.div`
export function Modal({ title, children, onClose }: Props): VNode {
return (
- <div style={{ position: "fixed", top: 0, width: "100%", height: "100%" }}>
+ <div style={{ top: 0, width: "100%", height: "100%" }}>
<FullSize onClick={onClose?.onClick}>
<div
@@ -64,6 +64,7 @@ export function Modal({ title, children, onClose }: Props): VNode {
margin: "auto",
borderRadius: 8,
padding: 8,
+ zIndex: 100,
// overflow: "scroll",
}}
>
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 99e2d0a76..0e23d5850 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -81,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 b0f43d0d9..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,7 +161,7 @@ export function LoadingView({ hideHandler }: States.Loading): VNode {
export function ErrorView({
hideHandler,
error,
- proposalId,
+ transactionId,
}: States.Error): VNode {
const { i18n } = useTranslationContext();
return (
@@ -170,7 +171,7 @@ export function ErrorView({
i18n,
i18n.str`Could not load purchase proposal details`,
error,
- { proposalId },
+ { transactionId },
)}
/>
</Modal>
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
index 60839e1f0..41b0c5c76 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -15,6 +15,7 @@
*/
import {
AbsoluteTime,
+ ExchangeStateTransitionNotification,
NotificationType,
ObservabilityEventType,
RequestProgressNotification,
@@ -22,809 +23,1029 @@ import {
TalerErrorDetail,
TaskProgressNotification,
WalletNotification,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, JSX, VNode, h } from "preact";
+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 { SafeHandler } from "../mui/handlers.js";
import { WxApiType } from "../wxApi.js";
import { Modal } from "./Modal.js";
import { Time } from "./Time.js";
+import { TextField } from "../mui/TextField.js";
+import { WalletActivityTrack } from "../wxBackend.js";
-interface Props extends JSX.HTMLAttributes {
-}
+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);
-export function WalletActivity({ }: Props): VNode {
- const { i18n } = useTranslationContext()
- const [settings, updateSettings] = useSettings()
- const api = useBackendContext();
useEffect(() => {
- document.body.style.marginBottom = "250px"
+ document.body.style.marginBottom = `${
+ collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX
+ }px`;
return () => {
- document.body.style.marginBottom = "0px"
- }
- })
- const [table, setTable] = useState<"tasks" | "events">("tasks")
- return (
- <div style={{ position: "fixed", bottom: 0, background: "white", zIndex: 1, height: 250, overflowY: "scroll", width: "100%" }}>
- <div style={{ display: "flex", justifyContent: "space-between", float: "right" }}>
- <div />
- <div>
- <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
- updateSettings("showWalletActivity", false)
- }}>
- close
- </div>
+ 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>
- <div style={{ display: "flex", justifyContent: "space-around" }}>
- <Button variant={table === "tasks" ? "contained" : "outlined"}
+ );
+ }
+ 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("tasks")
+ setTable("events");
}}
>
- <i18n.Translate>Tasks</i18n.Translate>
+ <i18n.Translate>Events</i18n.Translate>
</Button>
- <Button variant={table === "events" ? "contained" : "outlined"}
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
style={{ margin: 4 }}
onClick={async () => {
- setTable("events")
+ setTable("tasks");
}}
>
- <i18n.Translate>Events</i18n.Translate>
+ <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>
- {(function (): VNode {
- switch (table) {
- case "events": {
- return <ObservabilityEventsTable />
- }
- case "tasks": {
- return <ActiveTasksTable />
+ <div
+ style={{
+ backgroundColor: "white",
+ }}
+ >
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
}
- default: {
- assertUnreachable(table)
- }
- }
- })()}
+ })()}
+ </div>
</div>
);
}
-interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[], onClick: (content: VNode) => void }
-type Notif = {
- id: string;
+interface MoreInfoPRops {
events: (WalletNotification & { when: AbsoluteTime })[];
- description: string;
- start: AbsoluteTime;
- end: AbsoluteTime;
- reference: {
- eventType: NotificationType,
- referenceType: "task" | "transaction" | "operation" | "exchange",
- id: string;
- } | undefined,
- MoreInfo: (p: MoreInfoPRops) => VNode;
+ 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>
+ 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>
+ return (
+ <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>
+ <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> : undefined}
- <dt>Experimental</dt>
- <dd>
- <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
- {JSON.stringify(not.experimentalUserData, undefined, 2)}
- </pre>
- </dd>
-
-
- </Fragment>
+ </Fragment>
+ );
}
-function ShowExchangeStateTransition({ events, onClick }: MoreInfoPRops): VNode {
+
+function ShowTransactionStateTransition({
+ events,
+ onClick,
+}: 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>
+ if (not.type !== NotificationType.TransactionStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Old state</dt>
<dd>
- from {not.oldExchangeState.exchangeEntryStatus} to {not.newExchangeState.exchangeEntryStatus}
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
</dd>
- </Fragment>}
- {not.oldExchangeState && not.newExchangeState.exchangeUpdateStatus !== not.oldExchangeState?.exchangeUpdateStatus && <Fragment>
- <dt>Update status</dt>
+ <dt>New state</dt>
<dd>
- from {not.oldExchangeState.exchangeUpdateStatus} to {not.newExchangeState.exchangeUpdateStatus}
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
</dd>
- </Fragment>}
- {not.oldExchangeState && not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && <Fragment>
- <dt>Tos status</dt>
+ <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>
- from {not.oldExchangeState.tosStatus} to {not.newExchangeState.tosStatus}
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
</dd>
- </Fragment>}
- </Fragment>
+ </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) & {
+type ObservaNotifWithTime = (
+ | TaskProgressNotification
+ | RequestProgressNotification
+) & {
when: AbsoluteTime;
-})
+};
function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
// let prev: ObservaNotifWithTime;
- const asd = events.map(not => {
- if (not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent) return <Fragment />;
+ 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.HttpFetchStart:
+ return "HTTP Request";
case ObservabilityEventType.DbQueryFinishSuccess:
case ObservabilityEventType.DbQueryFinishError:
- case ObservabilityEventType.DbQueryStart: return "Database"
+ case ObservabilityEventType.DbQueryStart:
+ return "Database";
case ObservabilityEventType.RequestFinishSuccess:
case ObservabilityEventType.RequestFinishError:
- case ObservabilityEventType.RequestStart: return "Wallet"
+ 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.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 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>
+ 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 {
+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) => {
+ return (
+ <tr>
+ <td>
+ <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>
-
+ 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" }}
+ 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>,
+ );
+ }}
>
- {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>
+ {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" }}
+ 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>,
+ );
+ }}
>
- {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>
+ {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" }}
+ 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>,
+ );
+ }}
>
- {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>
-
+ {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" }}
+ 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>,
+ );
+ }}
>
- {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>
+ {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" }}
+ 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>,
+ );
+ }}
>
- {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>
+ {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 getNotificationFor(id: string, event: WalletNotification, start: AbsoluteTime, list: Notif[]): Notif | undefined {
- const eventWithTime = { ...event, when: start }
- switch (event.type) {
- case NotificationType.BalanceChange: {
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "transaction",
- id: event.hintTransactionId,
- },
- description: "Balance change",
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowBalanceChange
- })
- }
- case NotificationType.BackupOperationError: {
- return ({
- id,
- events: [eventWithTime],
- reference: undefined,
- description: "Backup error",
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowBackupOperationError
- })
- }
- case NotificationType.TransactionStateTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.transactionId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "transaction",
- id: event.transactionId,
- },
- description: event.type,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowTransactionStateTransition
- })
- }
- case NotificationType.ExchangeStateTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.exchangeBaseUrl)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- description: "Exchange update",
- reference: {
- eventType: event.type,
- referenceType: "exchange",
- id: event.exchangeBaseUrl,
- },
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowExchangeStateTransition
- })
- }
- case NotificationType.TaskObservabilityEvent: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.taskId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "task",
- id: event.taskId,
- },
- description: `Task update ${event.taskId}`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- case NotificationType.WithdrawalOperationTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.uri)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "task",
- id: event.uri,
- },
- description: `Withdrawal operation updated`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- case NotificationType.RequestObservabilityEvent: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.requestId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "operation",
- id: event.requestId,
- },
- description: `wallet.${event.operation}(${event.requestId})`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- default: {
- assertUnreachable(event)
- }
- }
+function refresh(
+ api: WxApiType,
+ onUpdate: (list: WalletActivityTrack[]) => void,
+ filter: string,
+) {
+ api.background
+ .call("getNotifications", { filter })
+ .then((notif) => {
+ onUpdate(notif);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
}
-
-function refresh(api: WxApiType, onUpdate: (list: Notif[]) => void) {
- api.background.call("getNotifications", undefined).then(notif => {
-
- const list: Notif[] = []
- for (const n of notif) {
- if (n.notification.type === NotificationType.RequestObservabilityEvent &&
- n.notification.operation === "getActiveTasks") {
- //ignore monitor request
- continue;
- }
- const event = getNotificationFor(String(list.length), n.notification, n.when, list)
- // pepe.
- if (event) {
- list.unshift(event)
- }
- }
- onUpdate(list);
- }).catch(error => {
- console.log(error)
- })
-}
-
-export function ObservabilityEventsTable({ }: {}): VNode {
- const { i18n } = useTranslationContext()
+export function ObservabilityEventsTable(): VNode {
+ const { i18n } = useTranslationContext();
const api = useBackendContext();
- const [notifications, setNotifications] = useState<Notif[]>([])
- const [showDetails, setShowDetails] = useState<VNode>()
+ 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)
+ refresh(api, setNotifications, filter);
lastTimeout = setTimeout(() => {
periodicRefresh();
- }, 1000)
+ }, 1000);
- //clear on unload
- return () => { clearTimeout(lastTimeout) }
+ return () => {
+ clearTimeout(lastTimeout);
+ };
}
- return periodicRefresh()
- }, [1]);
-
- return <div>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
+ return periodicRefresh();
+ }, [filter]);
- <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
- api.background.call("clearNotifications", undefined).then(d => {
- refresh(api, setNotifications)
- })
- }}>
- clear
+ 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>
-
-
- </div>
- {showDetails && <Modal title="event details" onClose={{ onClick: (async () => { setShowDetails(undefined) }) as any }} >
- {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 }}>
- {not.description}
- </div>
- <div style={{ padding: 4 }}>
- <Time
- timestamp={not.start}
- format="yyyy/MM/dd HH:mm:ss"
- />
+ {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>
- <div style={{ padding: 4 }}><Time
- timestamp={not.end}
- format="yyyy/MM/dd HH:mm:ss"
- /></div>
- </div>
- </summary>
- <not.MoreInfo events={not.events} onClick={(details) => {
- setShowDetails(details)
- }} />
- </details>
- );
- })}
- </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 any
- }}>
- <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>
+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()
+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 [showError, setShowError] = useState<TalerErrorDetail>();
const tasks = state && !state.hasError ? state.response.tasks : [];
useEffect(() => {
- if (!state || state.hasError) return
+ if (!state || state.hasError) return;
const lastTimeout = setTimeout(() => {
state.retry();
- }, 1000)
+ }, 1000);
return () => {
- clearTimeout(lastTimeout)
- }
- }, [tasks])
+ clearTimeout(lastTimeout);
+ };
+ }, [tasks]);
- // const listenAllEvents = Array.from<NotificationType>({ length: 1 });
- // listenAllEvents.includes = () => true
- // useEffect(() => {
- // return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
- // state?.retry()
- // });
- // });
- return <Fragment>
- {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />}
+ 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.id.split(":")
- return (
- <tr>
- <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>
-} \ No newline at end of file
+ <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/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts
index 36de7c7e4..e30fdd72c 100644
--- a/packages/taler-wallet-webextension/src/context/alert.ts
+++ b/packages/taler-wallet-webextension/src/context/alert.ts
@@ -19,13 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerError, TalerErrorCode, 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 { InternationalizationAPI, 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";
@@ -175,9 +183,14 @@ export function alertFromError(
//HookError
description = error.message as TranslatedString;
if (error.type === "taler") {
- const msg = isWalletNotAvailable(i18n,error.details)
+ const msg = isWalletNotAvailable(i18n, error.details);
if (msg) {
- description = msg
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.details);
+ if (msg2) {
+ description = msg2;
+ }
}
cause = {
details: error.details,
@@ -185,12 +198,17 @@ export function alertFromError(
}
} else {
if (error instanceof BackgroundError) {
- const msg = isWalletNotAvailable(i18n,error.errorDetail)
+ const msg = isWalletNotAvailable(i18n, error.errorDetail);
if (msg) {
- description = msg
+ description = msg;
} else {
- description = (error.errorDetail.hint ??
- `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ 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,
@@ -217,20 +235,43 @@ export function alertFromError(
};
}
-function isWalletNotAvailable(i18n: InternationalizationAPI, detail: TalerErrorDetail): TranslatedString | undefined {
- if (detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE
- && detail.lastError) {
- const le = detail.lastError as TalerErrorDetail
+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".`
+ 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.`
+ 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
+ return undefined;
}
+//
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
new file mode 100644
index 000000000..ec09fd9f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
@@ -0,0 +1,73 @@
+/*
+ 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 { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { InsertLostView, InsertPendingRefreshView, UnknownView } from "./views.js";
+
+export interface Props {
+ talerExperimentUri: string | undefined;
+ onCancel: () => Promise<void>;
+ onSuccess: () => Promise<void>;
+}
+
+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 InsertLost {
+ status: "insertLost";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface PendingRefresh {
+ status: "pendingRefresh";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface Unknown {
+ status: "unknown";
+ experimentId: string;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ pendingRefresh: InsertPendingRefreshView,
+ insertLost: InsertLostView,
+ unknown: UnknownView,
+};
+
+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/DevExperiment/stories.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
new file mode 100644
index 000000000..c9851495f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
@@ -0,0 +1,33 @@
+/*
+ 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 { InsertLostView } from "./views.js";
+
+export default {
+ title: "dev-experiment",
+};
+
+export const Ready = tests.createExample(InsertLostView, {
+ status: "insertLost",
+ confirm: {},
+ error: undefined,
+});
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/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index fa7127fc0..e2c37fbba 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -18,13 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { Part } from "../../components/Part.js";
-import {
- SvgIcon
-} from "../../components/styled/index.js";
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,
@@ -39,7 +35,7 @@ export function ReadyView({
create,
toBeReceived,
requestAmount,
- doSelectExchange,
+ doSelectExchange: _doSelectExchange,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -58,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"),
);
}
}
@@ -131,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/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index ef135c1ba..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";
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 1542c8f29..8bbb8dac2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -21,18 +21,16 @@ 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";
+import { 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
@@ -67,29 +65,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.transactionId}
- />
- }
- 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 +80,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/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
index b9257215f..6b4584fea 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -14,7 +14,7 @@
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 { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
@@ -54,7 +54,7 @@ export function useComponentState({
const hook = useAsyncAsHook(async () => {
if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
- let payStatus;
+ let payStatus: PreparePayResult | undefined = undefined;
if (!amountParam && !summaryParam) {
payStatus = await api.wallet.call(
WalletApiOperation.PreparePayForTemplate,
@@ -125,7 +125,9 @@ export function useComponentState({
},
);
setNewOrder(payStatus.talerUri!);
- } catch (e) {}
+ } catch (e) {
+ console.error(e);
+ }
}
const errors = undefinedIfEmpty({
amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
@@ -164,7 +166,9 @@ export function useComponentState({
}
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/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/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 05aef690e..044f2434f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -14,15 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/* eslint-disable react-hooks/rules-of-hooks */
import {
AmountJson,
Amounts,
ExchangeFullDetails,
ExchangeListItem,
NotificationType,
- TalerError,
- parseWithdrawExchangeUri,
+ parseWithdrawExchangeUri
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -54,7 +52,7 @@ export function useComponentStateFromParams({
: undefined;
const exchangeByTalerUri = uri?.exchangeBaseUrl;
let ex: ExchangeFullDetails | undefined;
- if (exchangeByTalerUri && uri.exchangePub) {
+ if (exchangeByTalerUri) {
await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchangeByTalerUri,
masterPub: uri.exchangePub,
@@ -141,8 +139,8 @@ export function useComponentStateFromParams({
confirm: {
onClick: isValid
? pushAlertOnError(async () => {
- onAmountChanged(Amounts.stringify(amount));
- })
+ onAmountChanged(Amounts.stringify(amount));
+ })
: undefined,
},
amount: {
@@ -209,7 +207,7 @@ export function useComponentStateFromURI({
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri,
- notifyChangeFromPendingTimeoutMs: 30 * 1000,
+ // notifyChangeFromPendingTimeoutMs: 30 * 1000,
},
);
const {
@@ -244,9 +242,7 @@ export function useComponentStateFromURI({
}
return api.listener.onUpdateNotification(
[NotificationType.WithdrawalOperationTransition],
- (asd) => {
- uriInfoHook.retry();
- },
+ uriInfoHook.retry,
);
}, [readyToListen]);
@@ -377,9 +373,6 @@ function exchangeSelectionState(
};
}, []);
- const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
- undefined,
- );
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
async function doWithdrawAndCheckError(): Promise<void> {
@@ -395,9 +388,9 @@ function exchangeSelectionState(
onSuccess(res.transactionId);
}
} catch (e) {
- if (e instanceof TalerError) {
- setWithdrawError(e);
- }
+ console.error(e);
+ // if (e instanceof TalerError) {
+ // }
}
setDoingWithdraw(false);
}
@@ -439,12 +432,12 @@ 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
@@ -454,6 +447,7 @@ function exchangeSelectionState(
altCurrencies.length === 0
? []
: [toBeReceived.currency, ...altCurrencies];
+
const convAccount = amountHook.response.accounts.find((c) => {
return (
c.currencySpecification &&
@@ -463,9 +457,9 @@ function exchangeSelectionState(
const conversionInfo = !convAccount
? undefined
: {
- spec: convAccount.currencySpecification!,
- amount: Amounts.parseOrThrow(convAccount.transferAmount!),
- };
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
return {
status: "success",
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/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/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/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/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index e92903981..3c116fab2 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -18,11 +18,9 @@ import {
CoreApiResponse,
TalerUri,
WalletNotification,
- WalletRunConfig
+ WalletRunConfig,
} from "@gnu-taler/taler-util";
-import {
- WalletOperations
-} from "@gnu-taler/taler-wallet-core";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import {
ExtensionOperations,
MessageFromExtension,
@@ -46,11 +44,9 @@ export interface Permissions {
* Compatibility API that works on multiple browsers.
*/
export interface CrossBrowserPermissionsApi {
-
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
-
}
export enum ExtensionNotificationType {
@@ -67,25 +63,29 @@ export interface ClearNotificaitonNotification {
type: ExtensionNotificationType.ClearNotifications;
}
-export type ExtensionNotification = SettingsChangeNotification | ClearNotificaitonNotification
+export type ExtensionNotification =
+ | SettingsChangeNotification
+ | ClearNotificaitonNotification;
-export type MessageFromBackend = {
- type: "wallet",
- notification: WalletNotification
-} | {
- type: "web-extension",
- notification: ExtensionNotification
-};
+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,
@@ -109,7 +109,6 @@ export interface WalletWebExVersion {
}
type F = WalletRunConfig["features"];
-type kf = keyof F;
type WebexWalletConfig = {
[P in keyof F as `wallet${Capitalize<P>}`]: F[P];
};
@@ -231,7 +230,13 @@ export interface BackgroundPlatformAPI {
) => Promise<MessageResponse>,
): void;
+ /**
+ * Change web extension Icon
+ */
+ setAlertedIcon(): void;
+ setNormalIcon(): void;
}
+
export interface ForegroundPlatformAPI {
/**
* Check if the extension is running under
@@ -270,7 +275,7 @@ export interface ForegroundPlatformAPI {
/**
* Open a page and close the popup
- * @param url
+ * @param url
*/
openNewURLFromPopup(url: URL): void;
/**
@@ -309,9 +314,9 @@ export interface ForegroundPlatformAPI {
): Promise<MessageResponse>;
/**
- * Used by the wallet frontend to send notification about new information
- * @param message
- */
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
triggerWalletEvent(message: MessageFromBackend): void;
/**
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 ee071347a..e63040f5c 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -53,7 +53,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels: listenToAllChannels as any,
+ listenToAllChannels ,
registerReloadOnNewVersion,
sendMessageToAllChannels,
openNewURLFromPopup,
@@ -61,6 +61,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
useServiceWorkerAsBackgroundProcess,
keepAlive,
listenNetworkConnectionState,
+ setAlertedIcon,
+ setNormalIcon,
};
export default api;
@@ -69,7 +71,7 @@ const logger = new Logger("chrome.ts");
const WALLET_STORAGE_KEY = "wallet-settings";
-function jsonParseOrDefault(unparsed: any, def: any) {
+function jsonParseOrDefault(unparsed: string, def: unknown) {
if (!unparsed) return def;
try {
return JSON.parse(unparsed);
@@ -85,7 +87,7 @@ async function getSettingsFromStorage(): Promise<Settings> {
return jsonParseOrDefault(settings, defaultSettings);
}
-function keepAlive(callback: any): void {
+function keepAlive(callback: () => void): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@@ -103,7 +105,7 @@ function isFirefox(): boolean {
}
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;
@@ -116,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;
@@ -129,7 +131,7 @@ 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;
@@ -154,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 {
@@ -221,6 +223,13 @@ 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;
@@ -269,7 +278,7 @@ async function sendMessageToBackground<
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(() => {
@@ -300,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" });
}
@@ -362,7 +371,7 @@ function registerAllIncomingConnections(): void {
notification: {
type: ExtensionNotificationType.SettingsChange,
currentValue: jsonParseOrDefault(
- event[WALLET_STORAGE_KEY],
+ event[WALLET_STORAGE_KEY].newValue,
defaultSettings,
),
},
@@ -408,12 +417,12 @@ 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,
@@ -474,26 +483,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D
extends CanvasState,
- CanvasTransform,
- CanvasCompositing,
- CanvasImageSmoothing,
- CanvasFillStrokeStyles,
- CanvasShadowStyles,
- CanvasFilters,
- CanvasRect,
- CanvasDrawPath,
- CanvasUserInterface,
- CanvasText,
- CanvasDrawImage,
- CanvasImageData,
- CanvasPathDrawingStyles,
- CanvasTextDrawingStyles,
- CanvasPath {
+ CanvasTransform,
+ CanvasCompositing,
+ CanvasImageSmoothing,
+ CanvasFillStrokeStyles,
+ CanvasShadowStyles,
+ CanvasFilters,
+ CanvasRect,
+ CanvasDrawPath,
+ CanvasUserInterface,
+ CanvasText,
+ CanvasDrawImage,
+ CanvasImageData,
+ CanvasPathDrawingStyles,
+ CanvasTextDrawingStyles,
+ CanvasPath {
readonly canvas: OffscreenCanvas;
}
declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D;
- new (): OffscreenCanvasRenderingContext2D;
+ new(): OffscreenCanvasRenderingContext2D;
};
interface OffscreenCanvas extends EventTarget {
@@ -506,7 +515,7 @@ interface OffscreenCanvas extends EventTarget {
}
declare const OffscreenCanvas: {
prototype: OffscreenCanvas;
- new (width: number, height: number): OffscreenCanvas;
+ new(width: number, height: number): OffscreenCanvas;
};
function createCanvas(size: number): OffscreenCanvas {
@@ -659,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,
@@ -685,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 {
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index 1e43476ea..d6e743147 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -38,6 +38,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
listenNetworkConnectionState,
openNewURLFromPopup: () => undefined,
triggerWalletEvent: () => undefined,
+ setAlertedIcon: () => undefined,
+ setNormalIcon : () => undefined,
getPermissionsApi: () => ({
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
index 2fc21bb56..c698066e7 100644
--- a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -45,7 +45,7 @@ export function NoBalanceHelp({
</Button>
</Alert>
</Paper>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ <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.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
index 11a888412..21373c7cd 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -77,6 +77,17 @@ function ContentByUriType({
</Button>
</div>
);
+ case TalerUriAction.AddExchange:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a add exchange action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open add exchange page</i18n.Translate>
+ </Button>
+ </div>
+ );
case TalerUriAction.DevExperiment:
case TalerUriAction.PayPull:
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/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index 90037819f..452cc578e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -190,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/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
index d59501212..94b32c157 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -14,14 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { OperationFailWithBody, OperationOk, OperationResult, TalerExchangeApi } from "@gnu-taler/taler-util";
+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 { ConfirmAddExchangeView, VerifyView } from "./views.js";
export interface Props {
currency?: string;
@@ -37,6 +37,7 @@ export type State = State.Loading
export type CheckExchangeErrors = {
"invalid-version": string;
"invalid-currency": string;
+ "not-found": void;
"already-active": void;
"invalid-protocol": void;
}
@@ -80,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 5ae0aa8f4..4a04f762a 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -14,15 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangeEntryStatus, OperationFailWithBody, OperationOk, TalerExchangeApi, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } 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 { withSafe } from "../../mui/handlers.js";
import { RecursiveState } from "../../utils/index.js";
import { CheckExchangeErrors, Props, State } from "./index.js";
-import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
function urlFromInput(str: string): URL {
let result: URL;
@@ -83,6 +83,9 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
*/
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)
+ }
if (!api.isCompatible(config.body.version)) {
return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
}
@@ -155,7 +158,7 @@ function useDebounce<T>(
const [result, setResult] = useState<T | 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(() => {
@@ -180,7 +183,7 @@ function useDebounce<T>(
setResult(undefined);
}
}, 500);
- setHandler(h);
+ setHandler(h as unknown as number);
}, [value, setHandler, onTrigger]);
}
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 aa844f175..d0e78a94e 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
@@ -23,6 +23,7 @@ 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";
@@ -48,7 +49,11 @@ 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,
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
index 489d7eb3b..f6537bc68 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -17,13 +17,18 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
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,
@@ -57,40 +62,71 @@ export function VerifyView({
{(() => {
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>
+ 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>
+ 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>
+ 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>
+ 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>
+ 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)
+ assertUnreachable(result.case);
}
}
})()}
<p>
- <Input invalid={result && result.type !== "ok"} >
+ <Input invalid={result && result.type !== "ok"}>
<label>URL</label>
<input
type="text"
@@ -98,7 +134,7 @@ export function VerifyView({
value={url.value}
onInput={(e) => {
if (url.onInput) {
- url.onInput(e.currentTarget.value)
+ url.onInput(e.currentTarget.value);
}
}}
/>
@@ -138,10 +174,7 @@ export function VerifyView({
</Button>
<Button
variant="contained"
- disabled={
- !result ||
- result.type !== "ok"
- }
+ disabled={!result || result.type !== "ok"}
onClick={onAccept}
>
<i18n.Translate>Next</i18n.Translate>
@@ -149,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>
@@ -164,8 +205,7 @@ export function VerifyView({
);
}
-
-export function ConfirmView({
+export function ConfirmAddExchangeView({
url,
onCancel,
onConfirm,
@@ -186,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/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 62a519f06..884c2eab7 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 {
@@ -83,6 +85,8 @@ 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();
@@ -153,7 +157,7 @@ export function Application(): VNode {
)}
/>
- <Route
+<Route
path={Pages.balanceHistory.pattern}
component={({ currency }: { currency?: string }) => (
<WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
@@ -174,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}>
@@ -497,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
@@ -531,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/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
index 29cf23739..683378613 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -25,6 +25,7 @@ import {
ExchangeListItem,
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";
@@ -37,7 +38,11 @@ const exchangeArs: ExchangeListItem = {
currency: "ARS",
exchangeBaseUrl: "http://",
masterPub: "123qwe123",
- scopeInfo: undefined,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://",
+ },
tosStatus: ExchangeTosStatus.Accepted,
exchangeEntryStatus: ExchangeEntryStatus.Used,
exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index c40a3a64c..8a74a20f1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -169,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;
@@ -275,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>
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index c28e4188f..482b8d698 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -49,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: {
@@ -165,7 +165,9 @@ const exampleData = {
export const SomeBalanceWithNoTransactions = tests.createExample(
TestedComponent,
{
- transactions: [],
+ transactionsByDate: {
+ "11/11/11": [],
+ },
balances: [
{
available: "TESTKUDOS:10" as AmountString,
@@ -186,7 +188,9 @@ export const SomeBalanceWithNoTransactions = tests.createExample(
);
export const OneSimpleTransaction = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -203,13 +207,14 @@ 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: [],
@@ -230,14 +235,16 @@ export const TwoTransactionsAndZeroBalance = tests.createExample(
);
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: [],
@@ -257,22 +264,24 @@ export const OneTransactionPending = tests.createExample(TestedComponent, {
});
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.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -294,79 +303,81 @@ export const SomeTransactions = tests.createExample(TestedComponent, {
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: "aborting in progress",
- },
- txState: {
- major: TransactionMajorState.Aborting,
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "aborted payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "normal payment",
+ },
},
- txState: {
- major: TransactionMajorState.Aborted,
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "pending payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborting in progress",
+ },
+ txState: {
+ major: TransactionMajorState.Aborting,
+ },
},
- txState: {
- major: TransactionMajorState.Pending,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborted payment",
+ },
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "failed payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "pending payment",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- txState: {
- major: TransactionMajorState.Failed,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "failed payment",
+ },
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
},
- },
- exampleData.refund,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -389,15 +400,17 @@ export const SomeTransactionsInDifferentStates = tests.createExample(
export const SomeTransactionsWithTwoCurrencies = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.deposit,
- ],
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.refresh,
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -431,7 +444,9 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample(
);
export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -505,7 +520,9 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -578,12 +595,14 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
);
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: [],
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 233bd8f28..f81e6db9f 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -20,7 +20,7 @@ import {
NotificationType,
ScopeType,
Transaction,
- WalletBalance
+ WalletBalance,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -45,31 +45,39 @@ 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 { 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 [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 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: balance?.scopeInfo,
+ scopeInfo: showSearch ? undefined : balance?.scopeInfo,
sort: "descending",
includeRefreshes: settings.showRefeshTransactions,
- })
- return { b, tx }
- }, [balanceIndex]);
+ search,
+ });
+ return { b, tx };
+ }, [balanceIndex, search]);
useEffect(() => {
return api.listener.onUpdateNotification(
@@ -104,14 +112,48 @@ export function HistoryPage({
/>
);
}
+
+ 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)}
+ changeBalanceIndex={(b) => setBalanceIndex(b)}
balances={state.response.b.balances}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
- transactions={state.response.tx.transactions}
+ transactionsByDate={byDate}
/>
);
}
@@ -120,15 +162,15 @@ export function HistoryView({
balances,
balanceIndex,
changeBalanceIndex,
- transactions,
+ transactionsByDate,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
- balanceIndex: number,
+ balanceIndex: number;
changeBalanceIndex: (s: number) => void;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
- transactions: Transaction[];
+ transactionsByDate: Record<string, Transaction[]>;
balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
@@ -139,20 +181,7 @@ export function HistoryView({
? Amounts.jsonifyAmount(balance.available)
: undefined;
- const datesWithTransaction: string[] = [];
- const byDate = 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[] });
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
return (
<Fragment>
@@ -163,62 +192,19 @@ export function HistoryView({
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
+ marginRight: 20,
}}
>
- <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>
<Button
tooltip="Transfer money to the wallet"
startIcon={DownloadIcon}
variant="contained"
- onClick={() => goToWalletManualWithdraw(balance.scopeInfo.currency)}
+ onClick={() =>
+ goToWalletManualWithdraw(balance.scopeInfo.currency)
+ }
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
{available && Amounts.isNonZero(available) && (
<Button
@@ -232,6 +218,125 @@ export function HistoryView({
</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 ? (
@@ -253,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/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/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/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index 34dd24cea..0d0a31a2d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -159,7 +159,7 @@ export function SettingsView({
<WarningBox>
<i18n.Translate>
The version of wallet core is not supported. (supported
- version: {WALLET_CORE_SUPPORTED_VERSION})
+ version: {WALLET_CORE_SUPPORTED_VERSION}, wallet version: {coreVersion.version})
</i18n.Translate>
</WarningBox>
)}
@@ -274,6 +274,7 @@ function AdvanceSettings(): VNode {
<Checkbox
label={label}
name={name}
+ key={name}
description={description}
enabled={settings[settingsName]}
onToggle={async () => {
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 10ca67663..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,
@@ -230,72 +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,
- i18n.str`There was an error trying to complete the transaction`,
+ 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} style={{padding: 0}}>
- <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 ? (
@@ -426,7 +430,7 @@ export function TransactionView({
transaction,
onDelete,
onAbort,
- onBack,
+ // onBack,
onResume,
onSuspend,
onRetry,
@@ -443,10 +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}
@@ -466,30 +473,32 @@ 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
- && transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
<Fragment>
<InfoBox>
- {transaction.withdrawalDetails.exchangeCreditAccountDetails.length > 1 ?
+ {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.
+ 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>}
-
+ ) : (
+ <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}
@@ -581,6 +590,7 @@ export function TransactionView({
format="dd MMMM yyyy"
/>
}
+ .
</i18n.Translate>
</td>
</tr>
@@ -649,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>
);
}
@@ -695,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" />
}
@@ -705,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` `,
}}
/>
@@ -714,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` `,
}}
/>
@@ -732,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` `,
}}
/>
@@ -1026,10 +1036,110 @@ export function TransactionView({
);
}
- if (transaction.type === TransactionType.Recoup) {
- throw Error("recoup transaction not implemented");
+ 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.Reward) {
+ if (transaction.type === TransactionType.Recoup) {
throw Error("recoup transaction not implemented");
}
assertUnreachable(transaction);
@@ -1075,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>
@@ -1255,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>
);
}
@@ -1300,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>
);
}
@@ -1345,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>
);
}
@@ -1390,31 +1385,34 @@ 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,
@@ -1424,12 +1422,6 @@ export function WithdrawDetails({
}): 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;
-
return (
<PurchaseDetailsTable>
{conversion ? (
@@ -1443,7 +1435,7 @@ export function WithdrawDetails({
</td>
</tr>
{conversion.fraction === amount.value.fraction &&
- conversion.value === amount.value.value ? undefined : (
+ conversion.value === amount.value.value ? undefined : (
<tr>
<td>
<i18n.Translate>Converted</i18n.Translate>
@@ -1465,28 +1457,30 @@ export function WithdrawDetails({
</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>
);
}
@@ -1494,27 +1488,16 @@ export function WithdrawDetails({
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>
@@ -1526,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}>
@@ -1634,11 +1620,6 @@ export function PurchaseDetails({
</td>
</tr>
)} */}
- <tr>
- <td>
- <ShowFullContractTermPopup proposalId={proposalId} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1658,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>
);
}
@@ -1740,7 +1723,7 @@ function TrackingDepositDetails({
</tr>
{wireTransfers.map((wire) => (
- <tr>
+ <tr key={wire.id}>
<td>{wire.id}</td>
<td>
<Amount value={wire.amount} />
@@ -1750,6 +1733,7 @@ function TrackingDepositDetails({
</PurchaseDetailsTable>
);
}
+
function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
@@ -1765,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>
);
}
@@ -1976,8 +1962,9 @@ function ShowWithdrawalDetailForBankIntegrated({
if (
transaction.txState.major !== TransactionMajorState.Pending ||
transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
- )
+ ) {
return <Fragment />;
+ }
const raw = Amounts.parseOrThrow(transaction.amountRaw);
return (
<Fragment>
@@ -1989,7 +1976,7 @@ function ShowWithdrawalDetailForBankIntegrated({
setShowDetails(!showDetails);
}}
>
- show details
+ Show details.
</a>
</EnabledBySettings>
@@ -2003,7 +1990,7 @@ function ShowWithdrawalDetailForBankIntegrated({
/>
)}
{!transaction.withdrawalDetails.confirmed &&
- transaction.withdrawalDetails.bankConfirmationUrl ? (
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
<InfoBox>
<div style={{ display: "block" }}>
<i18n.Translate>
@@ -2026,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/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 495f015ff..4394a982f 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -31,8 +31,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
- WalletDiagnostics,
- WalletNotification,
+ WalletNotification
} from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
@@ -47,6 +46,7 @@ import {
MessageFromFrontendWallet,
} from "./platform/api.js";
import { platform } from "./platform/foreground.js";
+import { WalletActivityTrack } from "./wxBackend.js";
/**
*
@@ -55,7 +55,7 @@ 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;
@@ -75,8 +75,10 @@ export interface BackgroundOperations {
response: void;
};
getNotifications: {
- request: void;
- response: WalletEvent[];
+ request: {
+ filter: string;
+ };
+ response: WalletActivityTrack[];
};
clearNotifications: {
request: void;
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 44e4c0d48..5fa255f5d 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -27,11 +27,14 @@ import {
AbsoluteTime,
LogLevel,
Logger,
+ NotificationType,
OpenedPromise,
SetTimeoutTimerAPI,
TalerError,
TalerErrorCode,
TalerErrorDetail,
+ TransactionMinorState,
+ WalletNotification,
getErrorDetailFromException,
makeErrorDetail,
openPromise,
@@ -43,17 +46,18 @@ import {
DbAccess,
SynchronousCryptoWorkerFactoryPlain,
Wallet,
+ WalletApiOperation,
WalletOperations,
WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
} from "@gnu-taler/taler-wallet-core";
+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, WalletEvent } from "./wxApi.js";
-import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
+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,16 +90,163 @@ 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 notifications: WalletEvent[] = []
-async function getNotifications(): Promise<WalletEvent[]> {
- return notifications
+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 clearNotifications(): Promise<void> {
- notifications.splice(0, notifications.length)
+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;
@@ -229,8 +375,10 @@ async function dispatch<
case "wallet": {
const w = currentWallet;
if (!w) {
- const lastError: TalerErrorDetail = walletInit.lastError instanceof TalerError ?
- walletInit.lastError.errorDetail : undefined
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
return {
type: "error",
@@ -239,16 +387,22 @@ async function dispatch<
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
{ lastError },
- `wallet core not available${!lastError ? "" : `,last error: ${lastError.hint}`}`,
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
),
};
}
//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);
+ 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
+ resp.id = req.id;
+ return resp;
}
}
@@ -267,7 +421,7 @@ async function dispatch<
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
@@ -305,11 +459,12 @@ async function reinitWallet(): Promise<void> {
config: {
testing: {
emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
},
features: {
allowHttp: settings.walletAllowHttp,
},
- }
+ },
});
} catch (e) {
logger.error("could not initialize wallet", e);
@@ -318,28 +473,23 @@ async function reinitWallet(): Promise<void> {
}
wallet.addNotificationListener((message) => {
if (settings.showWalletActivity) {
- notifications.push({
- notification: message,
- when: AbsoluteTime.now()
- })
+ addNewWalletActivityNotification(activity, message);
}
+ processWalletNotification(message);
+
platform.sendMessageToAllChannels({
type: "wallet",
notification: message,
});
});
- platform.keepAlive(() => {
- return wallet.runTaskLoop().catch((e) => {
- logger.error("error during wallet task loop", e);
- });
- });
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
+ updateIconBasedOnBalance();
return walletInit.resolve();
}
@@ -377,3 +527,46 @@ export async function wxMain(): Promise<void> {
console.error(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;
+ }
+ }
+
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
+ }
+ }
+}
+
+/**
+ * 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();
+ }
+}