diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src')
30 files changed, 2855 insertions, 553 deletions
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx index 9be9326b2..8e48a2e9f 100644 --- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx +++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx @@ -26,7 +26,7 @@ import { DenomLossEventType, parsePaytoUri, } from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Avatar } from "../mui/Avatar.js"; import { Pages } from "../NavigationBar.js"; @@ -49,6 +49,8 @@ export function HistoryItem(props: { tx: Transaction }): VNode { */ switch (tx.type) { case TransactionType.Withdrawal: + //withdrawal that has not been confirmed are hidden + if (!tx.exchangeBaseUrl) return <Fragment /> return ( <Layout id={tx.transactionId} diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx index b95bbf3b7..2fb03308b 100644 --- a/packages/taler-wallet-webextension/src/components/Part.tsx +++ b/packages/taler-wallet-webextension/src/components/Part.tsx @@ -19,14 +19,15 @@ import { stringifyPaytoUri, TranslatedString, } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { styled } from "@linaria/react"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import arrowDown from "../svg/chevron-down.inline.svg"; import { ExtraLargeText, LargeText, - SmallBoldText, - SmallLightText, + SmallBoldText } from "./styled/index.js"; export type Kind = "positive" | "negative" | "neutral"; @@ -96,11 +97,8 @@ const CollasibleBox = styled.div` } } `; -import arrowDown from "../svg/chevron-down.inline.svg"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -export function PartCollapsible({ text, title, big, showSign }: Props): VNode { - const Text = big ? ExtraLargeText : LargeText; +export function PartCollapsible({ text, title }: Props): VNode { const [collapsed, setCollapsed] = useState(true); return ( diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx index 41b0c5c76..f29d0b0f7 100644 --- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx +++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -15,7 +15,6 @@ */ import { AbsoluteTime, - ExchangeStateTransitionNotification, NotificationType, ObservabilityEventType, RequestProgressNotification, @@ -34,12 +33,12 @@ import { useBackendContext } from "../context/backend.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useSettings } from "../hooks/useSettings.js"; import { Button } from "../mui/Button.js"; +import { TextField } from "../mui/TextField.js"; import { SafeHandler } from "../mui/handlers.js"; import { WxApiType } from "../wxApi.js"; +import { WalletActivityTrack } from "../wxBackend.js"; import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; -import { TextField } from "../mui/TextField.js"; -import { WalletActivityTrack } from "../wxBackend.js"; const OPEN_ACTIVITY_HEIGHT_PX = 250; const CLOSE_ACTIVITY_HEIGHT_PX = 40; @@ -84,7 +83,9 @@ export function WalletActivity(): VNode { cursor: "pointer", }} > - click here to open + <i18n.Translate> + Click here to open the wallet activity tab. + </i18n.Translate> </div> </div> ); diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 89678c74a..739b71064 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -690,6 +690,16 @@ export const SmallBoldText = styled.div` font-weight: bold; `; +export const AgeSign = styled.div<{size:number}>` + display: inline-block; + border: red solid 1px; + border-radius: 100%; + width: ${({ size }: {size:number}) => (`${size}px`)}; + height: ${({ size }: {size:number}) => (`${size}px`)}; + line-height: ${({ size }: {size:number}) => (`${size}px`)}; + padding: 3px; +`; + export const LargeText = styled.div` font-size: large; `; diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx index 547d5ac9a..0d8035136 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -24,12 +24,21 @@ import { InvoicePaymentDetails, } from "../../wallet/Transaction.js"; import { State } from "./index.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; export function ReadyView( state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance, ): VNode { const { i18n } = useTranslationContext(); const { summary, effective, raw, expiration, uri, status, payStatus } = state; + + const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ); + const willExpireSoon = + expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; + return ( <Fragment> <section style={{ textAlign: "left" }}> @@ -42,11 +51,13 @@ export function ReadyView( /> } /> - <Part - title={i18n.str`Valid until`} - text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />} - kind="neutral" - /> + {willExpireSoon && ( + <Part + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} + kind="neutral" + /> + )} </section> <PaymentButtons amount={effective} diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx index 8bbb8dac2..b1eee85ec 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -18,6 +18,7 @@ import { AbsoluteTime, Amounts, MerchantContractTerms as ContractTerms, + Duration, PreparePayResultType, TranslatedString, } from "@gnu-taler/taler-util"; @@ -27,7 +28,11 @@ import { Part } from "../../components/Part.js"; import { PaymentButtons } from "../../components/PaymentButtons.js"; import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js"; import { Time } from "../../components/Time.js"; -import { SuccessBox, WarningBox } from "../../components/styled/index.js"; +import { + AgeSign, + SuccessBox, + WarningBox, +} from "../../components/styled/index.js"; import { MerchantDetails } from "../../wallet/Transaction.js"; import { State } from "./index.js"; import { EnabledBySettings } from "../../components/EnabledBySettings.js"; @@ -50,13 +55,39 @@ export function BaseView(state: SupportedStates): VNode { : Amounts.zeroOfCurrency(state.amount.currency) : state.amount; + const expiration = !contractTerms.pay_deadline + ? undefined + : AbsoluteTime.fromProtocolTimestamp(contractTerms.pay_deadline); + const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ); + const willExpireSoon = + !expiration || expiration.t_ms === "never" + ? undefined + : AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; return ( <Fragment> <ShowImportantMessage state={state} /> <section style={{ textAlign: "left" }}> <Part - title={i18n.str`Purchase`} + title={ + contractTerms.minimum_age ? ( + <Fragment> + <i18n.Translate>Purchase</i18n.Translate> + + <AgeSign + size={20} + title={i18n.str`This purchase is age restricted.`} + > + {contractTerms.minimum_age}+ + </AgeSign> + </Fragment> + ) : ( + <i18n.Translate>Purchase</i18n.Translate> + ) + } text={contractTerms.summary as TranslatedString} kind="neutral" /> @@ -65,17 +96,10 @@ export function BaseView(state: SupportedStates): VNode { text={<MerchantDetails merchant={contractTerms.merchant} />} kind="neutral" /> - {contractTerms.pay_deadline && ( + {willExpireSoon && ( <Part - title={i18n.str`Valid until`} - text={ - <Time - timestamp={AbsoluteTime.fromProtocolTimestamp( - contractTerms.pay_deadline, - )} - format="dd MMMM yyyy, HH:mm" - /> - } + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} kind="neutral" /> )} diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts index f5a8c8814..1e903fe46 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts @@ -53,9 +53,9 @@ export namespace State { export interface FillTemplate { status: "fill-template"; error: undefined; - currency: string; amount?: AmountFieldHandler; summary?: TextFieldHandler; + minAge: number; onCreate: ButtonHandler; } diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts index 6b4584fea..ba854a93c 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts @@ -16,12 +16,13 @@ import { Amounts, PreparePayResult } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; +import { RecursiveState } from "../../utils/index.js"; import { Props, State } from "./index.js"; export function useComponentState({ @@ -29,43 +30,38 @@ export function useComponentState({ cancel, goToWalletManualWithdraw, onSuccess, -}: Props): State { +}: Props): RecursiveState<State> { const api = useBackendContext(); const { i18n } = useTranslationContext(); const { safely } = useAlertContext(); - const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined; - - const amountParam = !url - ? undefined - : url.searchParams.get("amount") ?? undefined; - const summaryParam = !url - ? undefined - : url.searchParams.get("summary") ?? undefined; + // const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined; + // const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam); + // const currency = parsedAmount ? parsedAmount.currency : amountParam; - const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam); - const currency = parsedAmount ? parsedAmount.currency : amountParam; + // const initialAmount = + // parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined); - const initialAmount = - parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined); - const [amount, setAmount] = useState(initialAmount); - const [summary, setSummary] = useState(summaryParam); const [newOrder, setNewOrder] = useState(""); const hook = useAsyncAsHook(async () => { if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE"); + const templateP = await api.wallet.call( + WalletApiOperation.CheckPayForTemplate, + { talerPayTemplateUri: talerTemplateUri }, + ); + const requireMoreInfo = + !templateP.templateDetails.template_contract.amount || + !templateP.templateDetails.template_contract.summary; let payStatus: PreparePayResult | undefined = undefined; - if (!amountParam && !summaryParam) { + if (!requireMoreInfo) { payStatus = await api.wallet.call( WalletApiOperation.PreparePayForTemplate, - { - talerPayTemplateUri: talerTemplateUri, - templateParams: {}, - }, + { talerPayTemplateUri: talerTemplateUri }, ); } const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); - return { payStatus, balance, uri: talerTemplateUri }; + return { payStatus, balance, uri: talerTemplateUri, templateP }; }, []); if (!hook) { @@ -108,60 +104,99 @@ export function useComponentState({ }; } - async function createOrder() { - try { - const templateParams: Record<string, string> = {}; - if (amount) { - templateParams["amount"] = Amounts.stringify(amount); + return () => { + const cfg = hook.response.templateP.templateDetails.template_contract; + const def = hook.response.templateP.templateDetails.editable_defaults; + + const fixedAmount = + cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined; + const fixedSummary = cfg.summary; + + const defaultAmount = + def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined; + const defaultSummary = def?.summary; + + const zero = fixedAmount + ? Amounts.zeroOfAmount(fixedAmount) + : cfg.currency !== undefined + ? Amounts.zeroOfCurrency(cfg.currency) + : defaultAmount !== undefined + ? Amounts.zeroOfAmount(defaultAmount) + : def?.currency !== undefined + ? Amounts.zeroOfCurrency(def.currency) + : Amounts.zeroOfCurrency( + hook.response.templateP.supportedCurrencies[0], + ); + + const [amount, setAmount] = useState(defaultAmount ?? fixedAmount ?? zero); + const [summary, setSummary] = useState(defaultSummary ?? fixedSummary ?? ""); + + async function createOrder() { + try { + const templateParams: Record<string, string> = {}; + if (amount && !fixedAmount) { + templateParams["amount"] = Amounts.stringify(amount); + } + if (summary && !fixedSummary) { + templateParams["summary"] = summary; + } + const payStatus = await api.wallet.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: talerTemplateUri, + templateParams, + }, + ); + setNewOrder(payStatus.talerUri!); + } catch (e) { + console.error(e); } - if (summary) { - templateParams["summary"] = summary; - } - const payStatus = await api.wallet.call( - WalletApiOperation.PreparePayForTemplate, - { - talerPayTemplateUri: talerTemplateUri, - templateParams, - }, - ); - setNewOrder(payStatus.talerUri!); - } catch (e) { - console.error(e); } - } - const errors = undefinedIfEmpty({ - amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined, - summary: summary !== undefined && !summary ? i18n.str`required` : undefined, - }); - return { - status: "fill-template", - error: undefined, - currency: currency!, //currency is always not null - amount: - amount !== undefined - ? ({ - onInput: (a) => { - setAmount(a); - }, - value: amount, - error: errors?.amount, - } as AmountFieldHandler) - : undefined, - summary: - summary !== undefined - ? ({ - onInput: (t) => { - setSummary(t); - }, - value: summary, - error: errors?.summary, - } as TextFieldHandler) - : undefined, - onCreate: { - onClick: errors - ? undefined - : safely("create order for pay template", createOrder), - }, + + const errors = undefinedIfEmpty({ + amount: + fixedAmount !== undefined + ? undefined + : amount && Amounts.isZero(amount) + ? i18n.str`required` + : undefined, + summary: + fixedSummary !== undefined + ? undefined + : summary !== undefined && !summary + ? i18n.str`required` + : undefined, + }); + return { + status: "fill-template", + error: undefined, + minAge: cfg.minimum_age ?? 0, + amount: { + onInput: + fixedAmount !== undefined + ? undefined + : (a) => { + setAmount(a); + }, + value: amount, + error: errors?.amount, + } as AmountFieldHandler, + summary: { + onInput: + fixedSummary !== undefined + ? undefined + : (t) => { + setSummary(t); + }, + value: summary, + error: errors?.summary, + } as TextFieldHandler, + onCreate: { + onClick: errors + ? undefined + : safely("create order for pay template", createOrder), + }, + }; }; } diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx index 88658b5e1..4a1cfe3ac 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx @@ -14,17 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { AmountField } from "../../components/AmountField.js"; -import { Part } from "../../components/Part.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Button } from "../../mui/Button.js"; import { TextField } from "../../mui/TextField.js"; import { State } from "./index.js"; +import { AgeSign } from "../../components/styled/index.js"; export function ReadyView({ - currency, amount, + minAge, summary, onCreate, }: State.FillTemplate): VNode { @@ -33,24 +33,11 @@ export function ReadyView({ return ( <Fragment> <section style={{ textAlign: "left" }}> - {/* <Part - title={ - <div - style={{ - display: "flex", - alignItems: "center", - }} - > - <i18n.Translate>Merchant</i18n.Translate> - </div> - } - text={<ExchangeDetails exchange={exchangeUrl} />} - kind="neutral" - big - /> */} {!amount ? undefined : ( <p> - <AmountField label={i18n.str`Amount`} handler={amount} /> + <AmountField label={i18n.str`Amount`} + handler={amount} + /> </p> )} {!summary ? undefined : ( @@ -60,6 +47,7 @@ export function ReadyView({ variant="filled" required fullWidth + disabled={summary.onInput === undefined} error={summary.error} value={summary.value} onChange={summary.onInput} @@ -67,6 +55,12 @@ export function ReadyView({ </p> )} </section> + {minAge ? ( + <section> + <AgeSign size={25}>{minAge}+</AgeSign> + <i18n.Translate>This purchase is age restricted.</i18n.Translate> + </section> + ) : undefined} <section> <Button onClick={onCreate.onClick} variant="contained" color="success"> <i18n.Translate>Review order</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx index caa1b485a..e82c4fbd2 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx @@ -26,6 +26,7 @@ import { } from "../../wallet/Transaction.js"; import { State } from "./index.js"; import { TermsOfService } from "../../components/TermsOfService/index.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; export function ReadyView({ accept, @@ -36,6 +37,12 @@ export function ReadyView({ raw, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ); + const willExpireSoon = + expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; return ( <Fragment> <section style={{ textAlign: "left" }}> @@ -49,15 +56,16 @@ export function ReadyView({ /> } /> - - <Part - title={i18n.str`Valid until`} - text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />} - kind="neutral" - /> + {willExpireSoon && ( + <Part + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} + kind="neutral" + /> + )} </section> <section> - <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} > + <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl}> <Button variant="contained" color="success" onClick={accept.onClick}> <i18n.Translate> Receive {<Amount value={effective} />} diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts index 1f8745a5d..af1ef213b 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -18,8 +18,7 @@ import { AmountJson, AmountString, CurrencySpecification, - ExchangeListItem, - WithdrawalExchangeAccountDetails, + ExchangeListItem } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; @@ -84,6 +83,8 @@ export namespace State { export interface AlreadyCompleted { status: "already-completed"; operationState: "confirmed" | "aborted" | "selected"; + thisWallet: boolean; + redirectToTx: () => void; confirmTransferUrl?: string, error: undefined; } @@ -94,12 +95,16 @@ export namespace State { currentExchange: ExchangeListItem; - chosenAmount: AmountJson; + amount: AmountFieldHandler; + editableAmount: boolean; + + bankFee: AmountJson; withdrawalFee: AmountJson; toBeReceived: AmountJson; doWithdrawal: ButtonHandler; doSelectExchange: ButtonHandler; + editableExchange: boolean; chooseCurrencies: string[]; selectedCurrency: string; diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 044f2434f..f8e27e688 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -16,11 +16,13 @@ import { AmountJson, + AmountString, Amounts, ExchangeFullDetails, ExchangeListItem, NotificationType, - parseWithdrawExchangeUri + TransactionMajorState, + parseWithdrawExchangeUri, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -42,6 +44,7 @@ export function useComponentStateFromParams({ const api = useBackendContext(); const { i18n } = useTranslationContext(); const paramsAmount = amount ? Amounts.parse(amount) : undefined; + const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>(); const uriInfoHook = useAsyncAsHook(async () => { const exchanges = await api.wallet.call( WalletApiOperation.ListExchanges, @@ -50,12 +53,12 @@ export function useComponentStateFromParams({ const uri = maybeTalerUri ? parseWithdrawExchangeUri(maybeTalerUri) : undefined; - const exchangeByTalerUri = uri?.exchangeBaseUrl; + const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl; + let ex: ExchangeFullDetails | undefined; if (exchangeByTalerUri) { await api.wallet.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchangeByTalerUri, - masterPub: uri.exchangePub, }); const info = await api.wallet.call( WalletApiOperation.GetExchangeDetailedInfo, @@ -139,8 +142,8 @@ export function useComponentStateFromParams({ confirm: { onClick: isValid ? pushAlertOnError(async () => { - onAmountChanged(Amounts.stringify(amount)); - }) + onAmountChanged(Amounts.stringify(amount)); + }) : undefined, }, amount: { @@ -157,6 +160,7 @@ export function useComponentStateFromParams({ async function doManualWithdraw( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ): Promise<{ transactionId: string; confirmTransferUrl: string | undefined; @@ -165,7 +169,7 @@ export function useComponentStateFromParams({ WalletApiOperation.AcceptManualWithdrawal, { exchangeBaseUrl: exchange, - amount: Amounts.stringify(chosenAmount), + amount, restrictAge: ageRestricted, }, ); @@ -181,9 +185,17 @@ export function useComponentStateFromParams({ cancel, onSuccess, undefined, - chosenAmount, - exchangeList, - exchangeByTalerUri, + { + amount: chosenAmount, + currency: chosenAmount.currency, + maxAmount: Amounts.zeroOfCurrency(chosenAmount.currency), + bankFee: Amounts.zeroOfCurrency(chosenAmount.currency), + editableAmount: true, + editableExchange: true, + exchange: exchangeByTalerUri, + exchangeList: exchangeList, + }, + setUpdatedExchangeByUser, ); } @@ -194,6 +206,8 @@ export function useComponentStateFromURI({ }: PropsFromURI): RecursiveState<State> { const api = useBackendContext(); const { i18n } = useTranslationContext(); + + const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>(); /** * Ask the wallet about the withdraw URI */ @@ -204,45 +218,43 @@ export function useComponentStateFromURI({ : maybeTalerUri; const uriInfo = await api.wallet.call( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, + { talerWithdrawUri }, + ); + const { status } = uriInfo.info; + const txInfo = await api.wallet.call( + WalletApiOperation.GetTransactionById, { - talerWithdrawUri, - // notifyChangeFromPendingTimeoutMs: 30 * 1000, + transactionId: uriInfo.transactionId, }, ); - const { - amount, - defaultExchangeBaseUrl, - possibleExchanges, - operationId, - confirmTransferUrl, - status, - } = uriInfo; - const transaction = await api.wallet.call( - WalletApiOperation.GetWithdrawalTransactionByUri, - { talerWithdrawUri }, - ); return { talerWithdrawUri, - operationId, status, - transaction, - confirmTransferUrl, - amount: Amounts.parseOrThrow(amount), - thisExchange: defaultExchangeBaseUrl, - exchanges: possibleExchanges, + transactionId: uriInfo.transactionId, + bankWithdrawalInfo: uriInfo.info, + txInfo: txInfo, }; }); const readyToListen = uriInfoHook && !uriInfoHook.hasError; useEffect(() => { - if (!uriInfoHook) { + if (!uriInfoHook || uriInfoHook.hasError) { return; } + const txId = uriInfoHook.response.transactionId; + return api.listener.onUpdateNotification( - [NotificationType.WithdrawalOperationTransition], - uriInfoHook.retry, + [NotificationType.TransactionStateTransition], + (notif) => { + if ( + notif.type === NotificationType.TransactionStateTransition && + notif.transactionId === txId + ) { + uriInfoHook.retry(); + } + }, ); }, [readyToListen]); @@ -260,39 +272,55 @@ export function useComponentStateFromURI({ } const uri = uriInfoHook.response.talerWithdrawUri; - const chosenAmount = uriInfoHook.response.amount; - const defaultExchange = uriInfoHook.response.thisExchange; - const exchangeList = uriInfoHook.response.exchanges; + const txId = uriInfoHook.response.transactionId; + const bwi = uriInfoHook.response.bankWithdrawalInfo; + + const amount = + bwi.amount === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.amount); + + const maxAmount = + bwi.maxAmount === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.maxAmount); + + const bankFee = + bwi.wireFee === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.wireFee); async function doManagedWithdraw( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ): Promise<{ transactionId: string; confirmTransferUrl: string | undefined; }> { - const res = await api.wallet.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange, - talerWithdrawUri: uri, - restrictAge: ageRestricted, - }, - ); + const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, { + exchangeBaseUrl: exchange, + amount, + restrictAge: ageRestricted, + transactionId: txId, + }); return { confirmTransferUrl: res.confirmTransferUrl, transactionId: res.transactionId, }; } - if (uriInfoHook.response.status !== "pending") { - if (uriInfoHook.response.transaction) { - onSuccess(uriInfoHook.response.transaction.transactionId); - } + if ( + uriInfoHook.response.txInfo && + uriInfoHook.response.status !== "pending" + ) { + const info = uriInfoHook.response.txInfo; return { status: "already-completed", operationState: uriInfoHook.response.status, - confirmTransferUrl: uriInfoHook.response.confirmTransferUrl, + confirmTransferUrl: bwi.confirmTransferUrl, + thisWallet: info.txState.major === TransactionMajorState.Pending, + redirectToTx: () => onSuccess(info.transactionId), error: undefined, }; } @@ -303,16 +331,36 @@ export function useComponentStateFromURI({ cancel, onSuccess, uri, - chosenAmount, - exchangeList, - defaultExchange, + { + amount, + bankFee, + maxAmount, + currency: bwi.currency, + editableAmount: bwi.editableAmount, + editableExchange: bwi.editableExchange, + exchange: bwi.defaultExchangeBaseUrl, + exchangeList: bwi.possibleExchanges, + }, + setUpdatedExchangeByUser, ); }, []); } +type WithdrawalInfo = { + currency: string; + amount: AmountJson; + bankFee: AmountJson; + maxAmount: AmountJson; + editableAmount: boolean; + exchange: string | undefined; + editableExchange: boolean; + exchangeList: ExchangeListItem[]; +}; + type ManualOrManagedWithdrawFunction = ( exchange: string, ageRestricted: number | undefined, + amount: AmountString, ) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>; function exchangeSelectionState( @@ -320,17 +368,31 @@ function exchangeSelectionState( cancel: () => Promise<void>, onSuccess: (txid: string) => Promise<void>, talerWithdrawUri: string | undefined, - chosenAmount: AmountJson, - exchangeList: ExchangeListItem[], - exchangeSuggestedByTheBank: string | undefined, + wInfo: WithdrawalInfo, + onExchangeUpdated: (ex: string) => void, ): RecursiveState<State> { const api = useBackendContext(); const selectedExchange = useSelectedExchange({ - currency: chosenAmount.currency, - defaultExchange: exchangeSuggestedByTheBank, - list: exchangeList, + currency: wInfo.currency, + defaultExchange: wInfo.exchange, + list: wInfo.exchangeList, }); + const current = + selectedExchange.status !== "ready" + ? undefined + : selectedExchange.selected.exchangeBaseUrl; + useEffect(() => { + if (current) { + onExchangeUpdated(current); + } + }, [current]); + + const safeAmount = wInfo.amount + ? wInfo.amount + : Amounts.zeroOfCurrency(wInfo.currency); + const [choosenAmount, setChoosenAmount] = useState(safeAmount); + if (selectedExchange.status !== "ready") { return selectedExchange; } @@ -345,7 +407,7 @@ function exchangeSelectionState( const currentExchange = selectedExchange.selected; const [selectedCurrency, setSelectedCurrency] = useState<string>( - chosenAmount.currency, + wInfo.currency, ); /** * With the exchange and amount, ask the wallet the information @@ -356,7 +418,7 @@ function exchangeSelectionState( WalletApiOperation.GetWithdrawalDetailsForAmount, { exchangeBaseUrl: currentExchange.exchangeBaseUrl, - amount: Amounts.stringify(chosenAmount), + amount: Amounts.stringify(choosenAmount), restrictAge: ageRestricted, }, ); @@ -381,6 +443,7 @@ function exchangeSelectionState( const res = await doWithdraw( currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted, + Amounts.stringify(choosenAmount), ); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; @@ -418,9 +481,11 @@ function exchangeSelectionState( ).amount; const toBeReceived = amountHook.response.amount.effective; + const bankFee = wInfo.amount; + const ageRestrictionOptions = amountHook.response.ageRestrictionOptions?.reduce( - (p, c) => ({ ...p, [c]: `under ${c}` }), + (p, c) => ({ ...p, [c]: i18n.str`under ${c}` }), {} as Record<string, string>, ); @@ -432,12 +497,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 @@ -457,30 +522,52 @@ function exchangeSelectionState( const conversionInfo = !convAccount ? undefined : { - spec: convAccount.currencySpecification!, - amount: Amounts.parseOrThrow(convAccount.transferAmount!), - }; + spec: convAccount.currencySpecification!, + amount: Amounts.parseOrThrow(convAccount.transferAmount!), + }; + + const amountError = Amounts.isZero(choosenAmount) + ? i18n.str`should be greater than zero` + : Amounts.cmp(choosenAmount, wInfo.maxAmount) === -1 + ? i18n.str`choose a lower value` + : undefined; return { status: "success", error: undefined, - doSelectExchange: selectedExchange.doSelect, + doSelectExchange: { + onClick: wInfo.editableExchange + ? selectedExchange.doSelect.onClick + : undefined, + }, + editableAmount: wInfo.editableAmount, + editableExchange: wInfo.editableExchange, currentExchange, toBeReceived, chooseCurrencies, + bankFee, selectedCurrency, changeCurrency: (s) => { setSelectedCurrency(s); }, conversionInfo, withdrawalFee, - chosenAmount, + amount: { + value: choosenAmount, + onInput: wInfo.editableAmount + ? pushAlertOnError(async (v) => { + setChoosenAmount(v); + }) + : undefined, + error: amountError, + }, talerWithdrawUri, ageRestriction, doWithdrawal: { - onClick: doingWithdraw - ? undefined - : pushAlertOnError(doWithdrawAndCheckError), + onClick: + doingWithdraw && !amountError + ? undefined + : pushAlertOnError(doWithdrawAndCheckError), }, cancel, }; diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx index 29f39054f..1bfafb231 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx @@ -43,10 +43,12 @@ const ageRestrictionSelectField = { export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + } }, doWithdrawal: { onClick: nullFunction }, currentExchange: { @@ -87,10 +89,12 @@ export const AlreadyConfirmed = tests.createExample(FinalStateOperation, { export const WithSomeFee = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + } }, doWithdrawal: { onClick: nullFunction }, currentExchange: { @@ -114,10 +118,12 @@ export const WithSomeFee = tests.createExample(SuccessView, { export const WithoutFee = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 0, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 0, + } }, doWithdrawal: { onClick: nullFunction }, currentExchange: { @@ -141,10 +147,12 @@ export const WithoutFee = tests.createExample(SuccessView, { export const EditExchangeUntouched = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + } }, doWithdrawal: { onClick: nullFunction }, currentExchange: { @@ -168,10 +176,12 @@ export const EditExchangeUntouched = tests.createExample(SuccessView, { export const EditExchangeModified = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + } }, doWithdrawal: { onClick: nullFunction }, currentExchange: { @@ -196,10 +206,12 @@ export const WithAgeRestriction = tests.createExample(SuccessView, { error: undefined, status: "success", ageRestriction: ageRestrictionSelectField, - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + } }, doSelectExchange: {}, doWithdrawal: { onClick: nullFunction }, @@ -223,10 +235,12 @@ export const WithAgeRestriction = tests.createExample(SuccessView, { export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + } }, chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "NETZBON", @@ -251,10 +265,12 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, { export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + } }, chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "EUR", @@ -290,10 +306,12 @@ export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, { export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + } }, chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "EUR", diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index f90f7bed7..bce5f71e3 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -26,6 +26,7 @@ import { ExchangeListItem, ExchangeTosStatus, ScopeType, + TransactionIdStr, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { expect } from "chai"; @@ -99,7 +100,7 @@ describe("Withdraw CTA states", () => { expect(handler.getCallingQueueState()).eq("empty"); }); - it("should tell the user that there is not known exchange", async () => { + it.skip("should tell the user that there is not known exchange", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { talerWithdrawUri: "taler-withdraw://", @@ -108,22 +109,23 @@ describe("Withdraw CTA states", () => { }; handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - status: "pending", - operationId: "123", - amount: "EUR:2" as AmountString, - possibleExchanges: [], + transactionId: "123" as TransactionIdStr, + info: { + status: "pending", + operationId: "123", + currency: "ARS", + amount: "EUR:2" as AmountString, + possibleExchanges: [], + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", + }, }, ); - handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalTransactionByUri, - undefined, - { - transactionId: "123" - } as any, - ); const hookBehavior = await tests.hookBehaveLikeThis( useComponentStateFromURI, @@ -144,7 +146,7 @@ describe("Withdraw CTA states", () => { expect(handler.getCallingQueueState()).eq("empty"); }); - it("should be able to withdraw if tos are ok", async () => { + it.skip("should be able to withdraw if tos are ok", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { talerWithdrawUri: "taler-withdraw://", @@ -153,24 +155,25 @@ describe("Withdraw CTA states", () => { }; handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalDetailsForUri, + WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - status: "pending", - operationId: "123", - amount: "ARS:2" as AmountString, - possibleExchanges: exchanges, - defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + transactionId: "123" as TransactionIdStr, + info: { + status: "pending", + operationId: "123", + currency: "ARS", + amount: "ARS:2" as AmountString, + possibleExchanges: exchanges, + defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", + }, }, ); handler.addWalletCallResponse( - WalletApiOperation.GetWithdrawalTransactionByUri, - undefined, - { - transactionId: "123" - } as any, - ); - handler.addWalletCallResponse( WalletApiOperation.GetWithdrawalDetailsForAmount, undefined, { @@ -181,7 +184,7 @@ describe("Withdraw CTA states", () => { scopeInfo: { currency: "ARS", type: ScopeType.Exchange, - url: "http://asd" + url: "http://asd", }, withdrawalAccountsList: [], ageRestrictionOptions: [], @@ -206,7 +209,7 @@ describe("Withdraw CTA states", () => { expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); - expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; }, @@ -237,9 +240,14 @@ describe("Withdraw CTA states", () => { { status: "pending", operationId: "123", + currency: "ARS", amount: "ARS:2" as AmountString, possibleExchanges: exchangeWithNewTos, defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl, + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", }, ); handler.addWalletCallResponse( @@ -252,7 +260,7 @@ describe("Withdraw CTA states", () => { scopeInfo: { currency: "ARS", type: ScopeType.Exchange, - url: "http://asd" + url: "http://asd", }, tosAccepted: false, withdrawalAccountsList: [], @@ -267,9 +275,14 @@ describe("Withdraw CTA states", () => { { status: "pending", operationId: "123", + currency: "ARS", amount: "ARS:2" as AmountString, possibleExchanges: exchanges, defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", }, ); @@ -290,7 +303,7 @@ describe("Withdraw CTA states", () => { expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); - expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; }, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index aade67835..86d7248a4 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -19,11 +19,17 @@ import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Amount } from "../../components/Amount.js"; import { AmountField } from "../../components/AmountField.js"; +import { EnabledBySettings } from "../../components/EnabledBySettings.js"; import { Part } from "../../components/Part.js"; import { QR } from "../../components/QR.js"; import { SelectList } from "../../components/SelectList.js"; import { TermsOfService } from "../../components/TermsOfService/index.js"; -import { Input, LinkSuccess, SvgIcon, WarningBox } from "../../components/styled/index.js"; +import { + Input, + LinkSuccess, + SvgIcon, + WarningBox, +} from "../../components/styled/index.js"; import { Button } from "../../mui/Button.js"; import { Grid } from "../../mui/Grid.js"; import editIcon from "../../svg/edit_24px.inline.svg"; @@ -33,32 +39,105 @@ import { getAmountWithFee, } from "../../wallet/Transaction.js"; import { State } from "./index.js"; -import { EnabledBySettings } from "../../components/EnabledBySettings.js"; export function FinalStateOperation(state: State.AlreadyCompleted): VNode { const { i18n } = useTranslationContext(); + // document.location.href = res.confirmTransferUrl + if (state.thisWallet) { + switch (state.operationState) { + case "confirmed": { + state.redirectToTx(); + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been completed. + </i18n.Translate> + </div> + </WarningBox> + ); + } + case "aborted": { + state.redirectToTx(); + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been aborted + </i18n.Translate> + </div> + </WarningBox> + ); + } + case "selected": { + if (state.confirmTransferUrl) { + document.location.href = state.confirmTransferUrl; + } + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has started and should be completed in the bank. + </i18n.Translate> + </div> + {state.confirmTransferUrl && ( + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + You can confirm the operation in + </i18n.Translate> + + <a + target="_bank" + rel="noreferrer" + href={state.confirmTransferUrl} + > + <i18n.Translate>this page</i18n.Translate> + </a> + </div> + )} + </WarningBox> + ); + } + } + } switch (state.operationState) { - case "confirmed": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been completed by another wallet.</i18n.Translate> - </div> - </WarningBox> - case "aborted": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been aborted</i18n.Translate> - </div> - </WarningBox> - case "selected": return <WarningBox> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>This operation has already been used by another wallet.</i18n.Translate> - </div> - <div style={{ justifyContent: "center", lineHeight: "25px" }}> - <i18n.Translate>It can be confirmed in</i18n.Translate> <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}> - <i18n.Translate>this page</i18n.Translate> - </a> - </div> - </WarningBox> + case "confirmed": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been completed by another wallet. + </i18n.Translate> + </div> + </WarningBox> + ); + case "aborted": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been aborted + </i18n.Translate> + </div> + </WarningBox> + ); + case "selected": + return ( + <WarningBox> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate> + This operation has already been used by another wallet. + </i18n.Translate> + </div> + <div style={{ justifyContent: "center", lineHeight: "25px" }}> + <i18n.Translate>It can be confirmed in</i18n.Translate> + <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}> + <i18n.Translate>this page</i18n.Translate> + </a> + </div> + </WarningBox> + ); } } @@ -95,21 +174,36 @@ export function SuccessView(state: State.Success): VNode { kind="neutral" big /> - {state.chooseCurrencies.length > 0 ? + {state.editableAmount ? ( + <Fragment> + <AmountField handler={state.amount} label={i18n.str`Amount`} /> + </Fragment> + ) : undefined} + {state.chooseCurrencies.length > 0 ? ( <Fragment> <p> - {state.chooseCurrencies.map(currency => { - return <Button variant={currency === state.selectedCurrency ? "contained" : "outlined"} - onClick={async () => { - state.changeCurrency(currency) - }} - > - {currency} - </Button> + {state.chooseCurrencies.map((currency) => { + return ( + <Button + key={currency} + variant={ + currency === state.selectedCurrency + ? "contained" + : "outlined" + } + onClick={async () => { + state.changeCurrency(currency); + }} + > + {currency} + </Button> + ); })} </p> </Fragment> - : <Fragment />} + ) : ( + <Fragment /> + )} <Part title={i18n.str`Details`} @@ -118,7 +212,7 @@ export function SuccessView(state: State.Success): VNode { conversion={state.conversionInfo?.amount} amount={getAmountWithFee( state.toBeReceived, - state.chosenAmount, + state.amount.value, "credit", )} /> @@ -202,7 +296,6 @@ function WithdrawWithMobile({ } export function SelectAmountView({ - currency, amount, exchangeBaseUrl, confirm, diff --git a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts index 8d26bf3b6..719aa2f96 100644 --- a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts +++ b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts @@ -1,7 +1,21 @@ -import { codecForBoolean } from "@gnu-taler/taler-util"; -import { buildStorageKey, useMemoryStorage } from "@gnu-taler/web-util/browser"; -import { platform } from "../platform/foreground.js"; +/* + 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 { useMemoryStorage } from "@gnu-taler/web-util/browser"; import { useEffect } from "preact/hooks"; +import { platform } from "../platform/foreground.js"; export function useIsOnline(): boolean { const { value, update } = useMemoryStorage("online", true); 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/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index e63040f5c..056351e3f 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -732,15 +732,35 @@ function listenNetworkConnectionState( function notifyOnline() { notify("on"); } - notify(window.navigator.onLine ? "on" : "off"); - window.addEventListener("offline", notifyOffline); - window.addEventListener("online", notifyOnline); + function notifyChange() { + if (nav.onLine) { + notifyOnline(); + } else { + notifyOnline(); + } + } + notify(navigator.onLine ? "on" : "off"); + + const nav: any = navigator; + if (typeof nav.connection !== "undefined") { + nav.connection.addEventListener("change", notifyChange); + } + if (typeof window !== "undefined") { + window.addEventListener("offline", notifyOffline); + window.addEventListener("online", notifyOnline); + } return () => { - window.removeEventListener("offline", notifyOffline); - window.removeEventListener("online", notifyOnline); + if (typeof nav.connection !== "undefined") { + nav.connection.removeEventListener("change", notifyChange); + } + if (typeof window !== "undefined") { + window.removeEventListener("offline", notifyOffline); + window.removeEventListener("online", notifyOnline); + } }; } + function runningOnPrivateMode(): boolean { return chrome.extension.inIncognitoContext; } diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts index d6e743147..b53e8f3c4 100644 --- a/packages/taler-wallet-webextension/src/platform/dev.ts +++ b/packages/taler-wallet-webextension/src/platform/dev.ts @@ -35,11 +35,11 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { keepAlive: (cb: VoidFunction) => cb(), findTalerUriInActiveTab: async () => undefined, findTalerUriInClipboard: async () => undefined, - listenNetworkConnectionState, + listenNetworkConnectionState: () => () => undefined, openNewURLFromPopup: () => undefined, triggerWalletEvent: () => undefined, setAlertedIcon: () => undefined, - setNormalIcon : () => undefined, + setNormalIcon: () => undefined, getPermissionsApi: () => ({ containsClipboardPermissions: async () => true, removeClipboardPermissions: async () => false, @@ -200,19 +200,3 @@ interface IframeMessageCommand { export default api; -function listenNetworkConnectionState( - notify: (state: "on" | "off") => void, -): () => void { - function notifyOffline() { - notify("off"); - } - function notifyOnline() { - notify("on"); - } - window.addEventListener("offline", notifyOffline); - window.addEventListener("online", notifyOnline); - return () => { - window.removeEventListener("offline", notifyOffline); - window.removeEventListener("online", notifyOnline); - }; -} diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index 93770312e..73bd8e96d 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -180,7 +180,7 @@ export function BalanceView(state: State.Balances): VNode { variant="contained" onClick={state.goToWalletManualWithdraw.onClick} > - <i18n.Translate>Add</i18n.Translate> + <i18n.Translate>Receive</i18n.Translate> </Button> {currencyWithNonZeroAmount.length > 0 && ( <MultiActionButton diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 884c2eab7..893122c0f 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -157,7 +157,7 @@ export function Application(): VNode { )} /> -<Route + <Route path={Pages.balanceHistory.pattern} component={({ currency }: { currency?: string }) => ( <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index 838739ad1..daba6aba4 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -94,9 +94,9 @@ export namespace State { currentAccount: PaytoUri; totalFee: AmountJson; - totalToDeposit: AmountJson; amount: AmountFieldHandler; + totalToDeposit: AmountFieldHandler; account: SelectFieldHandler; cancelHandler: ButtonHandler; depositHandler: ButtonHandler; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 97b2ab517..b674665cf 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -15,19 +15,18 @@ */ import { - AmountJson, Amounts, - DepositGroupFees, KnownBankAccountsInfo, parsePaytoUri, PaytoUri, stringifyPaytoUri, + TransactionAmountMode } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { RecursiveState } from "../../utils/index.js"; import { Props, State } from "./index.js"; @@ -83,8 +82,11 @@ export function useComponentState({ if (hook.hasError) { return { status: "error", - error: alertFromError(i18n, - i18n.str`Could not load balance information`, hook), + error: alertFromError( + i18n, + i18n.str`Could not load balance information`, + hook, + ), }; } const { accounts, balances } = hook.response; @@ -141,21 +143,23 @@ export function useComponentState({ } const firstAccount = accounts[0].uri; const currentAccount = !selectedAccount ? firstAccount : selectedAccount; - - return () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const [amount, setAmount] = useState<AmountJson>( - initialValue ?? ({} as any), + const zero = Amounts.zeroOfCurrency(currency) + return (): State => { + const [instructed, setInstructed] = useState( + {amount: initialValue ?? zero, type: TransactionAmountMode.Raw}, ); - const amountStr = Amounts.stringify(amount); + const amountStr = Amounts.stringify(instructed.amount); const depositPaytoUri = stringifyPaytoUri(currentAccount); - // eslint-disable-next-line react-hooks/rules-of-hooks const hook = useAsyncAsHook(async () => { - const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, { - amount: amountStr, - depositPaytoUri, - }); + const fee = await api.wallet.call( + WalletApiOperation.ConvertDepositAmount, + { + amount: amountStr, + type: instructed.type, + depositPaytoUri, + }, + ); return { fee }; }, [amountStr, depositPaytoUri]); @@ -183,18 +187,16 @@ export function useComponentState({ const totalFee = fee !== undefined - ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount + ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount : Amounts.zeroOfCurrency(currency); - const totalToDeposit = - fee !== undefined - ? Amounts.sub(amount, totalFee).amount - : Amounts.zeroOfCurrency(currency); + const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount); + const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount); - const isDirty = amount !== initialValue; + const isDirty = instructed.amount !== initialValue; const amountError = !isDirty ? undefined - : Amounts.cmp(balance, amount) === -1 + : Amounts.cmp(balance, totalEffective) === -1 ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; @@ -207,7 +209,7 @@ export function useComponentState({ if (!currency) return; const depositPaytoUri = stringifyPaytoUri(currentAccount); - const amountStr = Amounts.stringify(amount); + const amountStr = Amounts.stringify(totalEffective); await api.wallet.call(WalletApiOperation.CreateDepositGroup, { amount: amountStr, depositPaytoUri, @@ -220,8 +222,19 @@ export function useComponentState({ error: undefined, currency, amount: { - value: amount, - onInput: pushAlertOnError(async (a) => setAmount(a)), + value: totalEffective, + onInput: pushAlertOnError(async (a) => setInstructed({ + amount: a, + type: TransactionAmountMode.Effective, + })), + error: amountError, + }, + totalToDeposit: { + value: totalToDeposit, + onInput: pushAlertOnError(async (a) => setInstructed({ + amount: a, + type: TransactionAmountMode.Raw, + })), error: amountError, }, onAddAccount: { @@ -244,7 +257,6 @@ export function useComponentState({ onClick: unableToDeposit ? undefined : pushAlertOnError(doSend), }, totalFee, - totalToDeposit, }; }; } @@ -269,7 +281,7 @@ export function createLabelsForBankAccount( ): { [value: string]: string } { const initialList: Record<string, string> = {}; if (!knownBankAccounts.length) return initialList; - return knownBankAccounts.reduce((prev, cur, i) => { + return knownBankAccounts.reduce((prev, cur) => { prev[stringifyPaytoUri(cur.uri)] = cur.alias; return prev; }, initialList); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx index c23f83fdd..0ed62220b 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx @@ -53,7 +53,10 @@ export const WithNoAccountForIBAN = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); @@ -82,7 +85,10 @@ export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); @@ -111,6 +117,9 @@ export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, { onClick: nullFunction, }, totalFee: Amounts.zeroOfCurrency("USD"), - totalToDeposit: Amounts.parseOrThrow("USD:10"), + totalToDeposit: { + onInput:nullFunction, + value: Amounts.parseOrThrow("USD:10"), + }, // onCalculateFee: alwaysReturnFeeToOne, }); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 157cb868a..1144095e1 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -20,17 +20,16 @@ */ import { + AmountResponse, Amounts, AmountString, - DepositGroupFees, parsePaytoUri, - PrepareDepositResponse, ScopeType, - stringifyPaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { expect } from "chai"; import * as tests from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; import { nullFunction } from "../../mui/handlers.js"; import { createWalletApiMock } from "../../test-utils.js"; @@ -38,24 +37,14 @@ import { useComponentState } from "./state.js"; const currency = "EUR"; const amount = `${currency}:0`; -const withoutFee = (): PrepareDepositResponse => ({ - effectiveDepositAmount: `${currency}:5` as AmountString, - totalDepositCost: `${currency}:5` as AmountString, - fees: { - coin: Amounts.stringify(`${currency}:0`), - wire: Amounts.stringify(`${currency}:0`), - refresh: Amounts.stringify(`${currency}:0`), - }, +const withoutFee = (value: number): AmountResponse => ({ + effectiveAmount: `${currency}:${value}` as AmountString, + rawAmount: `${currency}:${value}` as AmountString, }); -const withSomeFee = (): PrepareDepositResponse => ({ - effectiveDepositAmount: `${currency}:5` as AmountString, - totalDepositCost: `${currency}:5` as AmountString, - fees: { - coin: Amounts.stringify(`${currency}:1`), - wire: Amounts.stringify(`${currency}:1`), - refresh: Amounts.stringify(`${currency}:1`), - }, +const withSomeFee = (value: number, fee: number): AmountResponse => ({ + effectiveAmount: `${currency}:${value}` as AmountString, + rawAmount: `${currency}:${value - fee}` as AmountString, }); describe("DepositPage states", () => { @@ -195,9 +184,9 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); const hookBehavior = await tests.hookBehaveLikeThis( @@ -255,15 +244,15 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); const accountSelected = stringifyPaytoUri(ibanPayto.uri); @@ -345,19 +334,19 @@ describe("DepositPage states", () => { }, ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withoutFee(), + withoutFee(0), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withSomeFee(), + withSomeFee(10,3), ); handler.addWalletCallResponse( - WalletApiOperation.PrepareDeposit, + WalletApiOperation.ConvertDepositAmount, undefined, - withSomeFee(), + withSomeFee(10,3), ); const accountSelected = stringifyPaytoUri(ibanPayto.uri); @@ -404,7 +393,7 @@ describe("DepositPage states", () => { expect(state.account.value).eq(accountSelected); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); - expect(state.totalToDeposit).deep.eq( + expect(state.totalToDeposit.value).deep.eq( Amounts.parseOrThrow(`${currency}:7`), ); expect(state.depositHandler.onClick).not.undefined; @@ -416,7 +405,7 @@ describe("DepositPage states", () => { expect(state.account.value).eq(accountSelected); expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); - expect(state.totalToDeposit).deep.eq( + expect(state.totalToDeposit.value).deep.eq( Amounts.parseOrThrow(`${currency}:7`), ); expect(state.depositHandler.onClick).not.undefined; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx index 908becb04..b3607ebba 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -26,7 +26,7 @@ import { Grid } from "../../mui/Grid.js"; import { State } from "./index.js"; export function AmountOrCurrencyErrorView( - p: State.AmountOrCurrencyError, + _p: State.AmountOrCurrencyError, ): VNode { const { i18n } = useTranslationContext(); @@ -145,7 +145,7 @@ export function ReadyView(state: State.Ready): VNode { </p> <Grid container spacing={2} columns={1}> <Grid item xs={1}> - <AmountField label={i18n.str`Amount`} handler={state.amount} /> + <AmountField label={i18n.str`Brut amount`} handler={state.amount} /> </Grid> <Grid item xs={1}> <AmountField @@ -156,12 +156,7 @@ export function ReadyView(state: State.Ready): VNode { /> </Grid> <Grid item xs={1}> - <AmountField - label={i18n.str`Total deposit`} - handler={{ - value: state.totalToDeposit, - }} - /> + <AmountField label={i18n.str`Net amount`} handler={state.totalToDeposit} /> </Grid> </Grid> </section> @@ -180,7 +175,7 @@ export function ReadyView(state: State.Ready): VNode { ) : ( <Button variant="contained" onClick={state.depositHandler.onClick}> <i18n.Translate> - Deposit {Amounts.stringifyValue(state.totalToDeposit)}{" "} + Deposit {Amounts.stringifyValue(state.totalToDeposit.value)}{" "} {state.currency} </i18n.Translate> </Button> diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index 53380e263..8f23c0685 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -17,25 +17,31 @@ import { AbsoluteTime, Amounts, - CoinDumpJson, CoinStatus, - ExchangeListItem, ExchangeTosStatus, LogLevel, NotificationType, ScopeType, - parseWithdrawUri, - stringifyWithdrawExchange, + stringifyWithdrawExchange } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; +import { Pages } from "../NavigationBar.js"; import { Checkbox } from "../components/Checkbox.js"; import { SelectList } from "../components/SelectList.js"; import { Time } from "../components/Time.js"; -import { DestructiveText, LinkPrimary, NotifyUpdateFadeOut, SubTitle, SuccessText, WarningText } from "../components/styled/index.js"; +import { ActiveTasksTable } from "../components/WalletActivity.js"; +import { + DestructiveText, + LinkPrimary, + NotifyUpdateFadeOut, + SubTitle, + SuccessText, + WarningText, +} from "../components/styled/index.js"; import { useAlertContext } from "../context/alert.js"; import { useBackendContext } from "../context/backend.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; @@ -44,11 +50,7 @@ import { Button } from "../mui/Button.js"; import { Grid } from "../mui/Grid.js"; import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; -import { Pages } from "../NavigationBar.js"; -import { CoinInfo } from "@gnu-taler/taler-wallet-core/dbless"; -import { ActiveTasksTable } from "../components/WalletActivity.js"; -type CoinsInfo = CoinDumpJson["coins"]; type CalculatedCoinfInfo = { // ageKeysCount: number | undefined; denom_value: number; @@ -64,15 +66,7 @@ type SplitedCoinInfo = { usable: CalculatedCoinfInfo[]; }; -export interface Props { - // FIXME: Pending operations don't exist anymore. -} - -function hashObjectId(o: any): string { - return JSON.stringify(o); -} - -export function DeveloperPage({ }: Props): VNode { +export function DeveloperPage(): VNode { const { i18n } = useTranslationContext(); const [downloadedDatabase, setDownloadedDatabase] = useState< { time: Date; content: string } | undefined @@ -110,8 +104,8 @@ export function DeveloperPage({ }: Props): VNode { useEffect(() => { return api.listener.onUpdateNotification(listenAllEvents, (ev) => { - console.log("event", ev) - return hook?.retry() + console.log("event", ev); + return hook?.retry(); }); }); @@ -275,7 +269,6 @@ export function DeveloperPage({ }: Props): VNode { })} /> - <SubTitle> <i18n.Translate>Exchange Entries</i18n.Translate> </SubTitle> @@ -336,19 +329,32 @@ export function DeveloperPage({ }: Props): VNode { ); } } - const uri = !e.masterPub ? undefined : stringifyWithdrawExchange({ - exchangeBaseUrl: e.exchangeBaseUrl, - exchangePub: e.masterPub, - }); + const uri = !e.masterPub + ? undefined + : stringifyWithdrawExchange({ + exchangeBaseUrl: e.exchangeBaseUrl, + }); return ( <tr key={idx}> <td> <a href={!uri ? undefined : Pages.defaultCta({ uri })}> - {e.scopeInfo ? `${e.scopeInfo.currency} (${e.scopeInfo.type === ScopeType.Global ? "global" : "regional"})` : e.currency} + {e.scopeInfo + ? `${e.scopeInfo.currency} (${ + e.scopeInfo.type === ScopeType.Global + ? "global" + : "regional" + })` + : e.currency} </a> </td> <td> - <a href={new URL(`/keys`, e.exchangeBaseUrl).href} target="_blank">{e.exchangeBaseUrl}</a> + <a + href={new URL(`/keys`, e.exchangeBaseUrl).href} + target="_blank" + rel="noreferrer" + > + {e.exchangeBaseUrl} + </a> </td> <td> {e.exchangeEntryStatus} / {e.exchangeUpdateStatus} @@ -359,10 +365,10 @@ export function DeveloperPage({ }: Props): VNode { <td> {e.lastUpdateTimestamp ? AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp( - e.lastUpdateTimestamp, - ), - ) + AbsoluteTime.fromPreciseTimestamp( + e.lastUpdateTimestamp, + ), + ) : "never"} </td> <td> @@ -381,31 +387,25 @@ export function DeveloperPage({ }: Props): VNode { </button> <button onClick={() => { - api.wallet.call( - WalletApiOperation.DeleteExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - }, - ); + api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + }); }} > Delete </button> <button onClick={() => { - api.wallet.call( - WalletApiOperation.DeleteExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - purge: true, - }, - ); + api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + purge: true, + }); }} > Purge </button> - {e.scopeInfo && e.masterPub && e.currency ? - (e.scopeInfo.type === ScopeType.Global ? + {e.scopeInfo && e.masterPub && e.currency ? ( + e.scopeInfo.type === ScopeType.Global ? ( <button onClick={() => { api.wallet.call( @@ -418,30 +418,27 @@ export function DeveloperPage({ }: Props): VNode { ); }} > - Make regional </button> - : e.scopeInfo.type === ScopeType.Auditor ? - undefined - - : e.scopeInfo.type === ScopeType.Exchange ? - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.AddGlobalCurrencyExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - currency: e.currency!, - exchangeMasterPub: e.masterPub!, - }, - ); - }} - > - - Make global - </button> - : undefined) : undefined - } + ) : e.scopeInfo.type === + ScopeType.Auditor ? undefined : e.scopeInfo.type === + ScopeType.Exchange ? ( + <button + onClick={() => { + api.wallet.call( + WalletApiOperation.AddGlobalCurrencyExchange, + { + exchangeBaseUrl: e.exchangeBaseUrl, + currency: e.currency!, + exchangeMasterPub: e.masterPub!, + }, + ); + }} + > + Make global + </button> + ) : undefined + ) : undefined} <button onClick={() => { api.wallet.call( @@ -469,7 +466,6 @@ export function DeveloperPage({ }: Props): VNode { </LinkPrimary> </div> - <Paper style={{ padding: 10, margin: 10 }}> <h3>Logging</h3> <div> diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx index 7b80977f3..b995a44d0 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -130,7 +130,6 @@ export function ReadyView({ ))} </div> <div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}> - --- {uri.value} --- <p> <CustomFieldByAccountType type={accountType.value as AccountType} diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 4394a982f..47b466fcd 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -55,7 +55,7 @@ import { WalletActivityTrack } from "./wxBackend.js"; const logger = new Logger("wxApi"); -export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0" +export const WALLET_CORE_SUPPORTED_VERSION = "5:0:0" export interface ExtendedPermissionsResponse { newValue: boolean; diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 5fa255f5d..a0b9f2908 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -39,7 +39,7 @@ import { makeErrorDetail, openPromise, setGlobalLogLevelFromString, - setLogLevelFromString + setLogLevelFromString, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { @@ -92,7 +92,7 @@ async function resetDb(): Promise<void> { export type WalletActivityTrack = { id: number; - events: (WalletNotification & {when: AbsoluteTime})[]; + events: (WalletNotification & { when: AbsoluteTime })[]; start: AbsoluteTime; type: NotificationType; end: AbsoluteTime; @@ -107,130 +107,138 @@ function getUniqueId(): number { //FIXME: maybe circular buffer const activity: WalletActivityTrack[] = []; -function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) { - const start = AbsoluteTime.now(); - const ev = {...n, when:start}; - switch (n.type) { +function convertWalletActivityNotification( + knownEvents: WalletActivityTrack[], + event: WalletNotification & { + when: AbsoluteTime; + }, +): WalletActivityTrack | undefined { + switch (event.type) { case NotificationType.BalanceChange: { - const groupId = `${n.type}:${n.hintTransactionId}`; - const found = list.find((a)=>a.groupId === groupId) + const groupId = `${event.type}:${event.hintTransactionId}`; + const found = knownEvents.find((a) => a.groupId === groupId); if (found) { - found.end = start; - found.events.unshift(ev) - return; + found.end = event.when; + found.events.unshift(event); + return found; } - list.push({ + return { id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, - }); - return; + }; } case NotificationType.BackupOperationError: { const groupId = ""; - list.push({ + return { id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, - }); - return; + }; } case NotificationType.TransactionStateTransition: { - const groupId = `${n.type}:${n.transactionId}`; - const found = list.find((a)=>a.groupId === groupId) + const groupId = `${event.type}:${event.transactionId}`; + const found = knownEvents.find((a) => a.groupId === groupId); if (found) { - found.end = start; - found.events.unshift(ev) - return; + found.end = event.when; + found.events.unshift(event); + return found; } - list.push({ + return { id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, - }); - return; + }; } case NotificationType.WithdrawalOperationTransition: { - return; + return undefined; } case NotificationType.ExchangeStateTransition: { - const groupId = `${n.type}:${n.exchangeBaseUrl}`; - const found = list.find((a)=>a.groupId === groupId) + const groupId = `${event.type}:${event.exchangeBaseUrl}`; + const found = knownEvents.find((a) => a.groupId === groupId); if (found) { - found.end = start; - found.events.unshift(ev) - return; + found.end = event.when; + found.events.unshift(event); + return found; } - list.push({ + return { id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, - }); - return; + }; } case NotificationType.Idle: { const groupId = ""; - list.push({ + return({ id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, }); - return; } case NotificationType.TaskObservabilityEvent: { - const groupId = `${n.type}:${n.taskId}`; - const found = list.find((a)=>a.groupId === groupId) + const groupId = `${event.type}:${event.taskId}`; + const found = knownEvents.find((a) => a.groupId === groupId); if (found) { - found.end = start; - found.events.unshift(ev) - return; + found.end = event.when; + found.events.unshift(event); + return found; } - list.push({ + return({ id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, }); - return; } case NotificationType.RequestObservabilityEvent: { - const groupId = `${n.type}:${n.operation}:${n.requestId}`; - const found = list.find((a)=>a.groupId === groupId) + const groupId = `${event.type}:${event.operation}:${event.requestId}`; + const found = knownEvents.find((a) => a.groupId === groupId); if (found) { - found.end = start; - found.events.unshift(ev) - return; + found.end = event.when; + found.events.unshift(event); + return found; } - list.push({ + return({ id: getUniqueId(), - type: n.type, - start, + type: event.type, + start: event.when, end: AbsoluteTime.never(), - events: [ev], + events: [event], groupId, }); - return; } } } +function addNewWalletActivityNotification( + list: WalletActivityTrack[], + n: WalletNotification, +) { + const start = AbsoluteTime.now(); + const ev = { ...n, when: start }; + const activity = convertWalletActivityNotification(list, ev); + if (activity) { + list.unshift(activity); // insert at start + } +} + async function getNotifications({ filter, }: { |