diff options
Diffstat (limited to 'packages/taler-wallet-webextension')
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> + + <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> + + <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> + + <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 " %1$s"" +msgstr "" +"Произошла ошибка при загрузке сведений о поставщике для " %1$s"" + +#: src/wallet/ProviderDetailPage.tsx:108 +#, c-format +msgid "There is not known provider with url "%1$s"." +msgstr "Нет провайдера с url "%1$s"." + +#: 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 %1$s" +msgstr "Отправить %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 'Add Recipient' 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 %1$s" +msgstr "Отсканируйте QR код или %1$s" + +#: src/cta/Payment/views.tsx:346 +#, c-format +msgid "Pay %1$s" +msgstr "Заплатить %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'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 "%1$s" 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 %1$s" +msgstr "Принять %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 %1$s" +msgstr "Получить %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'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'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 %1$s" +msgstr "Вывести %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'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 %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'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'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't match the expected currency {expectedCurrency}.</i18n.Translate> - </WarningBox> + return ( + <WarningBox> + <i18n.Translate> + This exchange currency "{result.body}" doesn'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(); + } +} |