aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx600
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts55
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts50
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts30
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts178
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx126
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx80
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx67
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts67
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx60
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx109
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 (