From c5f484d18a89bd6cda0c7a89eea5ee9d7fe4ba09 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 22 Apr 2022 16:10:21 -0300 Subject: [PATCH] deposit test case --- packages/taler-wallet-webextension/dev.mjs | 67 ++++ .../taler-wallet-webextension/package.json | 4 +- .../serve-esbuild.mjs | 24 -- .../src/cta/Deposit.stories.tsx | 140 +------ .../src/cta/Deposit.tsx | 247 ++---------- .../taler-wallet-webextension/src/cta/Pay.tsx | 28 +- .../src/cta/Withdraw.stories.tsx | 20 +- .../src/cta/Withdraw.tsx | 7 +- .../src/mui/handlers.ts | 21 + .../taler-wallet-webextension/src/stories.tsx | 69 +++- .../src/wallet/CreateManualWithdraw.test.ts | 3 +- .../src/wallet/CreateManualWithdraw.tsx | 26 +- .../src/wallet/DepositPage.stories.tsx | 60 ++- .../src/wallet/DepositPage.test.ts | 362 +++++++++++++++++- .../src/wallet/DepositPage.tsx | 312 ++++++++------- pnpm-lock.yaml | 17 + 16 files changed, 796 insertions(+), 611 deletions(-) create mode 100755 packages/taler-wallet-webextension/dev.mjs delete mode 100755 packages/taler-wallet-webextension/serve-esbuild.mjs create mode 100644 packages/taler-wallet-webextension/src/mui/handlers.ts diff --git a/packages/taler-wallet-webextension/dev.mjs b/packages/taler-wallet-webextension/dev.mjs new file mode 100755 index 000000000..6c88f8a24 --- /dev/null +++ b/packages/taler-wallet-webextension/dev.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/* eslint-disable no-undef */ + +import linaria from '@linaria/esbuild' +import esbuild from 'esbuild' +import { buildConfig } from "./build-fast-with-linaria.mjs" +import fs from 'fs'; +import WebSocket from "ws"; +import chokidar from "chokidar"; +import path from "path" + +const devServerBroadcastDelay = 500 +const devServerPort = 8002 +const wss = new WebSocket.Server({ port: devServerPort }); +const toWatch = ["./src"] + +function broadcast(file, event) { + setTimeout(() => { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + console.log(new Date(), file) + client.send(JSON.stringify(event)); + } + }); + }, devServerBroadcastDelay); +} +wss.addListener("connection", () => { + console.log("new client") +}) + +const watcher = chokidar + .watch(toWatch, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100, + }, + }) + .on("error", (error) => console.error(error)) + .on("change", async (file) => { + broadcast(file, { type: "RELOAD" }); + }) + .on("add", async (file) => { + broadcast(file, { type: "RELOAD" }); + }) + .on("unlink", async (file) => { + broadcast(file, { type: "RELOAD" }); + }); + + +fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json")) +fs.writeFileSync("dev-html/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css")) +fs.writeFileSync("dev-html/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js")) +fs.writeFileSync("dev-html/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map")) + +const server = await esbuild + .serve({ servedir: 'dev-html' }, { + ...buildConfig, outdir: 'dev-html/dist' + }) + .catch((e) => { + console.log(e) + process.exit(1) + }); + +console.log("ready!", server.port); + diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 1293c2b26..bf586834b 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -29,7 +29,8 @@ "preact": "^10.6.5", "preact-router": "3.2.1", "qrcode-generator": "^1.4.4", - "tslib": "^2.3.1" + "tslib": "^2.3.1", + "ws": "7.4.5" }, "devDependencies": { "@babel/core": "7.13.16", @@ -59,6 +60,7 @@ "babel-loader": "^8.2.3", "babel-plugin-transform-react-jsx": "^6.24.1", "chai": "^4.3.6", + "chokidar": "^3.5.3", "mocha": "^9.2.0", "nyc": "^15.1.0", "polished": "^4.1.4", diff --git a/packages/taler-wallet-webextension/serve-esbuild.mjs b/packages/taler-wallet-webextension/serve-esbuild.mjs deleted file mode 100755 index 68dff2c2d..000000000 --- a/packages/taler-wallet-webextension/serve-esbuild.mjs +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-undef */ - -import linaria from '@linaria/esbuild' -import esbuild from 'esbuild' -import { buildConfig } from "./build-fast-with-linaria.mjs" -import fs from 'fs'; - -fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json")) -fs.writeFileSync("dev-html/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css")) -fs.writeFileSync("dev-html/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js")) -fs.writeFileSync("dev-html/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map")) - -const server = await esbuild - .serve({ - servedir: 'dev-html', - }, { ...buildConfig, outdir: 'dev-html/dist' }) - .catch((e) => { - console.log(e) - process.exit(1) - }); - -console.log("ready!", server.port); - diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx index 923ea9e96..6432d532d 100644 --- a/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx @@ -21,7 +21,7 @@ import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util"; import { createExample } from "../test-utils.js"; -import { PaymentRequestView as TestedComponent } from "./Deposit.js"; +import { View as TestedComponent } from "./Deposit.js"; export default { title: "cta/deposit", @@ -29,140 +29,6 @@ export default { argTypes: {}, }; -export const NoBalance = createExample(TestedComponent, { - payStatus: { - status: PreparePayResultType.InsufficientBalance, - noncePriv: "", - proposalId: "proposal1234", - contractTerms: { - merchant: { - name: "someone", - }, - summary: "some beers", - amount: "USD:10", - } as Partial as any, - amountRaw: "USD:10", - }, -}); - -export const NoEnoughBalance = createExample(TestedComponent, { - payStatus: { - status: PreparePayResultType.InsufficientBalance, - noncePriv: "", - proposalId: "proposal1234", - contractTerms: { - merchant: { - name: "someone", - }, - summary: "some beers", - amount: "USD:10", - } as Partial as any, - amountRaw: "USD:10", - }, - balance: { - currency: "USD", - fraction: 40000000, - value: 9, - }, -}); - -export const PaymentPossible = createExample(TestedComponent, { - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - status: PreparePayResultType.PaymentPossible, - amountEffective: "USD:10", - amountRaw: "USD:10", - noncePriv: "", - contractTerms: { - nonce: "123213123", - merchant: { - name: "someone", - }, - amount: "USD:10", - summary: "some beers", - } as Partial as any, - contractTermsHash: "123456", - proposalId: "proposal1234", - }, -}); - -export const PaymentPossibleWithFee = createExample(TestedComponent, { - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - status: PreparePayResultType.PaymentPossible, - amountEffective: "USD:10.20", - amountRaw: "USD:10", - noncePriv: "", - contractTerms: { - nonce: "123213123", - merchant: { - name: "someone", - }, - amount: "USD:10", - summary: "some beers", - } as Partial as any, - contractTermsHash: "123456", - proposalId: "proposal1234", - }, -}); - -export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, { - payStatus: { - status: PreparePayResultType.AlreadyConfirmed, - amountEffective: "USD:10", - amountRaw: "USD:10", - contractTerms: { - merchant: { - name: "someone", - }, - fulfillment_message: - "congratulations! you are looking at the fulfillment message! ", - summary: "some beers", - amount: "USD:10", - } as Partial as any, - contractTermsHash: "123456", - proposalId: "proposal1234", - paid: false, - }, -}); - -export const AlreadyConfirmedWithoutFullfilment = createExample( - TestedComponent, - { - payStatus: { - status: PreparePayResultType.AlreadyConfirmed, - amountEffective: "USD:10", - amountRaw: "USD:10", - contractTerms: { - merchant: { - name: "someone", - }, - summary: "some beers", - amount: "USD:10", - } as Partial as any, - contractTermsHash: "123456", - proposalId: "proposal1234", - paid: false, - }, - }, -); - -export const AlreadyPaid = createExample(TestedComponent, { - payStatus: { - status: PreparePayResultType.AlreadyConfirmed, - amountEffective: "USD:10", - amountRaw: "USD:10", - contractTerms: { - merchant: { - name: "someone", - }, - fulfillment_message: - "congratulations! you are looking at the fulfillment message! ", - summary: "some beers", - amount: "USD:10", - } as Partial as any, - contractTermsHash: "123456", - proposalId: "proposal1234", - paid: true, - }, +export const Simple = createExample(TestedComponent, { + state: { status: "ready" }, }); diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.tsx b/packages/taler-wallet-webextension/src/cta/Deposit.tsx index 541bc733b..23c557b0c 100644 --- a/packages/taler-wallet-webextension/src/cta/Deposit.tsx +++ b/packages/taler-wallet-webextension/src/cta/Deposit.tsx @@ -39,6 +39,8 @@ import { TalerError } from "@gnu-taler/taler-wallet-core"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; +import { Loading } from "../components/Loading.js"; +import { LoadingError } from "../components/LoadingError.js"; import { LogoHeader } from "../components/LogoHeader.js"; import { Part } from "../components/Part.js"; import { @@ -49,157 +51,50 @@ import { WarningBox, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; +import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import * as wxApi from "../wxApi.js"; interface Props { - talerPayUri?: string; + talerDepositUri?: string; goBack: () => void; } -export function DepositPage({ talerPayUri, goBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const [payStatus, setPayStatus] = useState( - undefined, - ); - const [payResult, setPayResult] = useState( - undefined, - ); - const [payErrMsg, setPayErrMsg] = useState( - undefined, - ); +type State = Loading | Ready; +interface Loading { + status: "loading"; + hook: HookError | undefined; +} +interface Ready { + status: "ready"; +} - const balance = useAsyncAsHook(wxApi.getBalance, [ - NotificationType.CoinWithdrawn, - ]); - const balanceWithoutError = balance?.hasError - ? [] - : balance?.response.balances || []; - - const foundBalance = balanceWithoutError.find( - (b) => - payStatus && - Amounts.parseOrThrow(b.available).currency === - Amounts.parseOrThrow(payStatus?.amountRaw).currency, - ); - const foundAmount = foundBalance - ? Amounts.parseOrThrow(foundBalance.available) - : undefined; - // We use a string here so that dependency tracking for useEffect works properly - const foundAmountStr = foundAmount - ? Amounts.stringify(foundAmount) - : undefined; - - useEffect(() => { - if (!talerPayUri) return; - const doFetch = async (): Promise => { - try { - const p = await wxApi.preparePay(talerPayUri); - setPayStatus(p); - } catch (e) { - console.log("Got error while trying to pay", e); - if (e instanceof TalerError) { - setPayErrMsg(e); - } - if (e instanceof Error) { - setPayErrMsg(e.message); - } - } - }; - doFetch(); - }, [talerPayUri, foundAmountStr]); - - if (!talerPayUri) { - return ( - - missing pay uri - - ); - } - - if (!payStatus) { - if (payErrMsg instanceof TalerError) { - return ( - - - - Digital cash payment - -
- - Could not get the payment information for this order - - } - error={payErrMsg?.errorDetail} - /> -
-
- ); - } - if (payErrMsg) { - return ( - - - - Digital cash payment - -
-

- - Could not get the payment information for this order - -

- {payErrMsg} -
-
- ); - } - return ( - - Loading payment information ... - - ); - } - - const onClick = async (): Promise => { - // try { - // const res = await doPayment(payStatus); - // setPayResult(res); - // } catch (e) { - // console.error(e); - // if (e instanceof Error) { - // setPayErrMsg(e.message); - // } - // } +function useComponentState(uri: string | undefined): State { + return { + status: "loading", + hook: undefined, }; - - return ( - - ); } -export interface PaymentRequestViewProps { - payStatus: PreparePayResult; - payResult?: ConfirmPayResult; - onClick: () => void; - payErrMsg?: string; - uri: string; - balance: AmountJson | undefined; +export function DepositPage({ talerDepositUri, goBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const state = useComponentState(talerDepositUri); + if (state.status === "loading") { + if (!state.hook) return ; + return ( + Could not load pay status} + error={state.hook} + /> + ); + } + return ; } -export function PaymentRequestView({ - payStatus, - payResult, -}: PaymentRequestViewProps): VNode { - const totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw); - const contractTerms: ContractTerms = payStatus.contractTerms; + +export interface ViewProps { + state: State; +} +export function View({ state }: ViewProps): VNode { const { i18n } = useTranslationContext(); return ( @@ -209,78 +104,6 @@ export function PaymentRequestView({ Digital cash deposit - {payStatus.status === PreparePayResultType.AlreadyConfirmed && - (payStatus.paid ? ( - - Already paid - - ) : ( - - Already claimed - - ))} - {payResult && payResult.type === ConfirmPayResultType.Done && ( - -

- Payment complete -

-

- {!payResult.contractTerms.fulfillment_message ? ( - - You will now be sent back to the merchant you came from. - - ) : ( - payResult.contractTerms.fulfillment_message - )} -

-
- )} -
- {payStatus.status !== PreparePayResultType.InsufficientBalance && - Amounts.isNonZero(totalFees) && ( - Total to pay} - text={amountToPretty( - Amounts.parseOrThrow(payStatus.amountEffective), - )} - kind="negative" - /> - )} - Purchase amount} - text={amountToPretty(Amounts.parseOrThrow(payStatus.amountRaw))} - kind="neutral" - /> - {Amounts.isNonZero(totalFees) && ( - - Fee} - text={amountToPretty(totalFees)} - kind="negative" - /> - - )} - Merchant} - text={contractTerms.merchant.name} - kind="neutral" - /> - Purchase} - text={contractTerms.summary} - kind="neutral" - /> - {contractTerms.order_id && ( - Receipt} - text={`#${contractTerms.order_id}`} - kind="neutral" - /> - )} -
); } diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index 0d5d57378..832b4879c 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -65,7 +65,7 @@ import { useAsyncAsHook, useAsyncAsHook2, } from "../hooks/useAsyncAsHook.js"; -import { ButtonHandler } from "../wallet/CreateManualWithdraw.js"; +import { ButtonHandler } from "../mui/handlers.js"; import * as wxApi from "../wxApi.js"; interface Props { @@ -74,32 +74,6 @@ interface Props { goBack: () => void; } -async function doPayment( - payStatus: PreparePayResult, - api: typeof wxApi, -): Promise { - if (payStatus.status !== "payment-possible") { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR, - hint: `payment is not possible: ${payStatus.status}`, - }); - } - const proposalId = payStatus.proposalId; - const res = await api.confirmPay(proposalId, undefined); - if (res.type !== ConfirmPayResultType.Done) { - throw TalerError.fromUncheckedDetail({ - code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR, - hint: `could not confirm payment`, - payResult: res, - }); - } - const fu = res.contractTerms.fulfillment_url; - if (fu) { - document.location.href = fu; - } - return res; -} - type State = Loading | Ready | Confirmed; interface Loading { status: "loading"; diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx index 2191205c2..f2bc14f76 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx @@ -66,7 +66,9 @@ export const TermsOfServiceNotYetLoaded = createExample(TestedComponent, { exchange: { list: exchangeList, value: "exchange.demo.taler.net", - onChange: () => null, + onChange: async () => { + null; + }, }, showExchangeSelection: false, mustAcceptFirst: false, @@ -99,7 +101,9 @@ export const WithSomeFee = createExample(TestedComponent, { exchange: { list: exchangeList, value: "exchange.demo.taler.net", - onChange: () => null, + onChange: async () => { + null; + }, }, showExchangeSelection: false, mustAcceptFirst: false, @@ -133,7 +137,9 @@ export const WithoutFee = createExample(TestedComponent, { exchange: { list: exchangeList, value: "exchange.demo.taler.net", - onChange: () => null, + onChange: async () => { + null; + }, }, showExchangeSelection: false, mustAcceptFirst: false, @@ -167,7 +173,9 @@ export const EditExchangeUntouched = createExample(TestedComponent, { exchange: { list: exchangeList, value: "exchange.demo.taler.net", - onChange: () => null, + onChange: async () => { + null; + }, }, showExchangeSelection: true, mustAcceptFirst: false, @@ -202,7 +210,9 @@ export const EditExchangeModified = createExample(TestedComponent, { list: exchangeList, isDirty: true, value: "exchange.test.taler.net", - onChange: () => null, + onChange: async () => { + null; + }, }, showExchangeSelection: true, mustAcceptFirst: false, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx index 2293d6508..21f98ec9a 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx @@ -42,10 +42,7 @@ import { import { useTranslationContext } from "../context/translation.js"; import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { buildTermsOfServiceState } from "../utils/index.js"; -import { - ButtonHandler, - SelectFieldHandler, -} from "../wallet/CreateManualWithdraw.js"; +import { ButtonHandler, SelectFieldHandler } from "../mui/handlers.js"; import * as wxApi from "../wxApi.js"; import { Props as TermsOfServiceSectionProps, @@ -258,7 +255,7 @@ export function useComponentState( } const exchangeHandler: SelectFieldHandler = { - onChange: setNextExchange, + onChange: async (e) => setNextExchange(e), value: nextExchange ?? thisExchange, list: exchanges, isDirty: nextExchange !== undefined, diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts new file mode 100644 index 000000000..f75070c9c --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/handlers.ts @@ -0,0 +1,21 @@ +import { TalerError } from "@gnu-taler/taler-wallet-core"; + +export interface TextFieldHandler { + onInput: (value: string) => Promise; + value: string; + error?: string; +} + +export interface ButtonHandler { + onClick?: () => Promise; + error?: TalerError; +} + +export interface SelectFieldHandler { + onChange: (value: string) => Promise; + error?: string; + value: string; + isDirty?: boolean; + list: Record; +} + diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx index 3f74cf11b..1ad91a13b 100644 --- a/packages/taler-wallet-webextension/src/stories.tsx +++ b/packages/taler-wallet-webextension/src/stories.tsx @@ -69,10 +69,13 @@ const SideBar = styled.div` & > { ol { padding: 4px; - div { + div:first-child { background-color: lightcoral; cursor: pointer; } + div[data-hide="true"] { + display: none; + } dd { margin-left: 1em; padding: 4px; @@ -192,12 +195,12 @@ function ExampleList({ selected: ExampleItem | undefined; onSelectStory: (i: ExampleItem, id: string) => void; }): VNode { - const [open, setOpen] = useState(true); + const [isOpen, setOpen] = useState(selected && selected.group === name); return (
    -
    setOpen(!open)}>{name}
    - {open && - list.map((k) => ( +
    setOpen(!isOpen)}>{name}
    +
    + {list.map((k) => (
  1. {k.name}
    @@ -215,6 +218,7 @@ function ExampleList({ href={`#${eId}`} onClick={(e) => { e.preventDefault(); + location.hash = `#${eId}`; onSelectStory(r, eId); }} > @@ -226,6 +230,7 @@ function ExampleList({
  2. ))} +
); } @@ -335,6 +340,7 @@ function Application(): VNode { return ( + {allExamples.map((e) => ( { + if (!liveReloadMounted) { + setupLiveReload(port, () => { + setIsReloading(true); + window.location.reload(); + }); + liveReloadMounted = true; + } + }); + + if (isReloading) { + return ( +
+

reloading...

+
+ ); + } + return ; +} + +function setupLiveReload(port: number, onReload: () => void): void { + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + const host = location.hostname; + const socketPath = `${protocol}//${host}:${port}/socket`; + + const ws = new WebSocket(socketPath); + ws.onmessage = (message) => { + const event = JSON.parse(message.data); + if (event.type === "LOG") { + console.log(event.message); + } + if (event.type === "RELOAD") { + onReload(); + } + }; + ws.onerror = (error) => { + console.error(error); + }; +} diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts index f2bb4a7d2..a4b333f02 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts @@ -21,8 +21,9 @@ */ import { expect } from "chai"; +import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; import { mountHook } from "../test-utils.js"; -import { SelectFieldHandler, TextFieldHandler, useComponentState } from "./CreateManualWithdraw.js"; +import { useComponentState } from "./CreateManualWithdraw.js"; const exchangeListWithARSandUSD = { diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 0440c50a9..11bade6f5 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -37,6 +37,7 @@ import { SubTitle, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; +import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; import { Pages } from "../NavigationBar.js"; export interface Props { @@ -55,25 +56,6 @@ export interface State { exchange: SelectFieldHandler; } -export interface TextFieldHandler { - onInput: (value: string) => void; - value: string; - error?: string; -} - -export interface ButtonHandler { - onClick?: () => Promise; - error?: TalerError; -} - -export interface SelectFieldHandler { - onChange: (value: string) => void; - error?: string; - value: string; - isDirty?: boolean; - list: Record; -} - export function useComponentState( exchangeUrlWithCurrency: Record, initialAmount: string | undefined, @@ -109,12 +91,12 @@ export function useComponentState( const [amount, setAmount] = useState(initialAmount || ""); const parsedAmount = Amounts.parse(`${currency}:${amount}`); - function changeExchange(exchange: string): void { + async function changeExchange(exchange: string): Promise { setExchange(exchange); setCurrency(exchangeUrlWithCurrency[exchange]); } - function changeCurrency(currency: string): void { + async function changeCurrency(currency: string): Promise { setCurrency(currency); const found = Object.entries(exchangeUrlWithCurrency).find( (e) => e[1] === currency, @@ -140,7 +122,7 @@ export function useComponentState( }, amount: { value: amount, - onInput: (e: string) => setAmount(e), + onInput: async (e: string) => setAmount(e), }, parsedAmount, }; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx index edc2f971f..5f7966417 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx @@ -20,10 +20,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Balance, parsePaytoUri } from "@gnu-taler/taler-util"; +import { Amounts, Balance, parsePaytoUri } from "@gnu-taler/taler-util"; import type { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits.js"; import { createExample } from "../test-utils.js"; -import { View as TestedComponent } from "./DepositPage.js"; +import { + createLabelsForBankAccount, + View as TestedComponent, +} from "./DepositPage.js"; export default { title: "wallet/deposit", @@ -41,23 +44,44 @@ async function alwaysReturnFeeToOne(): Promise { } export const WithEmptyAccountList = createExample(TestedComponent, { - accounts: [], - balances: [ - { - available: "USD:10", - } as Balance, - ], - currency: "USD", - onCalculateFee: alwaysReturnFeeToOne, + state: { + status: "no-accounts", + cancelHandler: {}, + }, + // accounts: [], + // balances: [ + // { + // available: "USD:10", + // } as Balance, + // ], + // currency: "USD", + // onCalculateFee: alwaysReturnFeeToOne, }); +const ac = parsePaytoUri("payto://iban/ES8877998399652238")!; +const accountMap = createLabelsForBankAccount([ac]); + export const WithSomeBankAccounts = createExample(TestedComponent, { - accounts: [parsePaytoUri("payto://iban/ES8877998399652238")!], - balances: [ - { - available: "USD:10", - } as Balance, - ], - currency: "USD", - onCalculateFee: alwaysReturnFeeToOne, + state: { + status: "ready", + account: { + list: accountMap, + value: accountMap[0], + onChange: async () => { + null; + }, + }, + currency: "USD", + amount: { + onInput: async () => { + null; + }, + value: "10:USD", + }, + cancelHandler: {}, + depositHandler: {}, + totalFee: Amounts.getZero("USD"), + totalToDeposit: Amounts.parseOrThrow("USD:10"), + // onCalculateFee: alwaysReturnFeeToOne, + }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts index ac4e0ea93..c863b27d5 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts @@ -19,46 +19,390 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts, Balance } from "@gnu-taler/taler-util"; +import { Amounts, Balance, BalancesResponse, parsePaytoUri } from "@gnu-taler/taler-util"; import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { expect } from "chai"; import { mountHook } from "../test-utils.js"; import { useComponentState } from "./DepositPage.js"; +import * as wxApi from "../wxApi.js"; const currency = "EUR" -const feeCalculator = async (): Promise => ({ +const withoutFee = async (): Promise => ({ + coin: Amounts.parseOrThrow(`${currency}:0`), + wire: Amounts.parseOrThrow(`${currency}:0`), + refresh: Amounts.parseOrThrow(`${currency}:0`) +}) + +const withSomeFee = async (): Promise => ({ coin: Amounts.parseOrThrow(`${currency}:1`), wire: Amounts.parseOrThrow(`${currency}:1`), refresh: Amounts.parseOrThrow(`${currency}:1`) }) +const freeJustForIBAN = async (account: string): Promise => /IBAN/i.test(account) ? withoutFee() : withSomeFee() + const someBalance = [{ available: 'EUR:10' } as Balance] +const nullFunction: any = () => null; +type VoidFunction = () => void; + describe("DepositPage states", () => { - it("should have status 'no-balance' when balance is empty", () => { - const { getLastResultOrThrow } = mountHook(() => - useComponentState(currency, [], [], feeCalculator), + it("should have status 'no-balance' when balance is empty", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:0`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [] }) + } as Partial as any) ); + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + { const { status } = getLastResultOrThrow() expect(status).equal("no-balance") } + await assertNoPendingUpdate() + }); - it("should have status 'no-accounts' when balance is not empty and accounts is empty", () => { - const { getLastResultOrThrow } = mountHook(() => - useComponentState(currency, [], someBalance, feeCalculator), + it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:1`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [] }) + } as Partial as any) ); { const { status } = getLastResultOrThrow() - expect(status).equal("no-accounts") + expect(status).equal("loading") } + await waitNextUpdate() + { + const r = getLastResultOrThrow() + if (r.status !== "no-accounts") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + } + + await assertNoPendingUpdate() + }); + + const ibanPayto = parsePaytoUri("payto://iban/ES8877998399652238")!; + const talerBankPayto = parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!; + + it("should have status 'ready' but unable to deposit ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:1`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }) + } as Partial as any) + ); + + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should not be able to deposit more than the balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:1`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withoutFee + } as Partial as any) + ); + + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + + r.amount.onInput("10") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should calculate the fee upon entering amount ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:1`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee + } as Partial as any) + ); + + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + + r.amount.onInput("10") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) + expect(r.depositHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should calculate the fee upon selecting account ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:1`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto, talerBankPayto] }), + getFeeForDeposit: freeJustForIBAN + } as Partial as any) + ); + + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + + r.account.onChange("1") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("1") + expect(r.amount.value).eq("0") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + expect(r.depositHandler.onClick).undefined; + + r.amount.onInput("10") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("1") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) + expect(r.depositHandler.onClick).undefined; + + r.account.onChange("0") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)) + expect(r.depositHandler.onClick).undefined; + + } + + await assertNoPendingUpdate() + }); + + + it("should be able to deposit if has the enough balance ", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(currency, nullFunction, nullFunction, { + getBalance: async () => ({ + balances: [{ available: `${currency}:15`, }] + } as Partial), + listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), + getFeeForDeposit: withSomeFee + } as Partial as any) + ); + + { + const { status } = getLastResultOrThrow() + expect(status).equal("loading") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("0") + expect(r.depositHandler.onClick).undefined; + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) + + r.amount.onInput("10") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("10") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) + expect(r.depositHandler.onClick).not.undefined; + + r.amount.onInput("13") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("13") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)) + expect(r.depositHandler.onClick).not.undefined; + + r.amount.onInput("15") + } + + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("15") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:12`)) + expect(r.depositHandler.onClick).not.undefined; + r.amount.onInput("17") + } + await waitNextUpdate() + + { + const r = getLastResultOrThrow() + if (r.status !== "ready") expect.fail(); + expect(r.cancelHandler.onClick).not.undefined; + expect(r.currency).eq(currency); + expect(r.account.value).eq("0") + expect(r.amount.value).eq("17") + expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) + expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:14`)) + expect(r.depositHandler.onClick).undefined; + } + await assertNoPendingUpdate() + }); + }); \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx index 335dfd3c7..98328ae4a 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx @@ -15,16 +15,10 @@ TALER; see the file COPYING. If not, see */ -import { - AmountJson, - Amounts, - AmountString, - Balance, - PaytoUri, -} from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, PaytoUri } from "@gnu-taler/taler-util"; import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { Fragment, h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { SelectList } from "../components/SelectList.js"; @@ -38,12 +32,13 @@ import { WarningBox, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; -import * as wxApi from "../wxApi.js"; +import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { + ButtonHandler, SelectFieldHandler, TextFieldHandler, -} from "./CreateManualWithdraw.js"; +} from "../mui/handlers.js"; +import * as wxApi from "../wxApi.js"; interface Props { currency: string; @@ -51,119 +46,90 @@ interface Props { onSuccess: (currency: string) => void; } export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode { - const state = useAsyncAsHook(async () => { - const { balances } = await wxApi.getBalance(); - const { accounts } = await wxApi.listKnownBankAccounts(currency); - return { accounts, balances }; - }); + const state = useComponentState(currency, onCancel, onSuccess, wxApi); - const { i18n } = useTranslationContext(); - - async function doSend(p: PaytoUri, a: AmountJson): Promise { - const account = `payto://${p.targetType}/${p.targetPath}`; - const amount = Amounts.stringify(a); - await wxApi.createDepositGroup(account, amount); - onSuccess(currency); - } - - async function getFeeForAmount( - p: PaytoUri, - a: AmountJson, - ): Promise { - const account = `payto://${p.targetType}/${p.targetPath}`; - const amount = Amounts.stringify(a); - return await wxApi.getFeeForDeposit(account, amount); - } - - if (state === undefined) return ; - - if (state.hasError) { - return ( - Could not load deposit balance} - error={state} - /> - ); - } - - return ( - onCancel(currency)} - currency={currency} - accounts={state.response.accounts} - balances={state.response.balances} - onSend={doSend} - onCalculateFee={getFeeForAmount} - /> - ); + return ; } interface ViewProps { - accounts: Array; - currency: string; - balances: Balance[]; - onCancel: () => void; - onSend: (account: PaytoUri, amount: AmountJson) => Promise; - onCalculateFee: ( - account: PaytoUri, - amount: AmountJson, - ) => Promise; + state: State; } -type State = NoBalanceState | NoAccountsState | DepositState; +type State = Loading | NoBalanceState | NoAccountsState | DepositState; + +interface Loading { + status: "loading"; + hook: HookError | undefined; +} interface NoBalanceState { status: "no-balance"; } interface NoAccountsState { status: "no-accounts"; + cancelHandler: ButtonHandler; } interface DepositState { - status: "deposit"; + status: "ready"; + currency: string; amount: TextFieldHandler; account: SelectFieldHandler; totalFee: AmountJson; totalToDeposit: AmountJson; - unableToDeposit: boolean; - selectedAccount: PaytoUri; - parsedAmount: AmountJson | undefined; + // currentAccount: PaytoUri; + // parsedAmount: AmountJson | undefined; + cancelHandler: ButtonHandler; + depositHandler: ButtonHandler; +} + +async function getFeeForAmount( + p: PaytoUri, + a: AmountJson, + api: typeof wxApi, +): Promise { + const account = `payto://${p.targetType}/${p.targetPath}`; + const amount = Amounts.stringify(a); + return await api.getFeeForDeposit(account, amount); } export function useComponentState( currency: string, - accounts: PaytoUri[], - balances: Balance[], - onCalculateFee: ( - account: PaytoUri, - amount: AmountJson, - ) => Promise, + onCancel: (currency: string) => void, + onSuccess: (currency: string) => void, + api: typeof wxApi, ): State { - const accountMap = createLabelsForBankAccount(accounts); + const hook = useAsyncAsHook(async () => { + const { balances } = await api.getBalance(); + const { accounts } = await api.listKnownBankAccounts(currency); + const defaultSelectedAccount = + accounts.length > 0 ? accounts[0] : undefined; + return { accounts, balances, defaultSelectedAccount }; + }); + const [accountIdx, setAccountIdx] = useState(0); - const [amount, setAmount] = useState(undefined); + const [amount, setAmount] = useState(0); + + const [selectedAccount, setSelectedAccount] = useState< + PaytoUri | undefined + >(); + + const parsedAmount = Amounts.parse(`${currency}:${amount}`); + const [fee, setFee] = useState(undefined); - function updateAmount(num: number | undefined): void { - setAmount(num); - setFee(undefined); + + // const hookResponse = !hook || hook.hasError ? undefined : hook.response; + + // useEffect(() => {}, [hookResponse]); + + if (!hook || hook.hasError) { + return { + status: "loading", + hook, + }; } - const selectedAmountSTR: AmountString = `${currency}:${amount}`; - const totalFee = - fee !== undefined - ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount - : Amounts.getZero(currency); - - const selectedAccount = accounts.length ? accounts[accountIdx] : undefined; - - const parsedAmount = - amount === undefined ? undefined : Amounts.parse(selectedAmountSTR); - - useEffect(() => { - if (selectedAccount === undefined || parsedAmount === undefined) return; - onCalculateFee(selectedAccount, parsedAmount).then((result) => { - setFee(result); - }); - }, [amount, selectedAccount, parsedAmount, onCalculateFee]); + const { accounts, balances, defaultSelectedAccount } = hook.response; + const currentAccount = selectedAccount ?? defaultSelectedAccount; const bs = balances.filter((b) => b.available.startsWith(currency)); const balance = @@ -171,6 +137,63 @@ export function useComponentState( ? Amounts.parseOrThrow(bs[0].available) : Amounts.getZero(currency); + if (Amounts.isZero(balance)) { + return { + status: "no-balance", + }; + } + + if (!currentAccount) { + return { + status: "no-accounts", + cancelHandler: { + onClick: async () => { + onCancel(currency); + }, + }, + }; + } + const accountMap = createLabelsForBankAccount(accounts); + + async function updateAccount(accountStr: string): Promise { + const idx = parseInt(accountStr, 10); + const newSelected = accounts.length > idx ? accounts[idx] : undefined; + if (accountIdx === idx || !newSelected) return; + + if (!parsedAmount) { + setAccountIdx(idx); + setSelectedAccount(newSelected); + } else { + const result = await getFeeForAmount(newSelected, parsedAmount, api); + setAccountIdx(idx); + setSelectedAccount(newSelected); + setFee(result); + } + } + + async function updateAmount(numStr: string): Promise { + const num = parseFloat(numStr); + const newAmount = Number.isNaN(num) ? 0 : num; + if (amount === newAmount || !currentAccount) return; + const parsed = Amounts.parse(`${currency}:${newAmount}`); + if (!parsed) { + setAmount(newAmount); + } else { + const result = await getFeeForAmount(currentAccount, parsed, api); + setAmount(newAmount); + setFee(result); + } + } + + const totalFee = + fee !== undefined + ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount + : Amounts.getZero(currency); + + const totalToDeposit = parsedAmount + ? Amounts.sub(parsedAmount, totalFee).amount + : Amounts.getZero(currency); + const isDirty = amount !== 0; const amountError = !isDirty ? undefined @@ -180,65 +203,63 @@ export function useComponentState( ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; - const totalToDeposit = parsedAmount - ? Amounts.sub(parsedAmount, totalFee).amount - : Amounts.getZero(currency); - const unableToDeposit = + !parsedAmount || Amounts.isZero(totalToDeposit) || fee === undefined || amountError !== undefined; - if (Amounts.isZero(balance)) { - return { - status: "no-balance", - }; - } + async function doSend(): Promise { + if (!currentAccount || !parsedAmount) return; - if (!accounts || !accounts.length || !selectedAccount) { - return { - status: "no-accounts", - }; + const account = `payto://${currentAccount.targetType}/${currentAccount.targetPath}`; + const amount = Amounts.stringify(parsedAmount); + await api.createDepositGroup(account, amount); + onSuccess(currency); } return { - status: "deposit", + status: "ready", + currency, amount: { value: String(amount), - onInput: (e) => { - const num = parseFloat(e); - if (!Number.isNaN(num)) { - updateAmount(num); - } else { - updateAmount(undefined); - setFee(undefined); - } - }, + onInput: updateAmount, error: amountError, }, account: { list: accountMap, value: String(accountIdx), - onChange: (s) => setAccountIdx(parseInt(s, 10)), + onChange: updateAccount, + }, + cancelHandler: { + onClick: async () => { + onCancel(currency); + }, + }, + depositHandler: { + onClick: unableToDeposit ? undefined : doSend, }, totalFee, totalToDeposit, - unableToDeposit, - selectedAccount, - parsedAmount, + // currentAccount, + // parsedAmount, }; } -export function View({ - onCancel, - currency, - accounts, - balances, - onSend, - onCalculateFee, -}: ViewProps): VNode { +export function View({ state }: ViewProps): VNode { const { i18n } = useTranslationContext(); - const state = useComponentState(currency, accounts, balances, onCalculateFee); + + if (state === undefined) return ; + + if (state.status === "loading") { + if (!state.hook) return ; + return ( + Could not load deposit balance} + error={state.hook} + /> + ); + } if (state.status === "no-balance") { return ( @@ -258,7 +279,7 @@ export function View({

-
@@ -269,7 +290,7 @@ export function View({ return ( - Send {currency} to your account + Send {state.currency} to your account
@@ -286,7 +307,7 @@ export function View({ Amount
- {currency} + {state.currency} Deposit fee
- {currency} + {state.currency} Total deposit
- {currency} + {state.currency}