diff options
author | Sebastian <sebasjm@gmail.com> | 2024-08-05 13:19:35 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-08-05 13:19:35 -0300 |
commit | 784ec2f7695a1a0dc4eb75145309bde53d87eab9 (patch) | |
tree | f5eaa8a2845b2b545f6c1bf8e4d14277235bfd7c /packages/taler-wallet-webextension | |
parent | bdb151b8c73e526f72274c5a2d09a06c7f881175 (diff) |
use scope info for balance and tx
Diffstat (limited to 'packages/taler-wallet-webextension')
14 files changed, 380 insertions, 226 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index d3a0ab37c..56c08f906 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -39,7 +39,10 @@ import qrIcon from "./svg/qr_code_24px.inline.svg"; import settingsIcon from "./svg/settings_black_24dp.inline.svg"; import warningIcon from "./svg/warning_24px.inline.svg"; import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + encodeCrockForURI, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; /** * List of pages used by the wallet @@ -60,10 +63,7 @@ function replaceAll( ): string { let result = pattern; for (const v in vars) { - result = result.replace( - vars[v], - !values[v] ? "" : encodeURIComponent(values[v]), - ); + result = result.replace(vars[v], !values[v] ? "" : values[v]); } return result; } @@ -94,23 +94,41 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> { return f; } +/** + * taler action and scope info may contain + * character not suitable for URL + */ +type CrockEncodedString = string; + export const Pages = { welcome: "/welcome", balance: "/balance", - balanceHistory: pageDefinition<{ scope?: string }>( + balanceHistory: pageDefinition<{ scope?: CrockEncodedString }>( "/balance/history/:scope?", ), - searchHistory: pageDefinition<{ scope?: string }>( + searchHistory: pageDefinition<{ scope?: CrockEncodedString }>( "/search/history/:scope?", ), - balanceDeposit: pageDefinition<{ amount: string }>( - "/balance/deposit/:amount", - ), balanceTransaction: pageDefinition<{ tid: string }>( "/balance/transaction/:tid", ), - sendCash: pageDefinition<{ scope?: string }>("/destination/send/:scope"), - receiveCash: pageDefinition<{ scope?: string }>("/destination/get/:scope?"), + balanceDeposit: pageDefinition<{ + scope: CrockEncodedString; + amount?: string; + }>("/balance/deposit/:scope/:amount?"), + sendCash: pageDefinition<{ scope: CrockEncodedString; amount?: string }>( + "/destination/send/:scope/:amount?", + ), + // if no scope is specified, then exchange selection page will be shown + receiveCash: pageDefinition<{ scope?: CrockEncodedString; amount?: string }>( + "/destination/get/:scope?/:amount?", + ), + receiveCashForPurchase: pageDefinition<{ id?: string }>( + "/add-for-payment/purchase/:id", + ), + receiveCashForInvoice: pageDefinition<{ id?: string }>( + "/add-for-payment/purchase/:id", + ), dev: "/dev", exchanges: "/exchanges", @@ -127,8 +145,8 @@ export const Pages = { "/settings/exchange/add/:currency?", ), - defaultCta: pageDefinition<{ uri: string }>("/taler-uri/:uri"), - cta: pageDefinition<{ action: string }>("/cta/:action"), + defaultCta: pageDefinition<{ uri: CrockEncodedString }>("/taler-uri/:uri"), + cta: pageDefinition<{ action: CrockEncodedString }>("/cta/:action"), ctaPay: "/cta/pay", ctaPayTemplate: "/cta/pay/template", ctaRecovery: "/cta/recovery", @@ -137,17 +155,21 @@ export const Pages = { ctaDeposit: "/cta/deposit", ctaExperiment: "/cta/experiment", ctaAddExchange: "/cta/add/exchange", - ctaInvoiceCreate: pageDefinition<{ amount?: string }>( - "/cta/invoice/create/:amount?", - ), - ctaTransferCreate: pageDefinition<{ amount?: string }>( - "/cta/transfer/create/:amount?", - ), + ctaInvoiceCreate: pageDefinition<{ + scope: CrockEncodedString; + amount?: string; + }>("/cta/invoice/create/:scope/:amount?"), + ctaTransferCreate: pageDefinition<{ + scope: CrockEncodedString; + amount?: string; + }>("/cta/transfer/create/:scope/:amount?"), ctaInvoicePay: "/cta/invoice/pay", ctaTransferPickup: "/cta/transfer/pickup", - ctaWithdrawManual: pageDefinition<{ amount?: string }>( - "/cta/manual-withdraw/:amount?", - ), + + ctaWithdrawManual: pageDefinition<{ + scope: CrockEncodedString; + amount?: string; + }>("/cta/manual-withdraw/:scope/:amount?"), }; const talerUriActionToPageName: { @@ -178,7 +200,7 @@ export function getPathnameForTalerURI(talerUri: string): string | undefined { typeof Pages[pageName] === "function" ? (Pages[pageName] as any)() : Pages[pageName]; - return `${pageString}?talerUri=${encodeURIComponent(talerUri)}`; + return `${pageString}?talerUri=${encodeCrockForURI(talerUri)}`; } export type PopupNavBarOptions = "balance" | "backup" | "dev"; diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index 276d464a0..3f6708fc0 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -35,6 +35,7 @@ import { Settings, defaultSettings, } from "./api.js"; +import { encodeCrockForURI } from "@gnu-taler/web-util/browser"; const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { isFirefox, @@ -178,54 +179,54 @@ function openWalletURIFromPopup(uri: TalerUri): void { case TalerUriAction.WithdrawExchange: case TalerUriAction.Withdraw: url = chrome.runtime.getURL( - `static/wallet.html#/cta/withdraw?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/withdraw?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.Restore: url = chrome.runtime.getURL( - `static/wallet.html#/cta/recovery?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/recovery?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.Pay: url = chrome.runtime.getURL( - `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`, + `static/wallet.html#/cta/pay?talerUri=${encodeCrockForURI(talerUri)}`, ); break; case TalerUriAction.Refund: url = chrome.runtime.getURL( - `static/wallet.html#/cta/refund?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/refund?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.PayPull: url = chrome.runtime.getURL( - `static/wallet.html#/cta/invoice/pay?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/invoice/pay?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.PayPush: url = chrome.runtime.getURL( - `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.PayTemplate: url = chrome.runtime.getURL( - `static/wallet.html#/cta/pay/template?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/pay/template?talerUri=${encodeCrockForURI( talerUri, )}`, ); break; case TalerUriAction.AddExchange: url = chrome.runtime.getURL( - `static/wallet.html#/cta/add/exchange?talerUri=${encodeURIComponent( + `static/wallet.html#/cta/add/exchange?talerUri=${encodeCrockForURI( talerUri, )}`, ); diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx index 8c732728e..0a88b6537 100644 --- a/packages/taler-wallet-webextension/src/popup/Application.tsx +++ b/packages/taler-wallet-webextension/src/popup/Application.tsx @@ -20,13 +20,12 @@ * @author sebasjm */ -import { - ScopeInfo, - stringifyScopeInfoShort -} from "@gnu-taler/taler-util"; +import { ScopeInfo, stringifyScopeInfoShort } from "@gnu-taler/taler-util"; import { TranslationProvider, useTranslationContext, + encodeCrockForURI, + decodeCrockFromURI, } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { ComponentChildren, Fragment, VNode, h } from "preact"; @@ -63,7 +62,7 @@ function ApplicationView(): VNode { useEffect(() => { if (actionUri) { - route(Pages.cta({ action: encodeURIComponent(actionUri) })); + route(Pages.cta({ action: encodeCrockForURI(actionUri) })); } }, [actionUri]); @@ -86,18 +85,24 @@ function ApplicationView(): VNode { goToURL={redirectToURL} > <BalancePage - goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))} + goToWalletManualWithdraw={(scope: ScopeInfo) => + redirectTo( + Pages.receiveCash({ + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), + }), + ) + } goToWalletDeposit={(scope: ScopeInfo) => redirectTo( Pages.sendCash({ - scope: encodeURIComponent(stringifyScopeInfoShort(scope)), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } goToWalletHistory={(scope: ScopeInfo) => redirectTo( Pages.balanceHistory({ - scope: encodeURIComponent(stringifyScopeInfoShort(scope)), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -114,7 +119,7 @@ function ApplicationView(): VNode { return ( <PopupTemplate goToURL={redirectToURL}> <TalerActionFound - url={decodeURIComponent(action)} + url={decodeCrockFromURI(action)} onDismiss={() => { setDismissed(true); return redirectTo(Pages.balance); diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index a819d50e7..ca64ba5a3 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -42,9 +42,9 @@ import { AddNewActionView } from "../wallet/AddNewActionView.js"; import { NoBalanceHelp } from "./NoBalanceHelp.js"; export interface Props { - goToWalletDeposit: (currency: ScopeInfo) => Promise<void>; - goToWalletHistory: (currency: ScopeInfo) => Promise<void>; - goToWalletManualWithdraw: () => Promise<void>; + goToWalletDeposit: (scope: ScopeInfo) => Promise<void>; + goToWalletHistory: (scope: ScopeInfo) => Promise<void>; + goToWalletManualWithdraw: (scope: ScopeInfo) => Promise<void>; } export type State = State.Loading | State.Error | State.Action | State.Balances; @@ -127,7 +127,9 @@ function useComponentState({ onClick: pushAlertOnError(async () => setAddingAction(true)), }, goToWalletManualWithdraw: { - onClick: pushAlertOnError(goToWalletManualWithdraw), + onClick: pushAlertOnError(async () => { + goToWalletManualWithdraw(state.response.balances[0].scopeInfo) + }), }, goToWalletDeposit, goToWalletHistory, diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts index 5e781121b..1d0ac314a 100644 --- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts +++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts @@ -19,7 +19,10 @@ import { TalerError, TalerErrorCode, } from "@gnu-taler/taler-util"; + import type { MessageFromBackend } from "./platform/api.js"; +import { encodeCrockForURI } from "@gnu-taler/web-util/browser"; + /** * This will modify all the pages that the user load when navigating with Web Extension enabled @@ -64,7 +67,7 @@ function validateTalerUri(uri: string): boolean { function convertURIToWebExtensionPath(uri: string) { const url = new URL( chrome.runtime.getURL( - `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`, + `static/wallet.html#/taler-uri/${encodeCrockForURI(uri)}`, ), ); return url.href; diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts index 8b15380f9..0c6074c42 100644 --- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts +++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts @@ -14,6 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { encodeCrockForURI } from "@gnu-taler/web-util/browser"; + /** * WARNING * @@ -22,10 +24,10 @@ */ (() => { const logger = { - debug: (...msg: any[]) => { }, - info: (...msg: any[]) => + debug: (..._msg: unknown[]) => {}, + info: (...msg: unknown[]) => console.log(`${new Date().toISOString()} TALER`, ...msg), - error: (...msg: any[]) => + error: (...msg: unknown[]) => console.error(`${new Date().toISOString()} TALER`, ...msg), }; @@ -76,13 +78,15 @@ return undefined; } const host = `${config.protocol}//${config.hostname}`; - const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`; + const path = `static/wallet.html#/taler-uri/${encodeCrockForURI(uri)}`; return `${host}/${path}`; } function anchorOnClick(ev: MouseEvent) { if (!(ev.currentTarget instanceof Element)) { - logger.debug(`onclick: registered in a link that is not an HTML element`); + logger.debug( + `onclick: registered in a link that is not an HTML element`, + ); return; } const hrefAttr = ev.currentTarget.attributes.getNamedItem("href"); @@ -95,7 +99,9 @@ targetAttr && targetAttr.value ? targetAttr.value : "_self"; const page = convertURIToWebExtensionPath(hrefAttr.value); if (!page) { - logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`); + logger.debug( + `onclick: could not convert "${hrefAttr.value}" into path`, + ); return; } // we can use window.open, but maybe some browser will block it? @@ -118,7 +124,7 @@ function checkForNewAnchors( mutations: MutationRecord[], - observer: MutationObserver, + _observer: MutationObserver, ) { mutations.forEach((mut) => { if (mut.type === "childList") { @@ -137,7 +143,7 @@ * Register the anchor handler when found */ function registerProtocolHandler() { - if (document.body) overrideAllAnchor(document.body) + if (document.body) overrideAllAnchor(document.body); new MutationObserver(checkForNewAnchors).observe(document, { childList: true, subtree: true, @@ -179,7 +185,8 @@ }; if (apiEnabled) { - //@ts-ignore + // @ts-expect-error we now that `taler` doesn't exist. + // we are creating the property window.taler = taler; } @@ -196,5 +203,4 @@ } start(); -})() - +})(); diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 4947eb7a9..60630f747 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -33,6 +33,8 @@ import { } from "@gnu-taler/taler-util"; import { TranslationProvider, + decodeCrockFromURI, + encodeCrockForURI, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; @@ -178,23 +180,19 @@ export function Application(): VNode { scope={ !scope ? undefined - : parseScopeInfoShort((scope)) + : parseScopeInfoShort(decodeCrockFromURI(scope)) } goToWalletDeposit={(scope: ScopeInfo) => redirectTo( Pages.sendCash({ - scope: encodeURIComponent( - stringifyScopeInfoShort(scope), - ), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } - goToWalletManualWithdraw={(scope?: ScopeInfo) => + goToWalletManualWithdraw={(scope: ScopeInfo) => redirectTo( Pages.receiveCash({ - scope: !scope - ? undefined - : encodeURIComponent(stringifyScopeInfoShort(scope)), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -214,24 +212,20 @@ export function Application(): VNode { scope={ !scope ? undefined - : parseScopeInfoShort((scope)) + : parseScopeInfoShort(decodeCrockFromURI(scope)) } search goToWalletDeposit={(scope: ScopeInfo) => redirectTo( Pages.sendCash({ - scope: encodeURIComponent( - stringifyScopeInfoShort(scope), - ), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } - goToWalletManualWithdraw={(scope?: ScopeInfo) => + goToWalletManualWithdraw={(scope: ScopeInfo) => redirectTo( Pages.receiveCash({ - scope: !scope - ? undefined - : encodeURIComponent(stringifyScopeInfoShort(scope)), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -241,37 +235,103 @@ 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, + amount, + }: { + scope?: string; + amount: 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} + amount={!amount ? undefined : Amounts.parse(amount)} + goToWalletBankDeposit={(s, amount: string) => + redirectTo( + Pages.balanceDeposit({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + amount, + }), + ) + } + goToWalletWalletSend={(s, amount: string) => + redirectTo( + Pages.ctaTransferCreate({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + amount, + }), + ) + } + /> + </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, + amount, + }: { + scope?: string; + amount?: string; + }) => { + const s = !scope + ? undefined + : parseScopeInfoShort(decodeCrockFromURI(scope)); + + return ( + <WalletTemplate path="balance" goToURL={redirectToURL}> + <DestinationSelectionPage + type="get" + scope={s} + amount={!amount ? undefined : Amounts.parse(amount)} + goToWalletManualWithdraw={(s, amount?: string) => + redirectTo( + Pages.ctaWithdrawManual({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + amount, + }), + ) + } + goToWalletWalletInvoice={(s, amount?: string) => + redirectTo( + Pages.ctaInvoiceCreate({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + amount, + }), + ) + } + /> + </WalletTemplate> + ); + }} /> <Route @@ -283,9 +343,7 @@ export function Application(): VNode { goToWalletHistory={(scope: ScopeInfo) => redirectTo( Pages.balanceHistory({ - scope: encodeURIComponent( - stringifyScopeInfoShort(scope), - ), + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), }), ) } @@ -305,13 +363,21 @@ export function Application(): VNode { }) => ( <WalletTemplate path="balance" goToURL={redirectToURL}> <DepositPage - scope={parseScopeInfoShort((scope))} + scope={parseScopeInfoShort(decodeCrockFromURI(scope))} amount={!amount ? undefined : Amounts.parse(amount)} onCancel={(scope: ScopeInfo) => { - redirectTo(Pages.balanceHistory({ scope: encodeURIComponent(stringifyScopeInfoShort(scope)) })); + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), + }), + ); }} onSuccess={(scope: ScopeInfo) => { - redirectTo(Pages.balanceHistory({ scope: encodeURIComponent(stringifyScopeInfoShort(scope)) })); + redirectTo( + Pages.balanceHistory({ + scope: encodeCrockForURI(stringifyScopeInfoShort(scope)), + }), + ); }} /> </WalletTemplate> @@ -342,6 +408,7 @@ export function Application(): VNode { redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) } onWithdraw={(_amount: string) => + // FIXME: use receiveCashForPurchase redirectTo(Pages.receiveCash({ scope: "FIXME missing" })) } onBack={() => redirectTo(Pages.backup)} @@ -410,9 +477,10 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash payment`}> <PaymentPage - talerPayUri={decodeURIComponent(talerUri)} + talerPayUri={decodeCrockFromURI(talerUri)} goToWalletManualWithdraw={(_amount?: string) => - redirectTo(Pages.receiveCash({ scope: "FIXME missing" })) + // FIXME: use receiveCashForPruchase + redirectTo(Pages.receiveCash({})) } cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => @@ -427,9 +495,10 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash payment`}> <PaymentTemplatePage - talerTemplateUri={decodeURIComponent(talerUri)} + talerTemplateUri={decodeCrockFromURI(talerUri)} goToWalletManualWithdraw={(_amount?: string) => - redirectTo(Pages.receiveCash({ scope: "FIXME missing" })) + // FIXME: use receiveCashForPruchase + redirectTo(Pages.receiveCash({})) } cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => @@ -444,7 +513,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 })) @@ -458,7 +527,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> <WithdrawPageFromURI - talerWithdrawUri={decodeURIComponent(talerUri)} + talerWithdrawUri={decodeCrockFromURI(talerUri)} cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -470,29 +539,38 @@ 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> - )} + }) => { + 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 + onAmountChanged={async (newamount) => { + const page = `${Pages.ctaWithdrawManual({ + scope: encodeCrockForURI(stringifyScopeInfoShort(s)), + amount: newamount, + })}?talerUri=${encodeCrockForURI(talerUri)}`; + redirectTo(page); + }} + talerExchangeWithdrawUri={talerUri} + amount={amount} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + ); + }} /> <Route path={Pages.ctaDeposit} @@ -506,7 +584,7 @@ export function Application(): VNode { <CallToActionTemplate title={i18n.str`Digital cash deposit`}> <DepositPageCTA amountStr={Amounts.stringify(Amounts.parseOrThrow(amount))} - talerDepositUri={decodeURIComponent(talerUri)} + talerDepositUri={decodeCrockFromURI(talerUri)} cancel={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -548,9 +626,10 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash invoice`}> <InvoicePayPage - talerPayPullUri={decodeURIComponent(talerUri)} + talerPayPullUri={decodeCrockFromURI(talerUri)} goToWalletManualWithdraw={(_amount?: string) => - redirectTo(Pages.receiveCash({ scope: "FIXME missing" })) + // FIXME: use receiveCashForInvoice + redirectTo(Pages.receiveCash({})) } onClose={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => @@ -565,7 +644,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash transfer`}> <TransferPickupPage - talerPayPushUri={decodeURIComponent(talerUri)} + talerPayPushUri={decodeCrockFromURI(talerUri)} onClose={() => redirectTo(Pages.balance)} onSuccess={(tid: string) => redirectTo(Pages.balanceTransaction({ tid })) @@ -579,7 +658,7 @@ export function Application(): VNode { component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => ( <CallToActionTemplate title={i18n.str`Digital cash recovery`}> <RecoveryPage - talerRecoveryUri={decodeURIComponent(talerRecoveryUri)} + talerRecoveryUri={decodeCrockFromURI(talerRecoveryUri)} onCancel={() => redirectTo(Pages.balance)} onSuccess={() => redirectTo(Pages.backup)} /> @@ -591,7 +670,7 @@ export function Application(): VNode { component={({ talerUri }: { talerUri: string }) => ( <CallToActionTemplate title={i18n.str`Development experiment`}> <DevExperimentPage - talerExperimentUri={decodeURIComponent(talerUri)} + talerExperimentUri={decodeCrockFromURI(talerUri)} onCancel={() => redirectTo(Pages.balanceHistory({}))} onSuccess={() => redirectTo(Pages.balanceHistory({}))} /> @@ -601,7 +680,7 @@ export function Application(): VNode { <Route path={Pages.ctaAddExchange} component={({ talerUri }: { talerUri: string }) => { - const tUri = parseTalerUri(decodeURIComponent(talerUri)); + const tUri = parseTalerUri(decodeCrockFromURI(talerUri)); const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx index 8a3710f69..5c950bec8 100644 --- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx @@ -262,7 +262,7 @@ function BackupLayout(props: TransactionLayoutProps): VNode { <div style={{ color: !props.active ? "grey" : undefined }}> <a href={Pages.backupProviderDetail({ - pid: encodeURIComponent(props.id), + pid: props.id, })} > <span>{props.title}</span> diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts index b56fe5523..74726fc30 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, 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,17 @@ export type Props = PropsGet | PropsSend; interface PropsGet { type: "get"; - amount?: string; - goToWalletManualWithdraw: (amount: string) => void; - goToWalletWalletInvoice: (amount: string) => void; + scope?: ScopeInfo; + amount?: AmountJson; + goToWalletManualWithdraw: (s:ScopeInfo, amount: string) => void; + goToWalletWalletInvoice: (s:ScopeInfo, amount: string) => void; } interface PropsSend { type: "send"; - amount?: string; - goToWalletBankDeposit: (amount: string) => void; - goToWalletWalletSend: (amount: string) => void; + scope: ScopeInfo; + amount?: AmountJson; + goToWalletBankDeposit: (s:ScopeInfo, amount: string) => void; + goToWalletWalletSend: (s:ScopeInfo, amount: string) => void; } export type State = diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts index d4e270a6c..1ae9960d1 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts @@ -14,10 +14,16 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts } from "@gnu-taler/taler-util"; +import { + AmountJson, + Amounts, + 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"; +import { useEffect, useState } from "preact/hooks"; import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; @@ -28,54 +34,47 @@ export function useComponentState(props: Props): RecursiveState<State> { const api = useBackendContext(); const { pushAlertOnError } = useAlertContext(); - 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 info = hook && !hook.hasError ? hook.response : undefined; + const [amount, setAmount] = useState<AmountJson | undefined>( + props.amount ?? + (scope ? Amounts.zeroOfCurrency(scope.currency) : undefined), + ); - // const initialCurrency = parsedInitialAmount?.currency; + useEffect(() => { + if (!scope) return; + if (!amount) { + setAmount(Amounts.zeroOfCurrency(scope.currency)); + } + }, [scope]); - 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", - }, - ]; + { + 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 || !amount) { 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, {}), + api.wallet.call(WalletApiOperation.GetBalances, {}), ); if (!hook) { @@ -87,14 +86,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.balances.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,7 +118,7 @@ export function useComponentState(props: Props): RecursiveState<State> { status: "select-currency", error: undefined, onCurrencySelected: (c: string) => { - setAmount(Amounts.zeroOfCurrency(c)); + setScope(parseScopeInfoShort(c)); }, currencies, }; @@ -128,8 +143,8 @@ export function useComponentState(props: Props): RecursiveState<State> { onClick: invalid ? undefined : pushAlertOnError(async () => { - props.goToWalletBankDeposit(currencyAndAmount); - }), + props.goToWalletBankDeposit(scope, currencyAndAmount); + }), }, selectMax: { onClick: pushAlertOnError(async () => { @@ -146,8 +161,8 @@ export function useComponentState(props: Props): RecursiveState<State> { onClick: invalid ? undefined : pushAlertOnError(async () => { - props.goToWalletWalletSend(currencyAndAmount); - }), + props.goToWalletWalletSend(scope, currencyAndAmount); + }), }, amountHandler: { onInput: pushAlertOnError(async (s) => setAmount(s)), @@ -169,22 +184,22 @@ export function useComponentState(props: Props): RecursiveState<State> { onClick: invalid ? undefined : pushAlertOnError(async () => { - props.goToWalletManualWithdraw(currencyAndAmount); - }), + props.goToWalletManualWithdraw(scope, currencyAndAmount); + }), }, goToBank: { onClick: invalid ? undefined : pushAlertOnError(async () => { - props.goToWalletManualWithdraw(currencyAndAmount); - }), + props.goToWalletManualWithdraw(scope, currencyAndAmount); + }), }, goToWallet: { onClick: invalid ? undefined : pushAlertOnError(async () => { - props.goToWalletWalletInvoice(currencyAndAmount); - }), + props.goToWalletWalletInvoice(scope, currencyAndAmount); + }), }, amountHandler: { onInput: pushAlertOnError(async (s) => setAmount(s)), diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts index 683378613..fdea7685f 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts @@ -20,11 +20,13 @@ */ import { + AmountString, Amounts, ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, ExchangeUpdateStatus, + ScopeInfo, ScopeType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -34,12 +36,13 @@ 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", + currency, exchangeBaseUrl: "http://", masterPub: "123qwe123", scopeInfo: { - currency: "ARS", + currency, type: ScopeType.Exchange, url: "http://", }, @@ -57,16 +60,31 @@ describe("Destination selection states", () => { it("should select currency if no amount specified", async () => { const { handler, TestingContext } = createWalletApiMock(); - handler.addWalletCallResponse( - WalletApiOperation.ListExchanges, - {}, - { - exchanges: [exchangeArs], - }, - ); + handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, { + balances: [ + { + flags: [], + available: `${currency}:1` as AmountString, + hasPendingTransactions: false, + pendingIncoming: `${currency}:0` as AmountString, + pendingOutgoing: `${currency}:0` as AmountString, + requiresUserInput: false, + scopeInfo: { + currency, + type: ScopeType.Exchange, + url: "http://exchange.test.taler.net", + }, + }, + ], + }); const props = { type: "get" as const, + // scope: { + // currency: "ARS", + // type: ScopeType.Exchange, + // url: "http://asd.com", + // } as ScopeInfo, goToWalletManualWithdraw: nullFunction, goToWalletWalletInvoice: nullFunction, }; @@ -82,7 +100,7 @@ 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", }); @@ -111,9 +129,14 @@ describe("Destination selection states", () => { 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( @@ -133,16 +156,6 @@ describe("Destination selection states", () => { 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..a9105a601 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx @@ -280,7 +280,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> diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index 9feb03714..9aca75531 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -667,6 +667,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.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 59315d61f..561133887 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -54,7 +54,7 @@ interface Props { scope?: ScopeInfo; search?: boolean; goToWalletDeposit: (scope: ScopeInfo) => Promise<void>; - goToWalletManualWithdraw: (scope?: ScopeInfo) => Promise<void>; + goToWalletManualWithdraw: (scope: ScopeInfo) => Promise<void>; } export function HistoryPage({ scope, @@ -172,7 +172,7 @@ export function HistoryView({ scope: ScopeInfo; changeScope: (scope: ScopeInfo) => void; goToWalletDeposit: (scope: ScopeInfo) => Promise<void>; - goToWalletManualWithdraw: (scope?: ScopeInfo) => Promise<void>; + goToWalletManualWithdraw: (scope: ScopeInfo) => Promise<void>; transactionsByDate: Record<string, Transaction[]>; balances: WalletBalance[]; }): VNode { |