diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/PaymentButtons.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/components/PaymentButtons.tsx | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx new file mode 100644 index 000000000..7fa0376c9 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx @@ -0,0 +1,239 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU 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. + + GNU 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + AmountJson, + Amounts, + PaymentInsufficientBalanceDetails, + PreparePayResult, + PreparePayResultType, + TranslatedString, + parsePayUri, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { useBackendContext } from "../context/backend.js"; +import { Button } from "../mui/Button.js"; +import { ButtonHandler } from "../mui/handlers.js"; +import { assertUnreachable } from "../utils/index.js"; +import { Amount } from "./Amount.js"; +import { Part } from "./Part.js"; +import { QR } from "./QR.js"; +import { LinkSuccess, WarningBox } from "./styled/index.js"; + +interface Props { + payStatus: PreparePayResult; + payHandler: ButtonHandler | undefined; + uri: string; + amount: AmountJson; + goToWalletManualWithdraw: (currency: string) => Promise<void>; +} + +export function PaymentButtons({ + payStatus, + uri, + payHandler, + amount, + goToWalletManualWithdraw, +}: Props): VNode { + const { i18n } = useTranslationContext(); + if (payStatus.status === PreparePayResultType.PaymentPossible) { + return ( + <Fragment> + <section> + <Button + variant="contained" + color="success" + onClick={payHandler?.onClick} + > + <i18n.Translate> + Pay + {<Amount value={amount} />} + </i18n.Translate> + </Button> + </section> + <PayWithMobile uri={uri} /> + </Fragment> + ); + } + + if (payStatus.status === PreparePayResultType.InsufficientBalance) { + const reason = getReason(payStatus.balanceDetails); + + let BalanceMessage = ""; + switch (reason) { + case "age-acceptable": { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + payStatus.balanceDetails.balanceAgeAcceptable, + )} ${amount.currency} to pay for this contract which is restricted.`; + break; + } + case "available": { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + payStatus.balanceDetails.balanceAvailable, + )} ${amount.currency} available.`; + break; + } + case "merchant-acceptable": { + BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( + payStatus.balanceDetails.balanceReceiverAcceptable, + )} ${amount.currency + } . To know more you can check which exchange and auditors the merchant trust.`; + break; + } + case "merchant-depositable": { + BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( + payStatus.balanceDetails.balanceReceiverDepositable, + )} ${amount.currency + } . To know more you can check which wire methods the merchant accepts.`; + break; + } + case "material": { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + payStatus.balanceDetails.balanceMaterial, + )} ${amount.currency + } to spend right know. There are some coins that need to be refreshed.`; + break; + } + case "fee-gap": { + BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue( + Amounts.stringify( + Amounts.sub( + amount, + payStatus.balanceDetails.maxEffectiveSpendAmount, + ).amount, + ), + )} ${amount.currency + } more balance in your wallet or ask your merchant to cover more of the fees.`; + break; + } + default: + assertUnreachable(reason); + } + + return ( + <Fragment> + <section> + <WarningBox>{BalanceMessage}</WarningBox> + </section> + <section> + <Button + variant="contained" + color="success" + onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))} + > + <i18n.Translate>Get digital cash</i18n.Translate> + </Button> + </section> + <PayWithMobile uri={uri} /> + </Fragment> + ); + } + if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { + return ( + <Fragment> + <section> + {payStatus.paid && payStatus.contractTerms.fulfillment_message && ( + <Part + title={i18n.str`Merchant message`} + text={ + payStatus.contractTerms.fulfillment_message as TranslatedString + } + kind="neutral" + /> + )} + </section> + </Fragment> + ); + } + + assertUnreachable(payStatus); +} + +function PayWithMobile({ uri }: { uri: string }): VNode { + const { i18n } = useTranslationContext(); + const api = useBackendContext(); + + const payUri = parsePayUri(uri); + + const [showQR, setShowQR] = useState<string | undefined>(undefined); + async function sharePrivatePaymentURI() { + if (!payUri) { + return; + } + if (!showQR) { + const result = await api.wallet.call(WalletApiOperation.SharePayment, { + merchantBaseUrl: payUri.merchantBaseUrl, + orderId: payUri.orderId, + }); + setShowQR(result.privatePayUri); + } else { + setShowQR(undefined); + } + } + if (!payUri) { + return <Fragment /> + } + return ( + <section> + <LinkSuccess upperCased onClick={sharePrivatePaymentURI}> + {!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`} + </LinkSuccess> + {showQR && ( + <div> + <QR text={showQR} /> + <i18n.Translate> + Scan the QR code or + <a href={showQR}> + <i18n.Translate>click here</i18n.Translate> + </a> + </i18n.Translate> + </div> + )} + </section> + ); +} + +type NoEnoughBalanceReason = + | "available" + | "material" + | "age-acceptable" + | "merchant-acceptable" + | "merchant-depositable" + | "fee-gap"; + +function getReason( + info: PaymentInsufficientBalanceDetails, +): NoEnoughBalanceReason { + if (Amounts.cmp(info.amountRequested, info.balanceAvailable) > 0) { + return "available"; + } + if (Amounts.cmp(info.amountRequested, info.balanceMaterial) > 0) { + return "material"; + } + if (Amounts.cmp(info.amountRequested, info.balanceAgeAcceptable) > 0) { + return "age-acceptable"; + } + if (Amounts.cmp(info.amountRequested, info.balanceReceiverAcceptable) > 0) { + return "merchant-acceptable"; + } + if (Amounts.cmp(info.amountRequested, info.balanceReceiverDepositable) > 0) { + return "merchant-depositable"; + } + return "fee-gap"; +} |