diff options
author | Sebastian <sebasjm@gmail.com> | 2022-09-10 23:21:44 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-09-10 23:21:44 -0300 |
commit | e4f3acfeb2ae6a24c579e7ba8d89625f398d2ee6 (patch) | |
tree | a71787f25c9a6093ef16c7f36dec1d2e7cd94312 | |
parent | dda90b51f6fc6fca48a68bc53088e1ed3f018a21 (diff) |
fix #7343
9 files changed, 236 insertions, 110 deletions
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts index 2521ee69c..71aedc638 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AbsoluteTime, AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { AbsoluteTime, AmountJson, PreparePayResult, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { ButtonHandler } from "../../mui/handlers.js"; @@ -26,11 +26,14 @@ import { LoadingUriView, ReadyView } from "./views.js"; export interface Props { talerPayPullUri: string; onClose: () => Promise<void>; + goToWalletManualWithdraw: (amount?: string) => Promise<void>; } export type State = | State.Loading | State.LoadingUriError + | State.NoEnoughBalance + | State.NoBalanceForCurrency | State.Ready; export namespace State { @@ -47,22 +50,38 @@ export namespace State { export interface BaseInfo { error: undefined; + uri: string; cancel: ButtonHandler; - } - export interface Ready extends BaseInfo { - status: "ready"; amount: AmountJson, + goToWalletManualWithdraw: (currency: string) => Promise<void>; summary: string | undefined, expiration: AbsoluteTime | undefined, + operationError?: TalerErrorDetail; + payStatus: PreparePayResult; + } + + export interface NoBalanceForCurrency extends BaseInfo { + status: "no-balance-for-currency" + balance: undefined; + } + export interface NoEnoughBalance extends BaseInfo { + status: "no-enough-balance" + balance: AmountJson; + } + + export interface Ready extends BaseInfo { + status: "ready"; error: undefined; + balance: AmountJson; accept: ButtonHandler; - operationError?: TalerErrorDetail; } } const viewMapping: StateViewMap<State> = { loading: Loading, "loading-uri": LoadingUriView, + "no-balance-for-currency": ReadyView, + "no-enough-balance": ReadyView, "ready": ReadyView, }; diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts index 27be1e424..f87cdf8e1 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts @@ -14,22 +14,31 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AbsoluteTime, Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Amounts, NotificationType, PreparePayResult, PreparePayResultType, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import * as wxApi from "../../wxApi.js"; import { Props, State } from "./index.js"; export function useComponentState( - { talerPayPullUri, onClose }: Props, + { talerPayPullUri, onClose, goToWalletManualWithdraw }: Props, api: typeof wxApi, ): State { const hook = useAsyncAsHook(async () => { - return await api.checkPeerPullPayment({ + const p2p = await api.checkPeerPullPayment({ talerUri: talerPayPullUri }) - }, []) + const balance = await api.getBalance(); + return { p2p, balance } + }) + + useEffect(() => { + api.onUpdateNotification([NotificationType.CoinWithdrawn], () => { + hook?.retry(); + }); + }); + const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) if (!hook) { @@ -45,12 +54,84 @@ export function useComponentState( }; } - const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response + // const { payStatus } = hook.response.p2p; + + const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response.p2p - const amount: string = contractTerms?.amount + + const amountStr: string = contractTerms?.amount + const amount = Amounts.parseOrThrow(amountStr) const summary: string | undefined = contractTerms?.summary const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration + const foundBalance = hook.response.balance.balances.find( + (b) => Amounts.parseOrThrow(b.available).currency === amount.currency, + ); + + const paymentPossible: PreparePayResult = { + status: PreparePayResultType.PaymentPossible, + proposalId: "fakeID", + contractTerms: { + } as any, + contractTermsHash: "asd", + amountRaw: hook.response.p2p.amount, + amountEffective: hook.response.p2p.amount, + noncePriv: "", + } as PreparePayResult + + const insufficientBalance: PreparePayResult = { + status: PreparePayResultType.InsufficientBalance, + proposalId: "fakeID", + contractTerms: { + } as any, + amountRaw: hook.response.p2p.amount, + noncePriv: "", + } + + + const baseResult = { + uri: talerPayPullUri, + cancel: { + onClick: onClose + }, + amount, + goToWalletManualWithdraw, + summary, + expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined, + operationError, + } + + if (!foundBalance) { + return { + status: "no-balance-for-currency", + error: undefined, + balance: undefined, + ...baseResult, + payStatus: insufficientBalance, + } + } + + const foundAmount = Amounts.parseOrThrow(foundBalance.available); + + //FIXME: should use pay result type since it check for coins exceptions + if (Amounts.cmp(foundAmount, amount) < 0) { //payStatus.status === PreparePayResultType.InsufficientBalance) { + return { + status: 'no-enough-balance', + error: undefined, + balance: foundAmount, + ...baseResult, + payStatus: insufficientBalance, + } + } + + // if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { + // return { + // status: "confirmed", + // balance: foundAmount, + // ...baseResult, + // }; + // } + async function accept(): Promise<void> { try { const resp = await api.acceptPeerPullPayment({ @@ -69,16 +150,12 @@ export function useComponentState( return { status: "ready", - amount: Amounts.parseOrThrow(amount), error: undefined, + ...baseResult, + payStatus: paymentPossible, + balance: foundAmount, accept: { onClick: accept }, - summary, - expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined, - cancel: { - onClick: onClose - }, - operationError } } diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx index 81b79a208..5a8a51932 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util"; import { createExample } from "../../test-utils.js"; import { ReadyView } from "./views.js"; @@ -32,6 +33,10 @@ export const Ready = createExample(ReadyView, { value: 1, fraction: 0, }, + payStatus: { + status: PreparePayResultType.PaymentPossible, + amountEffective: "ARS:1", + } as PreparePayResult, accept: {}, cancel: {}, }); diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx index 71bdb20b8..21b666abd 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -14,16 +14,23 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { h, VNode } from "preact"; +import { Amounts, PreparePayResultType } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; import { Amount } from "../../components/Amount.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; import { LogoHeader } from "../../components/LogoHeader.js"; import { Part } from "../../components/Part.js"; -import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; +import { + Link, + SubTitle, + WalletAction, + WarningBox, +} from "../../components/styled/index.js"; import { Time } from "../../components/Time.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; +import { ButtonsSection, PayWithMobile } from "../Payment/views.js"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { @@ -37,16 +44,21 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { ); } -export function ReadyView({ - operationError, - cancel, - accept, - expiration, - summary, - amount, -}: State.Ready): VNode { +export function ReadyView( + state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance, +): VNode { const { i18n } = useTranslationContext(); - + const { + operationError, + summary, + amount, + expiration, + uri, + status, + balance, + payStatus, + cancel, + } = state; return ( <WalletAction> <LogoHeader /> @@ -78,13 +90,14 @@ export function ReadyView({ kind="neutral" /> </section> - <section> - <Button variant="contained" color="success" onClick={accept.onClick}> - <i18n.Translate> - Pay {<Amount value={amount} />} - </i18n.Translate> - </Button> - </section> + <ButtonsSection + amount={amount} + balance={balance} + payStatus={payStatus} + uri={uri} + payHandler={status === "ready" ? state.accept : undefined} + goToWalletManualWithdraw={state.goToWalletManualWithdraw} + /> <section> <Link upperCased onClick={cancel.onClick}> <i18n.Translate>Cancel</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts index 889e532c2..6e401f7d2 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts @@ -15,6 +15,7 @@ */ import { AmountJson, ConfirmPayResult, PreparePayResult, PreparePayResultAlreadyConfirmed, PreparePayResultInsufficientBalance, PreparePayResultPaymentPossible } from "@gnu-taler/taler-util"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { ButtonHandler } from "../../mui/handlers.js"; @@ -85,7 +86,7 @@ export namespace State { status: "completed"; payStatus: PreparePayResult; payResult: ConfirmPayResult; - payHandler: ButtonHandler; + paymentError?: TalerError; balance: AmountJson; } } diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts index 842bb7ed6..ad4bb7004 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts @@ -101,9 +101,7 @@ export function useComponentState( status: "completed", balance: foundAmount, payStatus, - payHandler: { - error: payErrMsg, - }, + paymentError: payErrMsg, payResult, ...baseResult, }; diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx index c43745909..c799607ad 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -16,9 +16,12 @@ import { AbsoluteTime, + AmountJson, Amounts, ConfirmPayResultType, ContractTerms, + PreparePayResult, + PreparePayResultPaymentPossible, PreparePayResultType, Product, } from "@gnu-taler/taler-util"; @@ -42,6 +45,7 @@ import { import { Time } from "../../components/Time.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; +import { ButtonHandler } from "../../mui/handlers.js"; import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js"; import { State } from "./index.js"; @@ -164,7 +168,11 @@ export function BaseView(state: SupportedStates): VNode { )} </section> <ButtonsSection - state={state} + amount={state.amount} + balance={state.balance} + payStatus={state.payStatus} + uri={state.uri} + payHandler={state.status === "ready" ? state.payHandler : undefined} goToWalletManualWithdraw={state.goToWalletManualWithdraw} /> <section> @@ -276,9 +284,9 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode { } if (state.status == "completed") { - const { payResult, payHandler } = state; - if (payHandler.error) { - return <ErrorTalerOperation error={payHandler.error.errorDetail} />; + const { payResult, paymentError } = state; + if (paymentError) { + return <ErrorTalerOperation error={paymentError.errorDetail} />; } if (payResult.type === ConfirmPayResultType.Done) { return ( @@ -307,15 +315,11 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode { return <Fragment />; } -function PayWithMobile({ state }: { state: SupportedStates }): VNode { +export function PayWithMobile({ uri }: { uri: string }): VNode { const { i18n } = useTranslationContext(); const [showQR, setShowQR] = useState<boolean>(false); - const privateUri = - state.payStatus.status !== PreparePayResultType.AlreadyConfirmed - ? `${state.uri}&n=${state.payStatus.noncePriv}` - : state.uri; return ( <section> <LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}> @@ -327,10 +331,10 @@ function PayWithMobile({ state }: { state: SupportedStates }): VNode { </LinkSuccess> {showQR && ( <div> - <QR text={privateUri} /> + <QR text={uri} /> <i18n.Translate> Scan the QR code or - <a href={privateUri}> + <a href={uri}> <i18n.Translate>click here</i18n.Translate> </a> </i18n.Translate> @@ -340,50 +344,60 @@ function PayWithMobile({ state }: { state: SupportedStates }): VNode { ); } -function ButtonsSection({ - state, - goToWalletManualWithdraw, -}: { - state: SupportedStates; +interface ButtonSectionProps { + payStatus: PreparePayResult; + payHandler: ButtonHandler | undefined; + balance: AmountJson | undefined; + uri: string; + amount: AmountJson; goToWalletManualWithdraw: (currency: string) => Promise<void>; -}): VNode { +} + +export function ButtonsSection({ + payStatus, + uri, + payHandler, + balance, + amount, + goToWalletManualWithdraw, +}: ButtonSectionProps): VNode { const { i18n } = useTranslationContext(); - if (state.status === "ready") { + if (payStatus.status === PreparePayResultType.PaymentPossible) { + const privateUri = `${uri}&n=${payStatus.noncePriv}`; + return ( <Fragment> <section> <Button variant="contained" color="success" - onClick={state.payHandler.onClick} + onClick={payHandler?.onClick} > <i18n.Translate> Pay - {<Amount value={state.payStatus.amountEffective} />} + {<Amount value={amount} />} </i18n.Translate> </Button> </section> - <PayWithMobile state={state} /> + <PayWithMobile uri={privateUri} /> </Fragment> ); } - if ( - state.status === "no-enough-balance" || - state.status === "no-balance-for-currency" - ) { - // if (state.payStatus.status === PreparePayResultType.InsufficientBalance) { + + if (payStatus.status === PreparePayResultType.InsufficientBalance) { let BalanceMessage = ""; - if (!state.balance) { + if (!balance) { BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`; } else { - const balanceShouldBeEnough = - Amounts.cmp(state.balance, state.amount) !== -1; + const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1; if (balanceShouldBeEnough) { - BalanceMessage = i18n.str`Could not find enough coins to pay this order. Even if you have enough ${state.balance.currency} some restriction may apply.`; + BalanceMessage = i18n.str`Could not find enough coins to pay. Even if you have enough ${balance.currency} some restriction may apply.`; } else { - BalanceMessage = i18n.str`Your current balance is not enough for this order.`; + BalanceMessage = i18n.str`Your current balance is not enough.`; } } + const uriPrivate = `${uri}&n=${payStatus.noncePriv}`; + return ( <Fragment> <section> @@ -393,51 +407,45 @@ function ButtonsSection({ <Button variant="contained" color="success" - onClick={() => - goToWalletManualWithdraw(Amounts.stringify(state.amount)) - } + onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))} > <i18n.Translate>Get digital cash</i18n.Translate> </Button> </section> - <PayWithMobile state={state} /> + <PayWithMobile uri={uriPrivate} /> </Fragment> ); - // } - } - if (state.status === "confirmed") { - if (state.payStatus.status === PreparePayResultType.AlreadyConfirmed) { - return ( - <Fragment> - <section> - {state.payStatus.paid && - state.payStatus.contractTerms.fulfillment_message && ( - <Part - title={<i18n.Translate>Merchant message</i18n.Translate>} - text={state.payStatus.contractTerms.fulfillment_message} - kind="neutral" - /> - )} - </section> - {!state.payStatus.paid && <PayWithMobile state={state} />} - </Fragment> - ); - } } - - if (state.status === "completed") { - if (state.payResult.type === ConfirmPayResultType.Pending) { - return ( + if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { + return ( + <Fragment> <section> - <div> - <p> - <i18n.Translate>Processing</i18n.Translate>... - </p> - </div> + {payStatus.paid && payStatus.contractTerms.fulfillment_message && ( + <Part + title={<i18n.Translate>Merchant message</i18n.Translate>} + text={payStatus.contractTerms.fulfillment_message} + kind="neutral" + /> + )} </section> - ); - } + {!payStatus.paid && <PayWithMobile uri={uri} />} + </Fragment> + ); } + // if (state.status === "completed") { + // if (state.payResult.type === ConfirmPayResultType.Pending) { + // return ( + // <section> + // <div> + // <p> + // <i18n.Translate>Processing</i18n.Translate>... + // </p> + // </div> + // </section> + // ); + // } + // } + return <Fragment />; } diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts index c00d6d7f6..61fe86e3a 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts @@ -20,11 +20,13 @@ import { h, VNode } from "preact"; import { expect } from "chai"; describe("useTalerActionURL hook", () => { + it("should be set url to undefined when dismiss", async () => { const ctx = ({ children }: { children: any }): VNode => { return h(IoCProviderForTesting, { value: { findTalerUriInActiveTab: async () => "asd", + findTalerUriInClipboard: async () => "qwe", }, children, }); @@ -42,7 +44,10 @@ describe("useTalerActionURL hook", () => { { const [url, setDismissed] = getLastResultOrThrow(); - expect(url).equals("asd"); + expect(url).deep.equals({ + location: "clipboard", + uri: "qwe" + }); setDismissed(true); } @@ -53,7 +58,6 @@ describe("useTalerActionURL hook", () => { if (url !== undefined) throw Error("invalid"); expect(url).undefined; } - await assertNoPendingUpdate(); }); }); diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts index 74d7cbbd9..e1b08278b 100644 --- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts +++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts @@ -52,7 +52,8 @@ export function useTalerActionURL(): [ } } check(); - }); + }, [setTalerActionUrl]); + const url = dismissed ? undefined : talerActionUrl; return [url, setDismissed]; } |