/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* This file is part of TALER (C) 2015 GNUnet e.V. TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * Page shown to the user to confirm entering * a contract. */ /** * Imports. */ import { AmountJson, AmountLike, Amounts, AmountString, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, ContractTerms, NotificationType, PreparePayResult, PreparePayResultType, Product, } 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 { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { LogoHeader } from "../components/LogoHeader.js"; import { Part } from "../components/Part.js"; import { QR } from "../components/QR.js"; import { ButtonSuccess, Link, LinkSuccess, SmallLightText, SubTitle, SuccessBox, WalletAction, WarningBox, } from "../components/styled/index.js"; import { useTranslationContext } from "../context/translation.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import * as wxApi from "../wxApi.js"; interface Props { talerPayUri?: string; goToWalletManualWithdraw: (currency?: string) => void; goBack: () => void; } const doPayment = async ( payStatus: PreparePayResult, ): Promise => { if (payStatus.status !== "payment-possible") { throw Error(`invalid state: ${payStatus.status}`); } const proposalId = payStatus.proposalId; const res = await wxApi.confirmPay(proposalId, undefined); if (res.type !== ConfirmPayResultType.Done) { throw Error("payment pending"); } 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(); const [payResult, setPayResult] = 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]); useEffect(() => { const payStatus = hook && !hook.hasError ? hook.response.payStatus : undefined; if ( payStatus && payStatus.status === PreparePayResultType.AlreadyConfirmed && payStatus.paid ) { const fu = payStatus.contractTerms.fulfillment_url; if (fu) { setTimeout(() => { document.location.href = fu; }, 3000); } } }, []); if (!hook) { return ; } if (hook.hasError) { return ( Could not load pay status} error={hook} /> ); } const foundBalance = hook.response.balance.balances.find( (b) => Amounts.parseOrThrow(b.available).currency === Amounts.parseOrThrow(hook.response.payStatus.amountRaw).currency, ); const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined; const onClick = async (): Promise => { try { const res = await doPayment(hook.response.payStatus); setPayResult(res); } catch (e) { console.error(e); if (e instanceof Error) { setPayErrMsg(e.message); } } }; 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, goToWalletManualWithdraw, balance, }: PaymentRequestViewProps): VNode { const { i18n } = useTranslationContext(); let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw); const contractTerms: ContractTerms = payStatus.contractTerms; if (!contractTerms) { return ( Could not load contract terms from merchant or wallet backend. } /> ); } 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 ( 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) && ( Total to pay} text={} kind="negative" /> )} Purchase amount} text={} kind="neutral" /> {Amounts.isNonZero(totalFees) && ( Fee} text={} 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" /> )} {contractTerms.products && contractTerms.products.length > 0 && ( )}
Cancel
); } function ProductList({ products }: { products: Product[] }): VNode { const { i18n } = useTranslationContext(); return ( List of products
{products.map((p, i) => { if (p.price) { const pPrice = Amounts.parseOrThrow(p.price); return (
{p.quantity ?? 1} x {p.description}{" "} {Amounts.stringify(pPrice)}
{Amounts.stringify( Amounts.mult(pPrice, p.quantity ?? 1).amount, )}
); } return (
{p.quantity ?? 1} x {p.description}
Total {` `} {p.price ? ( `${Amounts.stringifyValue( Amounts.mult( Amounts.parseOrThrow(p.price), p.quantity ?? 1, ).amount, )} ${p}` ) : ( free )}
); })}
); }