diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet')
24 files changed, 994 insertions, 618 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts index 94b32c157..43898ecc1 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util"; +import { OperationAlternative, OperationFail, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -34,13 +34,6 @@ export type State = State.Loading | State.Confirm | State.Verify; -export type CheckExchangeErrors = { - "invalid-version": string; - "invalid-currency": string; - "not-found": void; - "already-active": void; - "invalid-protocol": void; -} export namespace State { export interface Loading { @@ -73,11 +66,16 @@ export namespace State { url: TextFieldHandler, loading: boolean; knownExchanges: URL[], - result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> | OperationFailWithBody<CheckExchangeErrors> | undefined, + result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> + | OperationAlternative<"invalid-version", string> + | OperationAlternative<"invalid-currency", string> + | OperationFail<"not-found"> + | OperationFail<"already-active"> + | OperationFail<"invalid-protocol"> + | undefined, expectedCurrency: string | undefined, } } - const viewMapping: StateViewMap<State> = { loading: Loading, error: ErrorAlertView, diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts index 4a04f762a..c0756d1e2 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util"; +import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailure, opKnownFailureWithBody } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; import { useCallback, useEffect, useState } from "preact/hooks"; @@ -22,7 +22,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { withSafe } from "../../mui/handlers.js"; import { RecursiveState } from "../../utils/index.js"; -import { CheckExchangeErrors, Props, State } from "./index.js"; +import { Props, State } from "./index.js"; function urlFromInput(str: string): URL { let result: URL; @@ -59,11 +59,11 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) { const baseUrl = urlFromInput(str) if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") { - return opKnownFailureWithBody<CheckExchangeErrors>("invalid-protocol", undefined) + return opKnownFailure("invalid-protocol"as const) } const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href); if (found !== -1) { - return opKnownFailureWithBody<CheckExchangeErrors>("already-active", undefined); + return opKnownFailure("already-active"as const); } /** @@ -84,13 +84,13 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any); const config = await api.getConfig() if (config.type === "fail") { - return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined) + return opKnownFailure("not-found" as const) } if (!api.isCompatible(config.body.version)) { - return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version) + return opKnownFailureWithBody("invalid-version"as const, config.body.version) } if (currency !== undefined && currency !== config.body.currency) { - return opKnownFailureWithBody<CheckExchangeErrors>("invalid-currency", config.body.currency) + return opKnownFailureWithBody("invalid-currency"as const, config.body.currency) } const keys = await api.getKeys() return keys @@ -177,7 +177,7 @@ function useDebounce<T>( setError(er); } else { // @ts-expect-error cause still not in typescript - setError(new Error('unkown error on debounce', { cause: er })) + setError(new Error('unknown error on debounce', { cause: er })) } setLoading(false); setResult(undefined); diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx index f6537bc68..882d95670 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx @@ -120,8 +120,9 @@ export function VerifyView({ </WarningBox> ); } + default: { - assertUnreachable(result.case); + assertUnreachable(result); } } })()} diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx index dd1777fd1..62f1ffbb1 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx +++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx @@ -27,7 +27,7 @@ export interface Props { export function AddNewActionView({ onCancel }: Props): VNode { const [url, setUrl] = useState(""); - const uri = parseTalerUri(url); + const uri = parseTalerUri(url.toLowerCase()); const { i18n } = useTranslationContext(); async function redirectToWallet(): Promise<void> { diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 893122c0f..783935143 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -22,14 +22,21 @@ import { Amounts, + ScopeInfo, TalerUri, TalerUriAction, TranslatedString, + parsePaytoUri, + parseScopeInfoShort, parseTalerUri, + stringifyPaytoUri, + stringifyScopeInfoShort, stringifyTalerUri, } from "@gnu-taler/taler-util"; import { TranslationProvider, + decodeCrockFromURI, + encodeCrockForURI, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; @@ -87,6 +94,8 @@ import { WalletActivity } from "../components/WalletActivity.js"; import { EnabledBySettings } from "../components/EnabledBySettings.js"; import { DevExperimentPage } from "../cta/DevExperiment/index.js"; import { ConfirmAddExchangeView } from "./AddExchange/views.js"; +import { ManageAccountPage } from "./ManageAccount/index.js"; +import { SupportedBanksForAccount } from "./SupportedBanksForAccount.js"; export function Application(): VNode { const { i18n } = useTranslationContext(); @@ -96,7 +105,7 @@ export function Application(): VNode { redirectTo(Pages.balanceTransaction({ tid })); } function redirectToURL(str: string): void { - window.location.href = new URL(str).href + window.location.href = new URL(str).href; } return ( @@ -115,12 +124,17 @@ export function Application(): VNode { <Route path={Pages.qr} component={() => ( - <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + <WalletTemplate + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <QrReaderPage onDetected={(talerActionUrl: TalerUri) => { redirectTo( Pages.defaultCta({ - uri: stringifyTalerUri(talerActionUrl), + uri: encodeCrockForURI( + stringifyTalerUri(talerActionUrl), + ), }), ); }} @@ -132,7 +146,10 @@ export function Application(): VNode { <Route path={Pages.settings} component={() => ( - <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + <WalletTemplate + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <SettingsPage /> </WalletTemplate> )} @@ -159,17 +176,33 @@ export function Application(): VNode { <Route path={Pages.balanceHistory.pattern} - component={({ currency }: { currency?: string }) => ( - <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + component={({ scope }: { scope?: string }) => ( + <WalletTemplate + path="balance" + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <HistoryPage - currency={currency} - goToWalletDeposit={(currency: string) => - redirectTo(Pages.sendCash({ amount: `${currency}:0` })) + scope={ + !scope + ? undefined + : parseScopeInfoShort(decodeCrockFromURI(scope)) } - goToWalletManualWithdraw={(currency?: string) => + goToWalletDeposit={(scope: ScopeInfo) => + redirectTo( + Pages.sendCash({ + scope: encodeCrockForURI( + stringifyScopeInfoShort(scope), + ), + }), + ) + } + goToWalletManualWithdraw={(scope?: ScopeInfo) => redirectTo( Pages.receiveCash({ - amount: !currency ? undefined : `${currency}:0`, + scope: !scope + ? undefined + : encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -179,18 +212,34 @@ export function Application(): VNode { /> <Route path={Pages.searchHistory.pattern} - component={({ currency }: { currency?: string }) => ( - <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + component={({ scope }: { scope?: string }) => ( + <WalletTemplate + path="balance" + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <HistoryPage - currency={currency} + scope={ + !scope + ? undefined + : parseScopeInfoShort(decodeCrockFromURI(scope)) + } search - goToWalletDeposit={(currency: string) => - redirectTo(Pages.sendCash({ amount: `${currency}:0` })) + goToWalletDeposit={(scope: ScopeInfo) => + redirectTo( + Pages.sendCash({ + scope: encodeCrockForURI( + stringifyScopeInfoShort(scope), + ), + }), + ) } - goToWalletManualWithdraw={(currency?: string) => + goToWalletManualWithdraw={(scope?: ScopeInfo) => redirectTo( Pages.receiveCash({ - amount: !currency ? undefined : `${currency}:0`, + scope: !scope + ? undefined + : encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -200,37 +249,127 @@ export function Application(): VNode { /> <Route path={Pages.sendCash.pattern} - component={({ amount }: { amount?: string }) => ( - <WalletTemplate path="balance" goToURL={redirectToURL}> - <DestinationSelectionPage - type="send" - amount={amount} - goToWalletBankDeposit={(amount: string) => - redirectTo(Pages.balanceDeposit({ amount })) - } - goToWalletWalletSend={(amount: string) => - redirectTo(Pages.ctaTransferCreate({ amount })) - } - /> - </WalletTemplate> - )} + component={({ scope }: { scope?: string }) => { + if (!scope) return <Redirect to={Pages.balanceHistory({})} />; + const s = parseScopeInfoShort(decodeCrockFromURI(scope)); + if (!s) return <Redirect to={Pages.balanceHistory({})} />; + + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + <DestinationSelectionPage + type="send" + scope={s} + goToWalletKnownBankDeposit={(s, p) => + redirectTo( + Pages.ctaDeposit({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + account: encodeCrockForURI(stringifyPaytoUri(p)), + }), + ) + } + goToWalletNewBankDeposit={(s) => + redirectTo( + Pages.bankManange({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + }), + ) + } + goToWalletWalletSend={(s) => + redirectTo( + Pages.ctaTransferCreate({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + }), + ) + } + /> + </WalletTemplate> + ); + }} + /> + <Route + path={Pages.bankManange.pattern} + component={({ scope }: { scope?: string }) => { + const s = !scope + ? undefined + : parseScopeInfoShort(decodeCrockFromURI(scope)); + if (!s) return <div>missing scope</div>; + + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + <ManageAccountPage + scope={s} + onAccountAdded={(account) => + redirectTo( + Pages.ctaDeposit({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + account: encodeCrockForURI( + stringifyPaytoUri(account), + ), + }), + ) + } + onCancel={() => { + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + }), + ); + }} + /> + </WalletTemplate> + ); + }} + /> + <Route + path={Pages.receiveCashForPurchase.pattern} + component={({ id: _purchaseId }: { id?: string }) => { + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + not yet implemented + </WalletTemplate> + ); + }} + /> + <Route + path={Pages.receiveCashForInvoice.pattern} + component={({ id: _invoiceId }: { id?: string }) => { + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + not yet implemented + </WalletTemplate> + ); + }} /> <Route path={Pages.receiveCash.pattern} - component={({ amount }: { amount?: string }) => ( - <WalletTemplate path="balance" goToURL={redirectToURL}> - <DestinationSelectionPage - type="get" - amount={amount} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.ctaWithdrawManual({ amount })) - } - goToWalletWalletInvoice={(amount?: string) => - redirectTo(Pages.ctaInvoiceCreate({ amount })) - } - /> - </WalletTemplate> - )} + component={({ scope }: { scope?: string }) => { + const s = !scope + ? undefined + : parseScopeInfoShort(decodeCrockFromURI(scope)); + + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + <DestinationSelectionPage + type="get" + scope={s} + goToWalletManualWithdraw={(s) => + redirectTo( + Pages.ctaWithdrawManualForScope({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + }), + ) + } + goToWalletWalletInvoice={(s) => + redirectTo( + Pages.ctaInvoiceCreate({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + }), + ) + } + /> + </WalletTemplate> + ); + }} /> <Route @@ -239,8 +378,14 @@ export function Application(): VNode { <WalletTemplate path="balance" goToURL={redirectToURL}> <TransactionPage tid={tid} - goToWalletHistory={(currency?: string) => - redirectTo(Pages.balanceHistory({ currency })) + goToWalletHistory={(scope: ScopeInfo) => + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI( + stringifyScopeInfoShort(scope), + ), + }), + ) } /> </WalletTemplate> @@ -249,25 +394,47 @@ export function Application(): VNode { <Route path={Pages.balanceDeposit.pattern} - component={({ amount }: { amount: string }) => ( - <WalletTemplate path="balance" goToURL={redirectToURL}> - <DepositPage - amount={amount} - onCancel={(currency: string) => { - redirectTo(Pages.balanceHistory({ currency })); - }} - onSuccess={(currency: string) => { - redirectTo(Pages.balanceHistory({ currency })); - }} - /> - </WalletTemplate> - )} + component={({ scope }: { scope: string }) => { + const s = parseScopeInfoShort(decodeCrockFromURI(scope)); + if (!s) { + return <div>missing scope</div>; + } + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + <DepositPage + scope={s} + onCancel={(scope: ScopeInfo) => { + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI( + stringifyScopeInfoShort(scope), + ), + }), + ); + }} + onSuccess={(scope: ScopeInfo) => { + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI( + stringifyScopeInfoShort(scope), + ), + }), + ); + }} + /> + </WalletTemplate> + ); + }} /> <Route path={Pages.backup} component={() => ( - <WalletTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + <WalletTemplate + path="backup" + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <BackupPage onAddProvider={() => redirectTo(Pages.backupProviderAdd)} /> @@ -283,8 +450,9 @@ export function Application(): VNode { onPayProvider={(uri: string) => redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) } - onWithdraw={(amount: string) => - redirectTo(Pages.receiveCash({ amount })) + onWithdraw={(_amount: string) => + // FIXME: use receiveCashForPurchase + redirectTo(Pages.receiveCash({ scope: "FIXME missing" })) } onBack={() => redirectTo(Pages.backup)} /> @@ -314,7 +482,11 @@ export function Application(): VNode { <Route path={Pages.dev} component={() => ( - <WalletTemplate path="dev" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}> + <WalletTemplate + path="dev" + goToTransaction={redirectToTxInfo} + goToURL={redirectToURL} + > <DeveloperPage /> </WalletTemplate> )} @@ -326,7 +498,7 @@ export function Application(): VNode { <Route path={Pages.defaultCta.pattern} component={({ uri }: { uri: string }) => { - const path = getPathnameForTalerURI(uri); + const path = getPathnameForTalerURI(decodeCrockFromURI(uri)); if (!path) { return ( <CallToActionTemplate title={i18n.str`Taler URI handler`}> @@ -343,14 +515,37 @@ export function Application(): VNode { return <Redirect to={path} />; }} /> + {/* // FIXME: mem leak problems */} + <Route + path={Pages.defaultCtaSimple.pattern} + component={({ uri }: { uri: string }) => { + const path = getPathnameForTalerURI(decodeURIComponent(uri)); + if (!path) { + return ( + <CallToActionTemplate title={i18n.str`Taler URI handler`}> + <AlertView + alert={{ + type: "warning", + message: i18n.str`Could not found a handler for the Taler URI`, + description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`, + }} + /> + </CallToActionTemplate> + ); + } + return <Redirect to={path} />; + }} + /> + <Route path={Pages.ctaPay} component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash payment`}> <PaymentPage - talerPayUri={decodeURIComponent(talerUri)} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.receiveCash({ amount })) + talerPayUri={decodeCrockFromURI(talerUri)} + goToWalletManualWithdraw={(_amount?: string) => + // FIXME: use receiveCashForPruchase + redirectTo(Pages.receiveCash({})) } cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => @@ -365,9 +560,10 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash payment`}> <PaymentTemplatePage - talerTemplateUri={decodeURIComponent(talerUri)} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.receiveCash({ amount })) + talerTemplateUri={decodeCrockFromURI(talerUri)} + goToWalletManualWithdraw={(_amount?: string) => + // FIXME: use receiveCashForPruchase + redirectTo(Pages.receiveCash({})) } cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => @@ -382,7 +578,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash refund`}> <RefundPage - talerRefundUri={decodeURIComponent(talerUri)} + talerRefundUri={decodeCrockFromURI(talerUri)} cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -396,7 +592,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> <WithdrawPageFromURI - talerWithdrawUri={decodeURIComponent(talerUri)} + talerWithdrawUri={!talerUri ? undefined : decodeCrockFromURI(talerUri)} cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -408,55 +604,95 @@ export function Application(): VNode { <Route path={Pages.ctaWithdrawManual.pattern} component={({ + // scope, amount, talerUri, }: { + // scope: string; amount: string; talerUri: string; - }) => ( - <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> - <WithdrawPageFromParams - onAmountChanged={async (newamount) => { - const page = `${Pages.ctaWithdrawManual({ amount: newamount })}?talerUri=${encodeURIComponent(talerUri)}`; - redirectTo(page); - }} - talerExchangeWithdrawUri={talerUri} - amount={amount} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - </CallToActionTemplate> - )} + }) => { + return ( + <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> + <WithdrawPageFromParams + scope={undefined} + talerExchangeWithdrawUri={!talerUri ? undefined : decodeCrockFromURI(talerUri)} + amount={Amounts.parse(amount)} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ); + }} /> <Route - path={Pages.ctaDeposit} + path={Pages.ctaWithdrawManualForScope.pattern} component={({ + scope, amount, - talerUri, }: { + scope: string; amount: string; - talerUri: string; - }) => ( - <CallToActionTemplate title={i18n.str`Digital cash deposit`}> - <DepositPageCTA - amountStr={Amounts.stringify(Amounts.parseOrThrow(amount))} - talerDepositUri={decodeURIComponent(talerUri)} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - </CallToActionTemplate> - )} + }) => { + if (!scope) return <Redirect to={Pages.balanceHistory({})} />; + const s = parseScopeInfoShort(decodeCrockFromURI(scope)); + if (!s) return <Redirect to={Pages.balanceHistory({})} />; + + return ( + <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> + <WithdrawPageFromParams + talerExchangeWithdrawUri={undefined} + scope={s} + amount={Amounts.parse(amount)} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ); + }} + /> + <Route + path={Pages.ctaDeposit.pattern} + component={({ + scope, + account, + }: { + scope: string; + account: string; + }) => { + const s = parseScopeInfoShort(decodeCrockFromURI(scope)); + if (!s) { + return <div>missing scope</div>; + } + const p = parsePaytoUri(decodeCrockFromURI(account)); + if (!p) { + return <div>missing account</div>; + } + + return ( + <CallToActionTemplate title={i18n.str`Digital cash deposit`}> + <DepositPageCTA + scope={s} + account={p} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ); + }} /> <Route path={Pages.ctaInvoiceCreate.pattern} - component={({ amount }: { amount: string }) => ( + component={({ scope }: { scope: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash invoice`}> <InvoiceCreatePage - amount={Amounts.stringify(Amounts.parseOrThrow(amount))} + scope={parseScopeInfoShort(decodeCrockFromURI(scope))!} onClose={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -467,10 +703,10 @@ export function Application(): VNode { /> <Route path={Pages.ctaTransferCreate.pattern} - component={({ amount }: { amount: string }) => ( + component={({ scope }: { scope: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash transfer`}> <TransferCreatePage - amount={Amounts.stringify(Amounts.parseOrThrow(amount))} + scope={parseScopeInfoShort(decodeCrockFromURI(scope))!} onClose={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -481,41 +717,56 @@ export function Application(): VNode { /> <Route path={Pages.ctaInvoicePay} - component={({ talerUri }: { talerUri: string }) => ( - <CallToActionTemplate title={i18n.str`Digital cash invoice`}> - <InvoicePayPage - talerPayPullUri={decodeURIComponent(talerUri)} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.receiveCash({ amount })) - } - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - </CallToActionTemplate> - )} + component={({ talerUri }: { talerUri: string }) => { + const uri = (decodeCrockFromURI(talerUri)); + if (!uri) { + return <div>missing taler uri</div>; + } + + return ( + <CallToActionTemplate title={i18n.str`Digital cash invoice`}> + <InvoicePayPage + talerPayPullUri={uri} + goToWalletManualWithdraw={(_amount?: string) => + // FIXME: use receiveCashForInvoice + redirectTo(Pages.receiveCash({})) + } + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ) + }} /> <Route path={Pages.ctaTransferPickup} - component={({ talerUri }: { talerUri: string }) => ( - <CallToActionTemplate title={i18n.str`Digital cash transfer`}> - <TransferPickupPage - talerPayPushUri={decodeURIComponent(talerUri)} - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - </CallToActionTemplate> - )} + component={({ talerUri }: { talerUri: string }) => { + const uri = (decodeCrockFromURI(talerUri)); + if (!uri) { + return <div>missing taler uri</div>; + } + + return ( + <CallToActionTemplate title={i18n.str`Digital cash transfer`}> + <TransferPickupPage + talerPayPushUri={uri} + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ) + }} /> <Route path={Pages.ctaRecovery} component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash recovery`}> <RecoveryPage - talerRecoveryUri={decodeURIComponent(talerRecoveryUri)} + talerRecoveryUri={!talerRecoveryUri ? undefined : decodeCrockFromURI(talerRecoveryUri)} onCancel={() => redirectTo(Pages.balance)} onSuccess={() => redirectTo(Pages.backup)} /> @@ -527,7 +778,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Development experiment`}> <DevExperimentPage - talerExperimentUri={decodeURIComponent(talerUri)} + talerExperimentUri={!talerUri ? undefined : decodeCrockFromURI(talerUri)} onCancel={() => redirectTo(Pages.balanceHistory({}))} onSuccess={() => redirectTo(Pages.balanceHistory({}))} /> @@ -537,23 +788,63 @@ export function Application(): VNode { <Route path={Pages.ctaAddExchange} component={({ talerUri }: { talerUri: string }) => { - const tUri = parseTalerUri(decodeURIComponent(talerUri)) - const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined + const tUri = parseTalerUri( + decodeCrockFromURI(talerUri).toLowerCase(), + ); + const baseUrl = + tUri?.type === TalerUriAction.AddExchange + ? tUri.exchangeBaseUrl + : undefined; if (!baseUrl) { - redirectTo(Pages.balanceHistory({})) - return <div> - invalid url {talerUri} - </div> + redirectTo(Pages.balanceHistory({})); + return <div>invalid url {talerUri}</div>; } - return <CallToActionTemplate title={i18n.str`Add exchange`}> - <ConfirmAddExchangeView - url={baseUrl} - status="confirm" - error={undefined} - onCancel={() => redirectTo(Pages.balanceHistory({}))} - onConfirm={() => redirectTo(Pages.balanceHistory({}))} - /> - </CallToActionTemplate> + return ( + <CallToActionTemplate title={i18n.str`Add exchange`}> + <ConfirmAddExchangeView + url={baseUrl} + status="confirm" + error={undefined} + onCancel={() => redirectTo(Pages.balanceHistory({}))} + onConfirm={() => redirectTo(Pages.balanceHistory({}))} + /> + </CallToActionTemplate> + ); + }} + /> + <Route + path={Pages.paytoBanks.pattern} + component={({ payto }: { payto: string }) => { + const pUri = parsePaytoUri( + decodeCrockFromURI(payto).toLowerCase(), + ); + if (!pUri) { + redirectTo(Pages.balanceHistory({})); + return <div>invalid uri {pUri}</div>; + } + return ( + <WalletTemplate goToURL={redirectToURL}> + <SupportedBanksForAccount account={pUri} /> + </WalletTemplate> + ); + }} + /> + <Route + path={Pages.paytoQrs.pattern} + component={({ payto }: { payto: string }) => { + const pUri = parsePaytoUri( + decodeCrockFromURI(payto).toLowerCase(), + ); + if (!pUri) { + redirectTo(Pages.balanceHistory({})); + return <div>invalid uri {pUri}</div>; + } + return ( + <WalletTemplate goToURL={redirectToURL}> + {/* <AllQrsForAccount account={pUri} /> */} + <pre>{JSON.stringify({ title: "QRS", pUri })}</pre> + </WalletTemplate> + ); }} /> {/** @@ -613,7 +904,7 @@ function CallToActionTemplate({ <WalletAction> <LogoHeader /> <section style={{ display: "flex", justifyContent: "right", margin: 0 }}> - <LinkPrimary href={Pages.balance}> + <LinkPrimary href={`#${Pages.balance}`}> <div style={{ height: 24, @@ -633,7 +924,7 @@ function CallToActionTemplate({ {children} </AlertProvider> <section style={{ display: "flex", justifyContent: "right" }}> - <LinkPrimary href={Pages.balance}> + <LinkPrimary href={`#${Pages.balance}`}> <i18n.Translate>Return to wallet</i18n.Translate> </LinkPrimary> </section> @@ -665,7 +956,8 @@ function WalletTemplate({ <WalletNavBar path={path} /> <PendingTransactions goToTransaction={goToTransaction} - goToURL={goToURL} /> + goToURL={goToURL} + /> <WalletBox> <AlertProvider> <CurrentAlerts /> diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx index 8a3710f69..645fbf67c 100644 --- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx @@ -261,9 +261,9 @@ function BackupLayout(props: TransactionLayoutProps): VNode { <RowBorderGray> <div style={{ color: !props.active ? "grey" : undefined }}> <a - href={Pages.backupProviderDetail({ - pid: encodeURIComponent(props.id), - })} + href={`#${Pages.backupProviderDetail({ + pid: props.id, + })}`} > <span>{props.title}</span> </a> diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index daba6aba4..22ad1c1e7 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountJson, PaytoUri } from "@gnu-taler/taler-util"; +import { AmountJson, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -34,9 +34,9 @@ import { } from "./views.js"; export interface Props { - amount?: string; - onCancel: (currency: string) => void; - onSuccess: (currency: string) => void; + scope:ScopeInfo; + onCancel: (scope: ScopeInfo) => void; + onSuccess: (scope: ScopeInfo) => void; } export type State = @@ -62,8 +62,8 @@ export namespace State { export interface AddingAccount { status: "manage-account"; error: undefined; - currency: string; - onAccountAdded: (p: string) => void; + scope: ScopeInfo; + onAccountAdded: (p: PaytoUri) => void; onCancel: () => void; } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index b674665cf..29f533385 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -32,47 +32,35 @@ import { RecursiveState } from "../../utils/index.js"; import { Props, State } from "./index.js"; export function useComponentState({ - amount: amountStr, + scope, onCancel, onSuccess, }: Props): RecursiveState<State> { const api = useBackendContext(); const { i18n } = useTranslationContext(); const { pushAlertOnError } = useAlertContext(); - const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr); - const currency = parsed !== undefined ? parsed.currency : undefined; + + const zero = Amounts.zeroOfCurrency(scope.currency); const hook = useAsyncAsHook(async () => { const { balances } = await api.wallet.call( WalletApiOperation.GetBalances, - {}, + { + }, ); + const { accounts } = await api.wallet.call( WalletApiOperation.ListKnownBankAccounts, - { currency }, + { currency: scope.currency }, ); return { accounts, balances }; }); - const initialValue = - parsed !== undefined - ? parsed - : currency !== undefined - ? Amounts.zeroOfCurrency(currency) - : undefined; - // const [accountIdx, setAccountIdx] = useState<number>(0); const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [addingAccount, setAddingAccount] = useState(false); - if (!currency) { - return { - status: "amount-or-currency-error", - error: undefined, - }; - } - if (!hook) { return { status: "loading", @@ -102,9 +90,9 @@ export function useComponentState({ return { status: "manage-account", error: undefined, - currency, - onAccountAdded: (p: string) => { - updateAccountFromList(p); + scope, + onAccountAdded: (p: PaytoUri) => { + updateAccountFromList(stringifyPaytoUri(p)); setAddingAccount(false); hook.retry(); }, @@ -115,17 +103,17 @@ export function useComponentState({ }; } - const bs = balances.filter((b) => b.available.startsWith(currency)); + const bs = balances.filter((b) => b.scopeInfo === scope); const balance = bs.length > 0 ? Amounts.parseOrThrow(bs[0].available) - : Amounts.zeroOfCurrency(currency); + : Amounts.zeroOfCurrency(scope.currency); if (Amounts.isZero(balance)) { return { status: "no-enough-balance", error: undefined, - currency, + currency: scope.currency, }; } @@ -133,7 +121,7 @@ export function useComponentState({ return { status: "no-accounts", error: undefined, - currency, + currency: scope.currency, onAddAccount: { onClick: pushAlertOnError(async () => { setAddingAccount(true); @@ -143,10 +131,9 @@ export function useComponentState({ } const firstAccount = accounts[0].uri; const currentAccount = !selectedAccount ? firstAccount : selectedAccount; - const zero = Amounts.zeroOfCurrency(currency) return (): State => { const [instructed, setInstructed] = useState( - {amount: initialValue ?? zero, type: TransactionAmountMode.Raw}, + { amount: zero, type: TransactionAmountMode.Raw }, ); const amountStr = Amounts.stringify(instructed.amount); const depositPaytoUri = stringifyPaytoUri(currentAccount); @@ -188,12 +175,12 @@ export function useComponentState({ const totalFee = fee !== undefined ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount - : Amounts.zeroOfCurrency(currency); + : zero; const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount); const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount); - const isDirty = instructed.amount !== initialValue; + const isDirty = instructed.amount !== zero; const amountError = !isDirty ? undefined : Amounts.cmp(balance, totalEffective) === -1 @@ -206,7 +193,7 @@ export function useComponentState({ amountError !== undefined; //amount field may be invalid async function doSend(): Promise<void> { - if (!currency) return; + // if (!currency) return; const depositPaytoUri = stringifyPaytoUri(currentAccount); const amountStr = Amounts.stringify(totalEffective); @@ -214,13 +201,13 @@ export function useComponentState({ amount: amountStr, depositPaytoUri, }); - onSuccess(currency); + onSuccess(scope!); } return { status: "ready", error: undefined, - currency, + currency: scope.currency, amount: { value: totalEffective, onInput: pushAlertOnError(async (a) => setInstructed({ @@ -250,7 +237,7 @@ export function useComponentState({ currentAccount, cancelHandler: { onClick: pushAlertOnError(async () => { - onCancel(currency); + onCancel(scope!); }), }, depositHandler: { diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 1144095e1..a96f09553 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -24,6 +24,7 @@ import { Amounts, AmountString, parsePaytoUri, + ScopeInfo, ScopeType, stringifyPaytoUri } from "@gnu-taler/taler-util"; @@ -36,21 +37,26 @@ import { createWalletApiMock } from "../../test-utils.js"; import { useComponentState } from "./state.js"; const currency = "EUR"; -const amount = `${currency}:0`; +const amount = Amounts.parseOrThrow(`${currency}:0`); const withoutFee = (value: number): AmountResponse => ({ effectiveAmount: `${currency}:${value}` as AmountString, rawAmount: `${currency}:${value}` as AmountString, }); +const defaultScope: ScopeInfo = { + type: ScopeType.Global, + currency +} + + const withSomeFee = (value: number, fee: number): AmountResponse => ({ effectiveAmount: `${currency}:${value}` as AmountString, rawAmount: `${currency}:${value - fee}` as AmountString, }); - describe("DepositPage states", () => { it("should have status 'no-enough-balance' when balance is empty", async () => { const { handler, TestingContext } = createWalletApiMock(); - const props = { amount, onCancel: nullFunction, onSuccess: nullFunction }; + const props = { scope: defaultScope, amount, onCancel: nullFunction, onSuccess: nullFunction }; handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { balances: [ @@ -61,11 +67,7 @@ describe("DepositPage states", () => { pendingIncoming: `${currency}:0` as AmountString, pendingOutgoing: `${currency}:0` as AmountString, requiresUserInput: false, - scopeInfo: { - currency, - type: ScopeType.Auditor, - url: "asd", - }, + scopeInfo: defaultScope, }, ], }); @@ -97,7 +99,7 @@ describe("DepositPage states", () => { it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => { const { handler, TestingContext } = createWalletApiMock(); - const props = { amount, onCancel: nullFunction, onSuccess: nullFunction }; + const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction }; handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { balances: [ @@ -108,11 +110,7 @@ describe("DepositPage states", () => { pendingIncoming: `${currency}:0` as AmountString, pendingOutgoing: `${currency}:0` as AmountString, requiresUserInput: false, - scopeInfo: { - currency, - type: ScopeType.Auditor, - url: "asd", - }, + scopeInfo: defaultScope, }, ], }); @@ -157,7 +155,7 @@ describe("DepositPage states", () => { it("should have status 'ready' but unable to deposit ", async () => { const { handler, TestingContext } = createWalletApiMock(); - const props = { amount, onCancel: nullFunction, onSuccess: nullFunction }; + const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction }; handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { balances: [ @@ -168,11 +166,7 @@ describe("DepositPage states", () => { pendingIncoming: `${currency}:0` as AmountString, pendingOutgoing: `${currency}:0` as AmountString, requiresUserInput: false, - scopeInfo: { - currency, - type: ScopeType.Auditor, - url: "asd", - }, + scopeInfo: defaultScope, }, ], }); @@ -217,7 +211,7 @@ describe("DepositPage states", () => { it("should not be able to deposit more than the balance ", async () => { const { handler, TestingContext } = createWalletApiMock(); - const props = { amount, onCancel: nullFunction, onSuccess: nullFunction }; + const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction }; handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { balances: [ @@ -228,11 +222,7 @@ describe("DepositPage states", () => { pendingIncoming: `${currency}:0` as AmountString, pendingOutgoing: `${currency}:0` as AmountString, requiresUserInput: false, - scopeInfo: { - currency, - type: ScopeType.Auditor, - url: "asd", - }, + scopeInfo: defaultScope, }, ], }); @@ -307,7 +297,7 @@ describe("DepositPage states", () => { it("should calculate the fee upon entering amount ", async () => { const { handler, TestingContext } = createWalletApiMock(); - const props = { amount, onCancel: nullFunction, onSuccess: nullFunction }; + const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction }; handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { balances: [ @@ -318,11 +308,7 @@ describe("DepositPage states", () => { pendingIncoming: `${currency}:0` as AmountString, pendingOutgoing: `${currency}:0` as AmountString, requiresUserInput: false, - scopeInfo: { - currency, - type: ScopeType.Auditor, - url: "asd", - }, + scopeInfo: defaultScope, }, ], }); diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts index b56fe5523..eeb972c08 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts @@ -14,15 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AmountJson, KnownBankAccountsInfo, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; import { AmountFieldHandler, - ButtonHandler, - ToggleHandler, + ButtonHandler } from "../../mui/handlers.js"; -import { compose, StateViewMap } from "../../utils/index.js"; +import { StateViewMap, compose } from "../../utils/index.js"; import { useComponentState } from "./state.js"; import { ReadyView, SelectCurrencyView } from "./views.js"; @@ -30,15 +30,16 @@ export type Props = PropsGet | PropsSend; interface PropsGet { type: "get"; - amount?: string; - goToWalletManualWithdraw: (amount: string) => void; - goToWalletWalletInvoice: (amount: string) => void; + scope?: ScopeInfo; + goToWalletManualWithdraw: (s:ScopeInfo) => void; + goToWalletWalletInvoice: (s:ScopeInfo) => void; } interface PropsSend { type: "send"; - amount?: string; - goToWalletBankDeposit: (amount: string) => void; - goToWalletWalletSend: (amount: string) => void; + scope: ScopeInfo; + goToWalletKnownBankDeposit: (s:ScopeInfo, p: PaytoUri) => void; + goToWalletNewBankDeposit: (s:ScopeInfo) => void; + goToWalletWalletSend: (s:ScopeInfo) => void; } export type State = @@ -69,20 +70,13 @@ export namespace State { status: "ready"; error: undefined; type: Props["type"]; - selectCurrency: ButtonHandler; - selectMax: ButtonHandler; - previous: Contact[]; + onSelectAccount: (p:PaytoUri) => void; + previous: KnownBankAccountsInfo[]; goToBank: ButtonHandler; goToWallet: ButtonHandler; - amountHandler: AmountFieldHandler; } } -export type Contact = { - icon_type: string; - name: string; - description: string; -}; const viewMapping: StateViewMap<State> = { loading: Loading, diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts index d4e270a6c..de2d439b6 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts @@ -14,7 +14,14 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts } from "@gnu-taler/taler-util"; +import { + ExchangeUpdateStatus, + KnownBankAccountsInfo, + PaytoUri, + ScopeType, + parseScopeInfoShort, + stringifyScopeInfoShort +} 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"; @@ -22,61 +29,46 @@ import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { RecursiveState, assertUnreachable } from "../../utils/index.js"; -import { Contact, Props, State } from "./index.js"; +import { Props, State } from "./index.js"; export function useComponentState(props: Props): RecursiveState<State> { const api = useBackendContext(); const { pushAlertOnError } = useAlertContext(); + const { i18n } = useTranslationContext(); - const parsedInitialAmount = !props.amount - ? undefined - : Amounts.parse(props.amount); + const [scope, setScope] = useState(props.scope); const hook = useAsyncAsHook(async () => { - if (!parsedInitialAmount) return undefined; - const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, { - currency: parsedInitialAmount.currency, - }); - return { balance }; + const resp = await api.wallet.call( + WalletApiOperation.ListKnownBankAccounts, + {}, + ); + return resp }); - const info = hook && !hook.hasError ? hook.response : undefined; + const previous: KnownBankAccountsInfo[] = props.type === "send" && hook && !hook.hasError ? hook.response.accounts : []; - // const initialCurrency = parsedInitialAmount?.currency; - - const [amount, setAmount] = useState( - !parsedInitialAmount ? undefined : parsedInitialAmount, - ); - //FIXME: get this information from wallet - // eslint-disable-next-line no-constant-condition - const previous: Contact[] = true - ? [] - : [ - { - name: "International Bank", - icon_type: "bank", - description: "account ending with 3454", - }, - { - name: "Max", - icon_type: "bank", - description: "account ending with 3454", - }, - { - name: "Alex", - icon_type: "bank", - description: "account ending with 3454", - }, - ]; - - if (!amount) { + if (!scope) { return () => { - // eslint-disable-next-line react-hooks/rules-of-hooks const { i18n } = useTranslationContext(); - // eslint-disable-next-line react-hooks/rules-of-hooks - const hook = useAsyncAsHook(() => - api.wallet.call(WalletApiOperation.ListExchanges, {}), - ); + const hook = useAsyncAsHook(async () => { + const resp = await api.wallet.call( + WalletApiOperation.ListExchanges, + {}, + ); + + const unknownIndex = resp.exchanges.findIndex( + (d) => d.exchangeUpdateStatus === ExchangeUpdateStatus.Initial, + ); + if (unknownIndex === -1) return resp; + + await api.wallet.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: resp.exchanges[unknownIndex].exchangeBaseUrl, + force: true, + }); + + return await api.wallet.call(WalletApiOperation.ListExchanges, {}); + }); if (!hook) { return { @@ -87,14 +79,30 @@ export function useComponentState(props: Props): RecursiveState<State> { if (hook.hasError) { return { status: "error", - error: alertFromError(i18n, - i18n.str`Could not load exchanges`, hook), + error: alertFromError(i18n, i18n.str`Could not load exchanges`, hook), }; } const currencies: Record<string, string> = {}; - hook.response.exchanges.forEach((e) => { - if (e.currency) { - currencies[e.currency] = e.currency; + hook.response.exchanges.forEach((b) => { + switch (b.scopeInfo.type) { + case ScopeType.Global: { + currencies[stringifyScopeInfoShort(b.scopeInfo)] = + b.scopeInfo.currency; + break; + } + case ScopeType.Exchange: { + currencies[stringifyScopeInfoShort(b.scopeInfo)] = + `${b.scopeInfo.currency} ${b.scopeInfo.url}`; + break; + } + case ScopeType.Auditor: { + currencies[stringifyScopeInfoShort(b.scopeInfo)] = + `${b.scopeInfo.currency} ${b.scopeInfo.url}`; + break; + } + default: { + assertUnreachable(b.scopeInfo); + } } }); currencies[""] = "Select a currency"; @@ -103,55 +111,32 @@ export function useComponentState(props: Props): RecursiveState<State> { status: "select-currency", error: undefined, onCurrencySelected: (c: string) => { - setAmount(Amounts.zeroOfCurrency(c)); + const scope = parseScopeInfoShort(c); + setScope(scope); }, currencies, }; }; } - const currencyAndAmount = Amounts.stringify(amount); - const invalid = Amounts.isZero(amount); - switch (props.type) { case "send": return { status: "ready", error: undefined, previous, - selectCurrency: { - onClick: pushAlertOnError(async () => { - setAmount(undefined); - }), - }, + onSelectAccount: pushAlertOnError(async (account: PaytoUri) => { + props.goToWalletKnownBankDeposit(scope, account); + }), goToBank: { - onClick: invalid - ? undefined - : pushAlertOnError(async () => { - props.goToWalletBankDeposit(currencyAndAmount); - }), - }, - selectMax: { onClick: pushAlertOnError(async () => { - const resp = await api.wallet.call( - WalletApiOperation.GetMaxDepositAmount, - { - currency: amount.currency, - }, - ); - setAmount(Amounts.parseOrThrow(resp.effectiveAmount)); + props.goToWalletNewBankDeposit(scope); }), }, goToWallet: { - onClick: invalid - ? undefined - : pushAlertOnError(async () => { - props.goToWalletWalletSend(currencyAndAmount); - }), - }, - amountHandler: { - onInput: pushAlertOnError(async (s) => setAmount(s)), - value: amount, + onClick: pushAlertOnError(async () => { + props.goToWalletWalletSend(scope); + }), }, type: props.type, }; @@ -160,35 +145,16 @@ export function useComponentState(props: Props): RecursiveState<State> { status: "ready", error: undefined, previous, - selectCurrency: { + goToBank: { onClick: pushAlertOnError(async () => { - setAmount(undefined); + props.goToWalletManualWithdraw(scope); }), }, - selectMax: { - onClick: invalid - ? undefined - : pushAlertOnError(async () => { - props.goToWalletManualWithdraw(currencyAndAmount); - }), - }, - goToBank: { - onClick: invalid - ? undefined - : pushAlertOnError(async () => { - props.goToWalletManualWithdraw(currencyAndAmount); - }), - }, + onSelectAccount: () => { }, goToWallet: { - onClick: invalid - ? undefined - : pushAlertOnError(async () => { - props.goToWalletWalletInvoice(currencyAndAmount); - }), - }, - amountHandler: { - onInput: pushAlertOnError(async (s) => setAmount(s)), - value: amount, + onClick: pushAlertOnError(async () => { + props.goToWalletWalletInvoice(scope); + }), }, type: props.type, }; diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx index e1ac958f7..c530a7020 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx @@ -27,33 +27,15 @@ export default { }; export const GetCash = tests.createExample(ReadyView, { - amountHandler: { - value: { - currency: "EUR", - fraction: 0, - value: 2, - }, - }, goToBank: {}, - selectMax: {}, goToWallet: {}, previous: [], - selectCurrency: {}, type: "get", }); export const SendCash = tests.createExample(ReadyView, { - amountHandler: { - value: { - currency: "EUR", - fraction: 0, - value: 1, - }, - }, - selectMax: {}, goToBank: {}, goToWallet: {}, previous: [], - selectCurrency: {}, type: "send", }); diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts index 683378613..9e75f0b6f 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts @@ -25,7 +25,8 @@ import { ExchangeListItem, ExchangeTosStatus, ExchangeUpdateStatus, - ScopeType, + ScopeInfo, + ScopeType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as tests from "@gnu-taler/web-util/testing"; @@ -34,14 +35,15 @@ import { nullFunction } from "../../mui/handlers.js"; import { createWalletApiMock } from "../../test-utils.js"; import { useComponentState } from "./state.js"; +const currency = "ARS"; const exchangeArs: ExchangeListItem = { - currency: "ARS", - exchangeBaseUrl: "http://", + currency, + exchangeBaseUrl: "http://exchange.test.taler.net", masterPub: "123qwe123", scopeInfo: { - currency: "ARS", + currency, type: ScopeType.Exchange, - url: "http://", + url: "http://exchange.test.taler.net", }, tosStatus: ExchangeTosStatus.Accepted, exchangeEntryStatus: ExchangeEntryStatus.Used, @@ -54,19 +56,20 @@ const exchangeArs: ExchangeListItem = { }; describe("Destination selection states", () => { - it("should select currency if no amount specified", async () => { + it.skip("should select currency if no amount specified", async () => { const { handler, TestingContext } = createWalletApiMock(); - handler.addWalletCallResponse( - WalletApiOperation.ListExchanges, - {}, - { - exchanges: [exchangeArs], - }, - ); + handler.addWalletCallResponse(WalletApiOperation.ListExchanges, undefined, { + exchanges: [exchangeArs], + }); const props = { type: "get" as const, + // scope: { + // currency: "ARS", + // type: ScopeType.Exchange, + // url: "http://asd.com", + // } as ScopeInfo, goToWalletManualWithdraw: nullFunction, goToWalletWalletInvoice: nullFunction, }; @@ -82,7 +85,8 @@ describe("Destination selection states", () => { if (state.status !== "select-currency") expect.fail(); if (state.error) expect.fail(); expect(state.currencies).deep.eq({ - ARS: "ARS", + "ARS/http%3A%2F%2Fexchange.test.taler.net": + "ARS http://exchange.test.taler.net", "": "Select a currency", }); @@ -94,9 +98,6 @@ describe("Destination selection states", () => { expect(state.goToBank.onClick).eq(undefined); expect(state.goToWallet.onClick).eq(undefined); - expect(state.amountHandler.value).deep.eq( - Amounts.parseOrThrow("ARS:0"), - ); }, ], TestingContext, @@ -106,14 +107,19 @@ describe("Destination selection states", () => { expect(handler.getCallingQueueState()).eq("empty"); }); - it("should be possible to start with an amount specified in request params", async () => { + it.skip("should be possible to start with an amount specified in request params", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { type: "get" as const, + scope: { + currency: "ARS", + type: ScopeType.Exchange, + url: "http://asd.com", + } as ScopeInfo, goToWalletManualWithdraw: nullFunction, goToWalletWalletInvoice: nullFunction, - amount: "ARS:2", + amount: Amounts.parseOrThrow("ARS:2"), }; const hookBehavior = await tests.hookBehaveLikeThis( @@ -129,19 +135,6 @@ describe("Destination selection states", () => { expect(state.goToBank.onClick).not.eq(undefined); expect(state.goToWallet.onClick).not.eq(undefined); - expect(state.amountHandler.value).deep.eq( - Amounts.parseOrThrow("ARS:2"), - ); - }, - (state) => { - if (state.status !== "ready") expect.fail(); - if (state.error) expect.fail(); - expect(state.goToBank.onClick).not.eq(undefined); - expect(state.goToWallet.onClick).not.eq(undefined); - - expect(state.amountHandler.value).deep.eq( - Amounts.parseOrThrow("ARS:2"), - ); }, ], TestingContext, diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx index 8a74a20f1..cf34ceb35 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx @@ -14,11 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { KnownBankAccountsInfo, PaytoUri, stringifyPaytoUri } 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 { AmountField } from "../../components/AmountField.js"; -import { EnabledBySettings } from "../../components/EnabledBySettings.js"; import { SelectList } from "../../components/SelectList.js"; import { Input, @@ -33,7 +32,7 @@ import { Pages } from "../../NavigationBar.js"; import arrowIcon from "../../svg/chevron-down.inline.svg"; import bankIcon from "../../svg/ri-bank-line.inline.svg"; import { assertUnreachable } from "../../utils/index.js"; -import { Contact, State } from "./index.js"; +import { State } from "./index.js"; export function SelectCurrencyView({ currencies, @@ -62,7 +61,7 @@ export function SelectCurrencyView({ </p> <div style={{ display: "flex", justifyContent: "space-between" }}> <div /> - <LinkPrimary href={Pages.settingsExchangeAdd({})}> + <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}> <i18n.Translate>Add an exchange</i18n.Translate> </LinkPrimary> </div> @@ -81,7 +80,6 @@ const Container = styled.div` const ContactTable = styled.table` width: 100%; & > tr > td { - padding: 8px; & > div:not([data-disabled]):hover { background-color: lightblue; } @@ -192,10 +190,8 @@ export function ReadyView(props: State.Ready): VNode { } } export function ReadyGetView({ - amountHandler, goToBank, goToWallet, - selectCurrency, previous, }: State.Ready): VNode { const { i18n } = useTranslationContext(); @@ -203,19 +199,8 @@ export function ReadyGetView({ return ( <Container> <h1> - <i18n.Translate>Specify the amount and the origin</i18n.Translate> + <i18n.Translate>Specify the origin</i18n.Translate> </h1> - <Grid container columns={2} justifyContent="space-between"> - <AmountField - label={i18n.str`Amount`} - required - handler={amountHandler} - /> - - <Button onClick={selectCurrency.onClick}> - <i18n.Translate>Change currency</i18n.Translate> - </Button> - </Grid> <Grid container spacing={1} columns={1}> {previous.length > 0 ? ( @@ -231,7 +216,7 @@ export function ReadyGetView({ <td> <RowExample info={info} - disabled={!amountHandler.onInput} + // disabled={!amountHandler.onInput} /> </td> </tr> @@ -241,26 +226,11 @@ export function ReadyGetView({ </Grid> </Fragment> ) : undefined} - {previous.length > 0 ? ( - <Grid item> - <p> - <i18n.Translate> - Or specify the origin of the money - </i18n.Translate> - </p> - </Grid> - ) : ( - <Grid item> - <p> - <i18n.Translate>Specify the origin of the money</i18n.Translate> - </p> - </Grid> - )} <Grid item container columns={2} spacing={1}> <Grid item xs={1}> <Paper style={{ padding: 8 }}> <p> - <i18n.Translate>From my bank account</i18n.Translate> + <i18n.Translate>From another bank account</i18n.Translate> </p> <Button onClick={goToBank.onClick}> <i18n.Translate>Withdraw</i18n.Translate> @@ -280,7 +250,7 @@ export function ReadyGetView({ <Grid item xs={1}> <Paper style={{ padding: 8 }}> <p> - <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate> + <i18n.Translate>From a <pre style={{display:"inline"}}>taler://</pre> URI or QR code</i18n.Translate> </p> <a href={Pages.qr}> <i18n.Translate>Enter URI here</i18n.Translate> @@ -293,33 +263,19 @@ export function ReadyGetView({ ); } export function ReadySendView({ - amountHandler, goToBank, + onSelectAccount, goToWallet, previous, - selectMax, }: State.Ready): VNode { const { i18n } = useTranslationContext(); return ( <Container> <h1> - <i18n.Translate>Specify the amount and the destination</i18n.Translate> + <i18n.Translate>Specify the destination</i18n.Translate> </h1> - <Grid container columns={2} justifyContent="space-between"> - <AmountField - label={i18n.str`Amount`} - required - handler={amountHandler} - /> - <EnabledBySettings name="advancedMode"> - <Button onClick={selectMax.onClick}> - <i18n.Translate>Send all</i18n.Translate> - </Button> - </EnabledBySettings> - </Grid> - <Grid container spacing={1} columns={1}> {previous.length > 0 ? ( <Fragment> @@ -334,7 +290,10 @@ export function ReadySendView({ <td> <RowExample info={info} - disabled={!amountHandler.onInput} + onClick={() => { + onSelectAccount(info.uri) + }} + // disabled={!amountHandler.onInput} /> </td> </tr> @@ -344,28 +303,11 @@ export function ReadySendView({ </Grid> </Fragment> ) : undefined} - {previous.length > 0 ? ( - <Grid item> - <p> - <i18n.Translate> - Or specify the destination of the money - </i18n.Translate> - </p> - </Grid> - ) : ( - <Grid item> - <p> - <i18n.Translate> - Specify the destination of the money - </i18n.Translate> - </p> - </Grid> - )} <Grid item container columns={2} spacing={1}> <Grid item xs={1}> <Paper style={{ padding: 8 }}> <p> - <i18n.Translate>To my bank account</i18n.Translate> + <i18n.Translate>To another bank account</i18n.Translate> </p> <Button onClick={goToBank.onClick}> <i18n.Translate>Deposit</i18n.Translate> @@ -391,31 +333,31 @@ export function ReadySendView({ function RowExample({ info, disabled, + onClick }: { - info: Contact; + info: KnownBankAccountsInfo; disabled?: boolean; + onClick?: () => void; }): VNode { - const icon = info.icon_type === "bank" ? bankIcon : undefined; + + return ( - <MediaExample data-disabled={disabled}> + <MediaExample data-disabled={disabled} onClick={onClick}> <MediaLeft> <CircleDiv> - {icon !== undefined ? ( <SvgIcon - title={info.name} + title={info.alias} dangerouslySetInnerHTML={{ - __html: icon, + __html: bankIcon, }} color="currentColor" /> - ) : ( - <span>A</span> - )} + </CircleDiv> </MediaLeft> <MediaBody> - <span>{info.name}</span> - <LightText>{info.description}</LightText> + <span>{info.alias}</span> + <LightText>{describeAccount(info.uri)}</LightText> </MediaBody> <MediaRight> <SvgIcon @@ -428,3 +370,21 @@ function RowExample({ </MediaExample> ); } + + +function describeAccount(p: PaytoUri): string { + if (!p.isKnown) { + return stringifyPaytoUri(p) + } + switch (p.targetType) { + case "iban": { + return p.iban + } + case "x-taler-bank": { + return `${p.host}/${p.account}` + } + case "bitcoin": { + return `${p.address}` + } + } +}
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index 9feb03714..9c17d3cff 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -25,7 +25,10 @@ import { stringifyWithdrawExchange, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + encodeCrockForURI, + 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"; @@ -337,7 +340,13 @@ export function DeveloperPage(): VNode { return ( <tr key={idx}> <td> - <a href={!uri ? undefined : Pages.defaultCta({ uri })}> + <a + href={ + !uri + ? undefined + : `#${Pages.defaultCta({ uri: encodeCrockForURI(uri) })}` + } + > {e.scopeInfo ? `${e.scopeInfo.currency} (${ e.scopeInfo.type === ScopeType.Global @@ -461,7 +470,7 @@ export function DeveloperPage(): VNode { )} <div style={{ display: "flex", justifyContent: "space-between" }}> <div /> - <LinkPrimary href={Pages.settingsExchangeAdd({})}> + <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}> <i18n.Translate>Add an exchange</i18n.Translate> </LinkPrimary> </div> @@ -667,6 +676,12 @@ function ShowAllCoins({ ); } +/** + * + * @param str + * @deprecated FIXME: use a better base64 function + * @returns + */ function toBase64(str: string): string { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 482b8d698..470ad0514 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -70,6 +70,7 @@ const exampleData = { reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", confirmed: false, exchangePaytoUris: ["payto://x-taler-bank/bank/account"], + reserveClosingDelay: { d_us: "forever" }, type: WithdrawalType.ManualTransfer, reserveIsReady: false, }, @@ -168,7 +169,12 @@ export const SomeBalanceWithNoTransactions = tests.createExample( transactionsByDate: { "11/11/11": [], }, - balances: [ + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, + balances: [ { available: "TESTKUDOS:10" as AmountString, flags: [], @@ -183,7 +189,7 @@ export const SomeBalanceWithNoTransactions = tests.createExample( }, }, ], - balanceIndex: 0, + }, ); @@ -191,6 +197,11 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, { transactionsByDate: { "11/11/11": [exampleData.withdraw], }, + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, balances: [ { flags: [], @@ -206,7 +217,7 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, { }, }, ], - balanceIndex: 0, + }); export const TwoTransactionsAndZeroBalance = tests.createExample( @@ -215,7 +226,12 @@ export const TwoTransactionsAndZeroBalance = tests.createExample( transactionsByDate: { "11/11/11": [exampleData.withdraw, exampleData.deposit], }, - balances: [ + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, + balances: [ { flags: [], available: "USD:0" as AmountString, @@ -230,7 +246,7 @@ export const TwoTransactionsAndZeroBalance = tests.createExample( }, }, ], - balanceIndex: 0, + }, ); @@ -245,6 +261,11 @@ export const OneTransactionPending = tests.createExample(TestedComponent, { }, ], }, + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, balances: [ { flags: [], @@ -260,7 +281,7 @@ export const OneTransactionPending = tests.createExample(TestedComponent, { }, }, ], - balanceIndex: 0, + }); export const SomeTransactions = tests.createExample(TestedComponent, { @@ -282,6 +303,11 @@ export const SomeTransactions = tests.createExample(TestedComponent, { exampleData.deposit, ], }, + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, balances: [ { flags: [], @@ -297,7 +323,7 @@ export const SomeTransactions = tests.createExample(TestedComponent, { }, }, ], - balanceIndex: 0, + }); export const SomeTransactionsInDifferentStates = tests.createExample( @@ -378,7 +404,12 @@ export const SomeTransactionsInDifferentStates = tests.createExample( exampleData.deposit, ], }, - balances: [ + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, + balances: [ { flags: [], available: "USD:10" as AmountString, @@ -393,7 +424,7 @@ export const SomeTransactionsInDifferentStates = tests.createExample( }, }, ], - balanceIndex: 0, + }, ); @@ -411,7 +442,12 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample( exampleData.deposit, ], }, - balances: [ + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, + balances: [ { flags: [], available: "USD:0" as AmountString, @@ -439,7 +475,7 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample( }, }, ], - balanceIndex: 0, + }, ); @@ -447,6 +483,11 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, { transactionsByDate: { "11/11/11": [exampleData.withdraw], }, + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, balances: [ { flags: [], @@ -514,7 +555,7 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, { }, }, ], - balanceIndex: 0, + }); export const FiveOfficialCurrenciesWithHighValue = tests.createExample( @@ -523,7 +564,12 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample( transactionsByDate: { "11/11/11": [exampleData.withdraw], }, - balances: [ + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, + balances: [ { flags: [], available: "USD:881001321230000" as AmountString, @@ -590,7 +636,7 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample( }, }, ], - balanceIndex: 0, + }, ); @@ -603,6 +649,11 @@ export const PeerToPeer = tests.createExample(TestedComponent, { exampleData.push_debit, ], }, + scope: { + currency: "Ásd", + type: ScopeType.Auditor, + url: "", + }, balances: [ { flags: [], @@ -618,5 +669,4 @@ export const PeerToPeer = tests.createExample(TestedComponent, { }, }, ], - balanceIndex: 0, }); diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index f81e6db9f..d67293920 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -18,9 +18,11 @@ import { AbsoluteTime, Amounts, NotificationType, + ScopeInfo, ScopeType, Transaction, WalletBalance, + stringifyScopeInfoShort, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -49,35 +51,34 @@ import { TextField } from "../mui/TextField.js"; import { TextFieldHandler } from "../mui/handlers.js"; interface Props { - currency?: string; + scope?: ScopeInfo; search?: boolean; - goToWalletDeposit: (currency: string) => Promise<void>; - goToWalletManualWithdraw: (currency?: string) => Promise<void>; + goToWalletDeposit: (scope: ScopeInfo) => Promise<void>; + goToWalletManualWithdraw: (scope?: ScopeInfo) => Promise<void>; } export function HistoryPage({ - currency: _c, + scope, search: showSearch, goToWalletManualWithdraw, goToWalletDeposit, }: Props): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); - const [balanceIndex, setBalanceIndex] = useState<number>(0); + const [selectedScope, setSelectedScope] = useState(scope); const [search, setSearch] = useState<string>(); const [settings] = useSettings(); const state = useAsyncAsHook(async () => { const b = await api.wallet.call(WalletApiOperation.GetBalances, {}); - const balance = - b.balances.length > 0 ? b.balances[balanceIndex] : undefined; + const balances = b.balances; const tx = await api.wallet.call(WalletApiOperation.GetTransactions, { - scopeInfo: showSearch ? undefined : balance?.scopeInfo, + scopeInfo: showSearch ? undefined : selectedScope, sort: "descending", includeRefreshes: settings.showRefeshTransactions, search, }); - return { b, tx }; - }, [balanceIndex, search]); + return { balances, transactions: tx.transactions }; + }, [selectedScope, search]); useEffect(() => { return api.listener.onUpdateNotification( @@ -103,17 +104,17 @@ export function HistoryPage({ ); } - if (!state.response.b.balances.length) { + if (!state.response.balances.length) { return ( <NoBalanceHelp goToWalletManualWithdraw={{ - onClick: pushAlertOnError(goToWalletManualWithdraw), + onClick: pushAlertOnError(() => goToWalletManualWithdraw(selectedScope)), }} /> ); } - const byDate = state.response.tx.transactions.reduce( + const txsByDate = state.response.transactions.reduce( (rv, x) => { const startDay = x.timestamp.t_s === "never" @@ -141,41 +142,48 @@ export function HistoryPage({ setSearch(d); }), }} - transactionsByDate={byDate} + transactionsByDate={txsByDate} /> ); } return ( <HistoryView - balanceIndex={balanceIndex} - changeBalanceIndex={(b) => setBalanceIndex(b)} - balances={state.response.b.balances} + scope={selectedScope ?? state.response.balances[0].scopeInfo} + changeScope={(b) => setSelectedScope(b)} + balances={state.response.balances} goToWalletManualWithdraw={goToWalletManualWithdraw} goToWalletDeposit={goToWalletDeposit} - transactionsByDate={byDate} + transactionsByDate={txsByDate} /> ); } export function HistoryView({ balances, - balanceIndex, - changeBalanceIndex, + scope, + changeScope, transactionsByDate, goToWalletManualWithdraw, goToWalletDeposit, }: { - balanceIndex: number; - changeBalanceIndex: (s: number) => void; - goToWalletDeposit: (currency: string) => Promise<void>; - goToWalletManualWithdraw: (currency?: string) => Promise<void>; + scope: ScopeInfo; + changeScope: (scope: ScopeInfo) => void; + goToWalletDeposit: (scope: ScopeInfo) => Promise<void>; + goToWalletManualWithdraw: (scope: ScopeInfo) => Promise<void>; transactionsByDate: Record<string, Transaction[]>; balances: WalletBalance[]; }): VNode { const { i18n } = useTranslationContext(); + const scopeStr = stringifyScopeInfoShort(scope); + const balanceIndex = balances.findIndex( + (b) => stringifyScopeInfoShort(b.scopeInfo) === scopeStr, + ); const balance = balances[balanceIndex]; + if (!balance) { + return <div>unknown scope</div>; + } const available = balance ? Amounts.jsonifyAmount(balance.available) @@ -200,9 +208,7 @@ export function HistoryView({ tooltip="Transfer money to the wallet" startIcon={DownloadIcon} variant="contained" - onClick={() => - goToWalletManualWithdraw(balance.scopeInfo.currency) - } + onClick={() => goToWalletManualWithdraw(balance.scopeInfo)} > <i18n.Translate>Receive</i18n.Translate> </Button> @@ -212,7 +218,7 @@ export function HistoryView({ startIcon={UploadIcon} variant="outlined" color="primary" - onClick={() => goToWalletDeposit(balance.scopeInfo.currency)} + onClick={() => goToWalletDeposit(balance.scopeInfo)} > <i18n.Translate>Send</i18n.Translate> </Button> @@ -238,9 +244,8 @@ export function HistoryView({ }} value={balanceIndex} onChange={(e) => { - changeBalanceIndex( - Number.parseInt(e.currentTarget.value, 10), - ); + const bIdx = Number.parseInt(e.currentTarget.value, 10); + changeScope(balances[bIdx].scopeInfo); }} > {balances.map((entry, index) => { diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts index 3a00d48ce..a76d77709 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { KnownBankAccountsInfo } from "@gnu-taler/taler-util"; +import { KnownBankAccountsInfo, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -28,8 +28,8 @@ import { useComponentState } from "./state.js"; import { ReadyView } from "./views.js"; export interface Props { - currency: string; - onAccountAdded: (uri: string) => void; + scope: ScopeInfo; + onAccountAdded: (uri: PaytoUri) => void; onCancel: () => void; } diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts index a7b2fe90f..72727ec64 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts @@ -26,38 +26,31 @@ import { useBackendContext } from "../../context/backend.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { AccountByType, Props, State } from "./index.js"; -import { useSettings } from "../../hooks/useSettings.js"; export function useComponentState({ - currency, + scope, onAccountAdded, onCancel, }: Props): State { const api = useBackendContext(); const { pushAlertOnError } = useAlertContext(); const { i18n } = useTranslationContext(); + const hook = useAsyncAsHook(() => - api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }), + api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency: scope.currency }), ); - const accountType: Record<string, string> = { - iban: "IBAN", - }; - const [settings] = useSettings(); - if (settings.extendedAccountTypes) { - accountType["bitcoin"] = "Bitcoin"; - accountType["x-taler-bank"] = "Taler Bank"; - } - const [payto, setPayto] = useState(""); - const [alias, setAlias] = useState(""); - const [type, setType] = useState("iban"); + const hook2 = useAsyncAsHook(() => + api.wallet.call(WalletApiOperation.GetDepositWireTypesForCurrency, { currency: scope.currency, scopeInfo: scope }), + ); - if (!hook) { + if (!hook || !hook2) { return { status: "loading", error: undefined, }; } + if (hook.hasError) { return { status: "error", @@ -68,6 +61,44 @@ export function useComponentState({ }; } + if (hook2.hasError) { + return { + status: "error", + error: alertFromError( + i18n, + i18n.str`Could not load supported wire methods`, + hook2), + }; + } + + if (hook2.response.wireTypes.length === 0) { + return { + status: "error", + error: { + type: "error", + message: i18n.str`No wire methods supported for this currency`, + description: i18n.str``, + cause: new Error("something"), + context: {}, + }, + }; + } + + const [payto, setPayto] = useState(""); + const [alias, setAlias] = useState(""); + const [type, setType] = useState(hook2.response.wireTypes[0]); + + const accountType: Record<string, string> = {}; + hook2.response.wireTypes.forEach(t => { + if (t === "iban") { + accountType[t] = "IBAN" + } else if (t === "x-taler-bank") { + accountType[t] = "x-taler-bank" + } else if (t === "bitcoin") { + accountType[t] = "Bitcoin" + } + }); + const uri = parsePaytoUri(payto); const found = hook.response.accounts.findIndex( @@ -80,10 +111,10 @@ export function useComponentState({ const normalizedPayto = stringifyPaytoUri(uri); await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, { alias, - currency, + currency: scope.currency, payto: normalizedPayto, }); - onAccountAdded(payto); + onAccountAdded(uri); } const paytoUriError = found ? "that account is already present" : undefined; @@ -112,7 +143,7 @@ export function useComponentState({ return { status: "ready", error: undefined, - currency, + currency:scope.currency, accountType: { list: accountType, value: type, diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx index 03a08016a..d205b2fa9 100644 --- a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx @@ -104,7 +104,7 @@ function NotificationItem({ return ( <NotificationLayout timestamp={timestamp} - href={Pages.balanceTransaction({ tid: info.transactionId })} + href={`#${Pages.balanceTransaction({ tid: info.transactionId })}`} title="Withdrawal on hold" subtitle="Know-your-customer validation is required" iconPath={"K"} @@ -115,7 +115,7 @@ function NotificationItem({ return ( <NotificationLayout timestamp={timestamp} - href={Pages.balanceTransaction({ tid: info.transactionId })} + href={`#${Pages.balanceTransaction({ tid: info.transactionId })}`} title="Merchant has refund your payment" subtitle="Accept or deny refund" iconPath={"K"} @@ -126,7 +126,7 @@ function NotificationItem({ return ( <NotificationLayout timestamp={timestamp} - href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`} + href={`#${Pages.ctaPay}?talerPayUri=${info.talerUri}`} title="Backup provider is unpaid" subtitle="Complete the payment or remove the service provider" iconPath={"K"} diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx index a01ea6967..9635cd077 100644 --- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx +++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx @@ -216,7 +216,7 @@ export function QrReaderPage({ onDetected }: Props): VNode { function onChangeDetect(str: string) { if (str) { - const uri = parseTalerUri(str); + const uri = parseTalerUri(str.toLowerCase()); if (!uri) { setError( i18n.str`URI is not valid. Taler URI should start with "taler://"`, @@ -233,7 +233,7 @@ export function QrReaderPage({ onDetected }: Props): VNode { function onChange(str: string) { if (str) { - if (!parseTalerUri(str)) { + if (!parseTalerUri(str.toLowerCase())) { setError( i18n.str`URI is not valid. Taler URI should start with "taler://"`, ); @@ -293,7 +293,7 @@ export function QrReaderPage({ onDetected }: Props): VNode { setError(i18n.str`something unexpected happen: ${error}`); } } - const uri = parseTalerUri(value); + const uri = parseTalerUri(value.toLowerCase()); return ( <Container> diff --git a/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx b/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx new file mode 100644 index 000000000..e2388c961 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx @@ -0,0 +1,60 @@ +/* + 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 { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { ErrorAlertView } from "../components/CurrentAlerts.js"; +import { alertFromError } from "../context/alert.js"; +import { useBackendContext } from "../context/backend.js"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; + +interface Props { + account: PaytoUri; +} + +export function SupportedBanksForAccount({ account }: Props): VNode { + const api = useBackendContext(); + const { i18n } = useTranslationContext(); + const state = useAsyncAsHook(() => { + return api.wallet.call(WalletApiOperation.GetBankingChoicesForPayto, { + paytoUri: stringifyPaytoUri(account), + }); + }); + if (!state) { + return <Loading />; + } + + if (state.hasError) { + return ( + <ErrorAlertView + error={alertFromError( + i18n, + i18n.str`Could not bank choices for account`, + state, + )} + /> + ); + } + + return ( + <div> + {state.response.choices.map((ch) => { + return <a href={ch.uri}>{ch.label}</a>; + })} + </div> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index 194f0e0bb..94d7e4c61 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -41,7 +41,7 @@ import { TransactionType, TransactionWithdrawal, WithdrawalDetails, - WithdrawalType + WithdrawalType, } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import beer from "../../static-dev/beer.png"; @@ -61,6 +61,7 @@ export default { const commonTransaction: TransactionCommon = { error: undefined, amountRaw: "KUDOS:11" as AmountString, + scopes: [], amountEffective: "KUDOS:9.2" as AmountString, txState: { major: TransactionMajorState.Done, @@ -86,6 +87,7 @@ const exampleData = { confirmed: false, reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"], + reserveClosingDelay: { d_us: "forever" }, type: WithdrawalType.ManualTransfer, }, } as TransactionWithdrawal, @@ -281,12 +283,17 @@ export const WithdrawPendingManual = tests.createExample( type: WithdrawalType.ManualTransfer, exchangePaytoUris: ["payto://iban/ES8877998399652238"], reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", - exchangeCreditAccountDetails: [{ - paytoUri: "payto://IBAN/1231231231", + reserveClosingDelay: { + d_us: 111, }, - { - paytoUri: "payto://IBAN/2342342342", - }], + exchangeCreditAccountDetails: [ + { + paytoUri: "payto://IBAN/1231231231", + }, + { + paytoUri: "payto://IBAN/2342342342", + }, + ], } as WithdrawalDetails, txState: { major: TransactionMajorState.Pending, diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 339ded173..036f75c63 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -20,11 +20,14 @@ import { Amounts, AmountString, DenomLossEventType, + Duration, MerchantInfo, NotificationType, OrderShortInfo, parsePaytoUri, PaytoUri, + ScopeInfo, + ScopeType, stringifyPaytoUri, TalerErrorCode, TalerPreciseTimestamp, @@ -41,7 +44,10 @@ import { WithdrawalType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + encodeCrockForURI, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { styled } from "@linaria/react"; import { isPast } from "date-fns"; import { ComponentChildren, Fragment, h, VNode } from "preact"; @@ -78,7 +84,7 @@ import { assertUnreachable } from "../utils/index.js"; interface Props { tid: string; - goToWalletHistory: (currency?: string) => Promise<void>; + goToWalletHistory: (scope: ScopeInfo) => Promise<void>; } export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { @@ -116,7 +122,13 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { ); } - const currency = Amounts.parse(state.response.amountRaw)?.currency; + const currency = Amounts.parse(state.response.amountEffective)!.currency; + const txScope = !state.response.scopes.length + ? { + type: ScopeType.Global as const, + currency, + } + : state.response.scopes[0]; return ( <TransactionView @@ -125,44 +137,44 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { await api.wallet.call(WalletApiOperation.FailTransaction, { transactionId, }); - goToWalletHistory(currency); + // goToWalletHistory(txScope); }} onSuspend={async () => { await api.wallet.call(WalletApiOperation.SuspendTransaction, { transactionId, }); - goToWalletHistory(currency); + // goToWalletHistory(txScope); }} onResume={async () => { await api.wallet.call(WalletApiOperation.ResumeTransaction, { transactionId, }); - goToWalletHistory(currency); + // goToWalletHistory(txScope); }} onAbort={async () => { await api.wallet.call(WalletApiOperation.AbortTransaction, { transactionId, }); - goToWalletHistory(currency); + // goToWalletHistory(txScope); }} onRetry={async () => { await api.wallet.call(WalletApiOperation.RetryTransaction, { transactionId, }); - goToWalletHistory(currency); + // goToWalletHistory(txScope); }} onDelete={async () => { await api.wallet.call(WalletApiOperation.DeleteTransaction, { transactionId, }); - goToWalletHistory(currency); + goToWalletHistory(txScope); }} onRefund={async (transactionId) => { await api.wallet.call(WalletApiOperation.StartRefundQuery, { transactionId, }); }} - onBack={() => goToWalletHistory(currency)} + onBack={() => goToWalletHistory(txScope)} /> ); } @@ -243,7 +255,9 @@ function TransactionTemplate({ /> ) : undefined} {transaction.txState.major === TransactionMajorState.Pending && - (transaction.txState.minor === TransactionMinorState.KycRequired ? ( + (transaction.txState.minor === TransactionMinorState.KycRequired || + transaction.txState.minor === + TransactionMinorState.BalanceKycRequired ? ( <AlertView alert={{ type: "warning", @@ -268,14 +282,6 @@ function TransactionTemplate({ ), }} /> - ) : transaction.txState.minor === - TransactionMinorState.AmlRequired ? ( - <WarningBox> - <i18n.Translate> - The transaction has been blocked since the account required an - AML check. - </i18n.Translate> - </WarningBox> ) : ( <WarningBox> <div style={{ justifyContent: "center", lineHeight: "25px" }}> @@ -452,8 +458,7 @@ export function TransactionView({ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? [] // : []; const blockedByKycOrAml = - transaction.txState.minor === TransactionMinorState.KycRequired || - transaction.txState.minor === TransactionMinorState.AmlRequired; + transaction.txState.minor === TransactionMinorState.KycRequired; return ( <TransactionTemplate transaction={transaction} @@ -520,6 +525,30 @@ export function TransactionView({ /> } /> + {transaction.txState.major === TransactionMajorState.Aborted && + transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer ? ( + <AlertView + alert={{ + type: "info", + message: i18n.str`Withdrawal incomplete.`, + description: ( + <i18n.Translate> + If you have already sent money to the service provider account + it will wire it back at{" "} + <Time + timestamp={AbsoluteTime.addDuration( + AbsoluteTime.fromPreciseTimestamp(transaction.timestamp), + Duration.fromTalerProtocolDuration( + transaction.withdrawalDetails.reserveClosingDelay, + ), + )} + format="dd MMMM yyyy, HH:mm" + /> + </i18n.Translate> + ), + }} + /> + ) : undefined} </TransactionTemplate> ); } @@ -575,9 +604,9 @@ export function TransactionView({ <i18n.Translate> {<Amount value={r.amountEffective} />}{" "} <a - href={Pages.balanceTransaction({ + href={`#${Pages.balanceTransaction({ tid: r.transactionId, - })} + })}`} > was refunded </a>{" "} @@ -714,9 +743,19 @@ export function TransactionView({ ) : transaction.wireTransferProgress === 0 ? ( <AlertView alert={{ - type: "warning", - message: i18n.str`Wire transfer is not initiated.`, - description: i18n.str` `, + type: "info", + message: i18n.str`Wire transfer still pending.`, + description: ( + <i18n.Translate> + The service provider deadline to make the wire transfer is:{" "} + <Time + timestamp={AbsoluteTime.fromProtocolTimestamp( + transaction.wireTransferDeadline, + )} + format="dd MMMM yyyy, HH:mm" + /> + </i18n.Translate> + ), }} /> ) : transaction.wireTransferProgress === 100 ? ( @@ -743,7 +782,17 @@ export function TransactionView({ alert={{ type: "info", message: i18n.str`Wire transfer in progress.`, - description: i18n.str` `, + description: ( + <i18n.Translate> + The service provider deadline to make the wire transfer is:{" "} + <Time + timestamp={AbsoluteTime.fromProtocolTimestamp( + transaction.wireTransferDeadline, + )} + format="dd MMMM yyyy, HH:mm" + /> + </i18n.Translate> + ), }} /> )} @@ -801,9 +850,9 @@ export function TransactionView({ > {transaction.paymentInfo ? ( <a - href={Pages.balanceTransaction({ + href={`#${Pages.balanceTransaction({ tid: transaction.refundedTransactionId, - })} + })}`} > {transaction.paymentInfo.summary} </a> @@ -1746,7 +1795,7 @@ function TrackingDepositDetails({ ); } -function DepositDetails({ amount }: { amount: AmountWithFee }): VNode { +export function DepositDetails({ amount }: { amount: AmountWithFee }): VNode { const { i18n } = useTranslationContext(); return ( |