From a5f052d69c6457ad0289fdcb56398ea1fabedc2a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 29 Aug 2022 11:32:07 -0300 Subject: using CTA for manual withdrawal --- .../src/cta/Withdraw/index.ts | 12 +- .../src/cta/Withdraw/state.ts | 218 +++++++++++++++++++-- .../src/cta/Withdraw/test.ts | 10 +- .../src/cta/Withdraw/views.tsx | 13 +- 4 files changed, 221 insertions(+), 32 deletions(-) (limited to 'packages/taler-wallet-webextension/src/cta/Withdraw') diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts index b12e8df3b..7425dbd29 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -23,15 +23,20 @@ import * as wxApi from "../../wxApi.js"; import { Props as TermsOfServiceSectionProps } from "../TermsOfServiceSection.js"; -import { useComponentState } from "./state.js"; +import { useComponentStateFromParams, useComponentStateFromURI } from "./state.js"; import { CompletedView, LoadingExchangeView, LoadingInfoView, LoadingUriView, SuccessView } from "./views.js"; -export interface Props { +export interface PropsFromURI { talerWithdrawUri: string | undefined; cancel: () => Promise; } +export interface PropsFromParams { + amount: string; + cancel: () => Promise; +} + export type State = | State.Loading | State.LoadingUriError @@ -93,4 +98,5 @@ const viewMapping: StateViewMap = { success: SuccessView, }; -export const WithdrawPage = compose("Withdraw", (p: Props) => useComponentState(p, wxApi), viewMapping) +export const WithdrawPageFromURI = compose("WithdrawPageFromURI", (p: PropsFromURI) => useComponentStateFromURI(p, wxApi), viewMapping) +export const WithdrawPageFromParams = compose("WithdrawPageFromParams", (p: PropsFromParams) => useComponentStateFromParams(p, wxApi), viewMapping) diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 849dd5cca..3b138e74d 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -15,16 +15,210 @@ */ -import { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; import { useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { buildTermsOfServiceState } from "../../utils/index.js"; import * as wxApi from "../../wxApi.js"; -import { Props, State } from "./index.js"; +import { PropsFromURI, PropsFromParams, State } from "./index.js"; -export function useComponentState( - { talerWithdrawUri, cancel }: Props, +export function useComponentStateFromParams( + { amount, cancel }: PropsFromParams, + api: typeof wxApi, +): State { + + const [ageRestricted, setAgeRestricted] = useState(0); + + const exchangeHook = useAsyncAsHook(api.listExchanges); + + const exchangeHookDep = + !exchangeHook || exchangeHook.hasError || !exchangeHook.response + ? undefined + : exchangeHook.response; + + const chosenAmount = Amounts.parseOrThrow(amount); + + // get the first exchange with the currency as the default one + const exchange = exchangeHookDep ? exchangeHookDep.exchanges.find(e => e.currency === chosenAmount.currency) : undefined + /** + * For the exchange selected, bring the status of the terms of service + */ + const terms = useAsyncAsHook(async () => { + if (!exchange) return undefined + + const exchangeTos = await api.getExchangeTos(exchange.exchangeBaseUrl, ["text/xml"]); + + const state = buildTermsOfServiceState(exchangeTos); + + return { state }; + }, [exchangeHookDep]); + + /** + * With the exchange and amount, ask the wallet the information + * about the withdrawal + */ + const amountHook = useAsyncAsHook(async () => { + if (!exchange) return undefined + + const info = await api.getExchangeWithdrawalInfo({ + exchangeBaseUrl: exchange.exchangeBaseUrl, + amount: chosenAmount, + tosAcceptedFormat: ["text/xml"], + }); + + const withdrawAmount = { + raw: Amounts.parseOrThrow(info.withdrawalAmountRaw), + effective: Amounts.parseOrThrow(info.withdrawalAmountEffective), + } + + return { amount: withdrawAmount }; + }, [exchangeHookDep]); + + const [reviewing, setReviewing] = useState(false); + const [reviewed, setReviewed] = useState(false); + + const [withdrawError, setWithdrawError] = useState( + undefined, + ); + const [doingWithdraw, setDoingWithdraw] = useState(false); + const [withdrawCompleted, setWithdrawCompleted] = useState(false); + + if (!exchangeHook) return { status: "loading", error: undefined } + if (exchangeHook.hasError) { + return { + status: "loading-uri", + error: exchangeHook, + }; + } + + if (!exchange) { + return { + status: "loading-exchange", + error: { + hasError: true, + operational: false, + message: "ERROR_NO-DEFAULT-EXCHANGE", + }, + }; + } + + async function doWithdrawAndCheckError(): Promise { + if (!exchange) return; + + try { + setDoingWithdraw(true); + + const response = await wxApi.acceptManualWithdrawal( + exchange.exchangeBaseUrl, + Amounts.stringify(amount), + ); + + setWithdrawCompleted(true); + } catch (e) { + if (e instanceof TalerError) { + setWithdrawError(e); + } + } + setDoingWithdraw(false); + } + + if (!amountHook) { + return { status: "loading", error: undefined } + } + if (amountHook.hasError) { + return { + status: "loading-info", + error: amountHook, + }; + } + if (!amountHook.response) { + return { status: "loading", error: undefined }; + } + if (withdrawCompleted) { + return { status: "completed", error: undefined }; + } + + const withdrawalFee = Amounts.sub( + amountHook.response.amount.raw, + amountHook.response.amount.effective, + ).amount; + const toBeReceived = amountHook.response.amount.effective; + + const { state: termsState } = (!terms + ? undefined + : terms.hasError + ? undefined + : terms.response) || { state: undefined }; + + async function onAccept(accepted: boolean): Promise { + if (!termsState || !exchange) return; + + try { + await api.setExchangeTosAccepted( + exchange.exchangeBaseUrl, + accepted ? termsState.version : undefined, + ); + setReviewed(accepted); + } catch (e) { + if (e instanceof Error) { + //FIXME: uncomment this and display error + // setErrorAccepting(e.message); + } + } + } + + const mustAcceptFirst = + termsState !== undefined && + (termsState.status === "changed" || termsState.status === "new"); + + const ageRestrictionOptions: Record | undefined = "6:12:18" + .split(":") + .reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {}); + + if (ageRestrictionOptions) { + ageRestrictionOptions["0"] = "Not restricted"; + } + + //TODO: calculate based on exchange info + const ageRestrictionEnabled = false; + const ageRestriction = ageRestrictionEnabled ? { + list: ageRestrictionOptions, + value: String(ageRestricted), + onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)), + } : undefined; + + return { + status: "success", + error: undefined, + exchangeUrl: exchange.exchangeBaseUrl, + toBeReceived, + withdrawalFee, + chosenAmount, + ageRestriction, + doWithdrawal: { + onClick: + doingWithdraw || (mustAcceptFirst && !reviewed) + ? undefined + : doWithdrawAndCheckError, + error: withdrawError, + }, + tosProps: !termsState + ? undefined + : { + onAccept, + onReview: setReviewing, + reviewed: reviewed, + reviewing: reviewing, + terms: termsState, + }, + mustAcceptFirst, + cancel, + }; +} + +export function useComponentStateFromURI( + { talerWithdrawUri, cancel }: PropsFromURI, api: typeof wxApi, ): State { const [ageRestricted, setAgeRestricted] = useState(0); @@ -50,21 +244,6 @@ export function useComponentState( ? undefined : uriInfoHook.response; - // const { amount, thisExchange } = useMemo(() => { - // if (!uriHookDep) - // return { - // amount: undefined, - // thisExchange: undefined, - // thisCurrencyExchanges: [], - // }; - - // const { uriInfo } = uriHookDep; - - // const amount = uriHookDep ? Amounts.parseOrThrow(uriHookDep.amount) : undefined; - // const thisExchange = uriHookDep?.thisExchange; - - // return { amount, thisExchange }; - // }, [uriHookDep]); /** * For the exchange selected, bring the status of the terms of service @@ -118,6 +297,7 @@ export function useComponentState( } const { amount, thisExchange } = uriInfoHook.response + const chosenAmount = Amounts.parseOrThrow(amount); if (!thisExchange) { diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index dd3f6c9c7..c72f906e5 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -27,7 +27,7 @@ import { import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core"; import { expect } from "chai"; import { mountHook } from "../../test-utils.js"; -import { useComponentState } from "./state.js"; +import { useComponentStateFromURI } from "./state.js"; const exchanges: ExchangeListItem[] = [ { @@ -56,7 +56,7 @@ describe("Withdraw CTA states", () => { it("should tell the user that the URI is missing", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ talerWithdrawUri: undefined, cancel: async () => { null } }, { + useComponentStateFromURI({ talerWithdrawUri: undefined, cancel: async () => { null } }, { listExchanges: async () => ({ exchanges }), getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ amount: "ARS:2", @@ -88,7 +88,7 @@ describe("Withdraw CTA states", () => { it("should tell the user that there is not known exchange", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { + useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { listExchanges: async () => ({ exchanges }), getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ amount: "EUR:2", @@ -122,7 +122,7 @@ describe("Withdraw CTA states", () => { it("should be able to withdraw if tos are ok", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { + useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { listExchanges: async () => ({ exchanges }), getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ amount: "ARS:2", @@ -188,7 +188,7 @@ describe("Withdraw CTA states", () => { it("should be accept the tos before withdraw", async () => { const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => - useComponentState({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { + useComponentStateFromURI({ talerWithdrawUri: "taler-withdraw://", cancel: async () => { null } }, { listExchanges: async () => ({ exchanges }), getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ amount: "ARS:2", diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index 5949076e0..850bd6e8f 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -122,14 +122,17 @@ export function SuccessView(state: State.Success): VNode {
Exchange - + + +
} text={} -- cgit v1.2.3