From 64acf8e2b1083de6f78b7d21dd2701af2fee1911 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Apr 2022 14:23:53 -0300 Subject: payments test case --- packages/taler-wallet-webextension/src/cta/Pay.tsx | 619 +++++++++++++-------- 1 file changed, 381 insertions(+), 238 deletions(-) (limited to 'packages/taler-wallet-webextension/src/cta/Pay.tsx') diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index f2661308c..0d5d57378 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -27,9 +27,7 @@ import { AmountJson, - AmountLike, Amounts, - AmountString, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, @@ -38,12 +36,14 @@ import { PreparePayResult, PreparePayResultType, Product, + TalerErrorCode, } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Amount } from "../components/Amount.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; +import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { LogoHeader } from "../components/LogoHeader.js"; @@ -60,7 +60,12 @@ import { WarningBox, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; +import { + HookError, + useAsyncAsHook, + useAsyncAsHook2, +} from "../hooks/useAsyncAsHook.js"; +import { ButtonHandler } from "../wallet/CreateManualWithdraw.js"; import * as wxApi from "../wxApi.js"; interface Props { @@ -69,47 +74,88 @@ interface Props { goBack: () => void; } -const doPayment = async ( +async function doPayment( payStatus: PreparePayResult, -): Promise => { + api: typeof wxApi, +): Promise { if (payStatus.status !== "payment-possible") { - throw Error(`invalid state: ${payStatus.status}`); + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR, + hint: `payment is not possible: ${payStatus.status}`, + }); } const proposalId = payStatus.proposalId; - const res = await wxApi.confirmPay(proposalId, undefined); + const res = await api.confirmPay(proposalId, undefined); if (res.type !== ConfirmPayResultType.Done) { - throw Error("payment pending"); + 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; -}; +} -export function PayPage({ - talerPayUri, - goToWalletManualWithdraw, - goBack, -}: Props): VNode { - const { i18n } = useTranslationContext(); +type State = Loading | Ready | Confirmed; +interface Loading { + status: "loading"; + hook: HookError | undefined; +} +interface Ready { + status: "ready"; + hook: undefined; + uri: string; + amount: AmountJson; + totalFees: AmountJson; + payStatus: PreparePayResult; + balance: AmountJson | undefined; + payHandler: ButtonHandler; + payResult: undefined; +} + +interface Confirmed { + status: "confirmed"; + hook: undefined; + uri: string; + amount: AmountJson; + totalFees: AmountJson; + payStatus: PreparePayResult; + balance: AmountJson | undefined; + payResult: ConfirmPayResult; + payHandler: ButtonHandler; +} + +export function useComponentState( + talerPayUri: string | undefined, + api: typeof wxApi, +): State { const [payResult, setPayResult] = useState( undefined, ); - const [payErrMsg, setPayErrMsg] = useState( - undefined, - ); + const [payErrMsg, setPayErrMsg] = useState(undefined); - const hook = useAsyncAsHook(async () => { - if (!talerPayUri) throw Error("Missing pay uri"); - const payStatus = await wxApi.preparePay(talerPayUri); - const balance = await wxApi.getBalance(); - return { payStatus, balance }; - }, [NotificationType.CoinWithdrawn]); + const hook = useAsyncAsHook2(async () => { + if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT"); + const payStatus = await api.preparePay(talerPayUri); + const balance = await api.getBalance(); + return { payStatus, balance, uri: talerPayUri }; + }); useEffect(() => { - const payStatus = - hook && !hook.hasError ? hook.response.payStatus : undefined; + api.onUpdateNotification([NotificationType.CoinWithdrawn], () => { + hook?.retry(); + }); + }); + + const hookResponse = !hook || hook.hasError ? undefined : hook.response; + + useEffect(() => { + if (!hookResponse) return; + const { payStatus } = hookResponse; if ( payStatus && payStatus.status === PreparePayResultType.AlreadyConfirmed && @@ -122,74 +168,139 @@ export function PayPage({ }, 3000); } } - }, []); - - if (!hook) { - return ; - } + }, [hookResponse]); - if (hook.hasError) { - return ( - Could not load pay status} - error={hook} - /> - ); + if (!hook || hook.hasError) { + return { + status: "loading", + hook, + }; } + const { payStatus } = hook.response; + const amount = Amounts.parseOrThrow(payStatus.amountRaw); const foundBalance = hook.response.balance.balances.find( - (b) => - Amounts.parseOrThrow(b.available).currency === - Amounts.parseOrThrow(hook.response.payStatus.amountRaw).currency, + (b) => Amounts.parseOrThrow(b.available).currency === amount.currency, ); const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined; - const onClick = async (): Promise => { + async function doPayment(): Promise { try { - const res = await doPayment(hook.response.payStatus); + if (payStatus.status !== "payment-possible") { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR, + hint: `payment is not possible: ${payStatus.status}`, + }); + } + const res = await api.confirmPay(payStatus.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) { + if (typeof window !== "undefined") { + document.location.href = fu; + } else { + console.log(`should redirect to ${fu}`); + } + } setPayResult(res); } catch (e) { - console.error(e); - if (e instanceof Error) { - setPayErrMsg(e.message); + if (e instanceof TalerError) { + setPayErrMsg(e); } } + } + + const payDisabled = + payErrMsg || + !foundAmount || + payStatus.status === PreparePayResultType.InsufficientBalance; + + const payHandler: ButtonHandler = { + onClick: payDisabled ? undefined : doPayment, + error: payErrMsg, + }; + + let totalFees = Amounts.getZero(amount.currency); + if (payStatus.status === PreparePayResultType.PaymentPossible) { + const amountEffective: AmountJson = Amounts.parseOrThrow( + payStatus.amountEffective, + ); + totalFees = Amounts.sub(amountEffective, amount).amount; + } + + if (!payResult) { + return { + status: "ready", + hook: undefined, + uri: hook.response.uri, + amount, + totalFees, + balance: foundAmount, + payHandler, + payStatus: hook.response.payStatus, + payResult, + }; + } + + return { + status: "confirmed", + hook: undefined, + uri: hook.response.uri, + amount, + totalFees, + balance: foundAmount, + payStatus: hook.response.payStatus, + payResult, + payHandler: {}, }; +} + +export function PayPage({ + talerPayUri, + goToWalletManualWithdraw, + goBack, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const state = useComponentState(talerPayUri, wxApi); + if (state.status === "loading") { + if (!state.hook) return ; + return ( + Could not load pay status} + error={state.hook} + /> + ); + } return ( - ); } -export interface PaymentRequestViewProps { - payStatus: PreparePayResult; - payResult?: ConfirmPayResult; - onClick: () => void; - payErrMsg?: string; - uri: string; - goToWalletManualWithdraw: (s: string) => void; - balance: AmountJson | undefined; -} -export function PaymentRequestView({ - uri, - payStatus, - payResult, - onClick, +export function View({ + state, + goBack, goToWalletManualWithdraw, - balance, -}: PaymentRequestViewProps): VNode { +}: { + state: Ready | Confirmed; + goToWalletManualWithdraw: (currency?: string) => void; + goBack: () => void; +}): VNode { const { i18n } = useTranslationContext(); - let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw); - const contractTerms: ContractTerms = payStatus.contractTerms; + const contractTerms: ContractTerms = state.payStatus.contractTerms; if (!contractTerms) { return ( @@ -203,124 +314,6 @@ export function PaymentRequestView({ ); } - const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw); - if (payStatus.status === PreparePayResultType.PaymentPossible) { - const amountEffective: AmountJson = Amounts.parseOrThrow( - payStatus.amountEffective, - ); - totalFees = Amounts.sub(amountEffective, amountRaw).amount; - } - - function Alternative(): VNode { - const [showQR, setShowQR] = useState(false); - const privateUri = - payStatus.status !== PreparePayResultType.AlreadyConfirmed - ? `${uri}&n=${payStatus.noncePriv}` - : uri; - if (!uri) return ; - return ( -
- setShowQR((qr) => !qr)}> - {!showQR ? ( - Pay with a mobile phone - ) : ( - Hide QR - )} - - {showQR && ( -
- - - Scan the QR code or - - click here - - -
- )} -
- ); - } - - function ButtonsSection(): VNode { - if (payResult) { - if (payResult.type === ConfirmPayResultType.Pending) { - return ( -
-
-

- Processing... -

-
-
- ); - } - return ; - } - if (payStatus.status === PreparePayResultType.PaymentPossible) { - return ( - -
- - - Pay {} - - -
- -
- ); - } - if (payStatus.status === PreparePayResultType.InsufficientBalance) { - return ( - -
- {balance ? ( - - - Your balance of {} is not enough to - pay for this purchase - - - ) : ( - - - Your balance is not enough to pay for this purchase. - - - )} -
-
- goToWalletManualWithdraw(amountRaw.currency)} - > - Withdraw digital cash - -
- -
- ); - } - if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { - return ( - -
- {payStatus.paid && contractTerms.fulfillment_message && ( - Merchant message} - text={contractTerms.fulfillment_message} - kind="neutral" - /> - )} -
- {!payStatus.paid && } -
- ); - } - return ; - } - return ( @@ -328,70 +321,31 @@ export function PaymentRequestView({ Digital cash payment - {payStatus.status === PreparePayResultType.AlreadyConfirmed && - (payStatus.paid ? ( - payStatus.contractTerms.fulfillment_url ? ( - - - Already paid, you are going to be redirected to{" "} - - {payStatus.contractTerms.fulfillment_url} - - - - ) : ( - - Already paid - - ) - ) : ( - - Already claimed - - ))} - {payResult && payResult.type === ConfirmPayResultType.Done && ( - -

- Payment complete -

-

- {!payResult.contractTerms.fulfillment_message ? ( - payResult.contractTerms.fulfillment_url ? ( - - You are going to be redirected to $ - {payResult.contractTerms.fulfillment_url} - - ) : ( - You can close this page. - ) - ) : ( - payResult.contractTerms.fulfillment_message - )} -

-
- )} + + +
- {payStatus.status !== PreparePayResultType.InsufficientBalance && - Amounts.isNonZero(totalFees) && ( + {state.payStatus.status !== PreparePayResultType.InsufficientBalance && + Amounts.isNonZero(state.totalFees) && ( Total to pay} - text={} + text={} kind="negative" /> )} Purchase amount} - text={} + text={} kind="neutral" /> - {Amounts.isNonZero(totalFees) && ( + {Amounts.isNonZero(state.totalFees) && ( Fee} - text={} + text={} kind="negative" /> @@ -417,9 +371,12 @@ export function PaymentRequestView({ )}
- +
- + Cancel
@@ -495,3 +452,189 @@ function ProductList({ products }: { products: Product[] }): VNode {
); } + +function ShowImportantMessage({ state }: { state: Ready | Confirmed }): VNode { + const { i18n } = useTranslationContext(); + const { payStatus } = state; + if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { + if (payStatus.paid) { + if (payStatus.contractTerms.fulfillment_url) { + return ( + + + Already paid, you are going to be redirected to{" "} + + {payStatus.contractTerms.fulfillment_url} + + + + ); + } + return ( + + Already paid + + ); + } + return ( + + Already claimed + + ); + } + + if (state.status == "confirmed") { + const { payResult, payHandler } = state; + if (payHandler.error) { + return ; + } + if (payResult.type === ConfirmPayResultType.Done) { + return ( + +

+ Payment complete +

+

+ {!payResult.contractTerms.fulfillment_message ? ( + payResult.contractTerms.fulfillment_url ? ( + + You are going to be redirected to $ + {payResult.contractTerms.fulfillment_url} + + ) : ( + You can close this page. + ) + ) : ( + payResult.contractTerms.fulfillment_message + )} +

+
+ ); + } + } + return ; +} + +function PayWithMobile({ state }: { state: Ready }): VNode { + const { i18n } = useTranslationContext(); + + const [showQR, setShowQR] = useState(false); + + const privateUri = + state.payStatus.status !== PreparePayResultType.AlreadyConfirmed + ? `${state.uri}&n=${state.payStatus.noncePriv}` + : state.uri; + return ( +
+ setShowQR((qr) => !qr)}> + {!showQR ? ( + Pay with a mobile phone + ) : ( + Hide QR + )} + + {showQR && ( +
+ + + Scan the QR code or + + click here + + +
+ )} +
+ ); +} + +function ButtonsSection({ + state, + goToWalletManualWithdraw, +}: { + state: Ready | Confirmed; + goToWalletManualWithdraw: (currency: string) => void; +}): VNode { + const { i18n } = useTranslationContext(); + if (state.status === "ready") { + const { payStatus } = state; + if (payStatus.status === PreparePayResultType.PaymentPossible) { + return ( + +
+ + + Pay {} + + +
+ +
+ ); + } + if (payStatus.status === PreparePayResultType.InsufficientBalance) { + return ( + +
+ {state.balance ? ( + + + Your balance of {} is not + enough to pay for this purchase + + + ) : ( + + + Your balance is not enough to pay for this purchase. + + + )} +
+
+ goToWalletManualWithdraw(state.amount.currency)} + > + Withdraw digital cash + +
+ +
+ ); + } + if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { + return ( + +
+ {payStatus.paid && + state.payStatus.contractTerms.fulfillment_message && ( + Merchant message} + text={state.payStatus.contractTerms.fulfillment_message} + kind="neutral" + /> + )} +
+ {!payStatus.paid && } +
+ ); + } + } + + if (state.status === "confirmed") { + if (state.payResult.type === ConfirmPayResultType.Pending) { + return ( +
+
+

+ Processing... +

+
+
+ ); + } + } + + return ; +} -- cgit v1.2.3