summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/PaymentButtons.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx239
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 &nbsp;
+ {<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 &nbsp;
+ <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";
+}