aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/cta/Payment/views.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx393
1 files changed, 393 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
new file mode 100644
index 000000000..a8c9a640a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -0,0 +1,393 @@
+/*
+ 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 {
+ Amounts,
+ ConfirmPayResultType,
+ ContractTerms,
+ PreparePayResultType,
+ Product,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+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 { QR } from "../../components/QR.js";
+import {
+ Link,
+ LinkSuccess,
+ SmallLightText,
+ SubTitle,
+ SuccessBox,
+ WalletAction,
+ WarningBox,
+} from "../../components/styled/index.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+export function LoadingUriView({ error }: State.LoadingUriError): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <LoadingError
+ title={<i18n.Translate>Could not load pay status</i18n.Translate>}
+ error={error}
+ />
+ );
+}
+
+type SupportedStates =
+ | State.Ready
+ | State.Confirmed
+ | State.NoBalanceForCurrency
+ | State.NoEnoughBalance;
+
+export function BaseView(state: SupportedStates): VNode {
+ const { i18n } = useTranslationContext();
+ const contractTerms: ContractTerms = state.payStatus.contractTerms;
+
+ return (
+ <WalletAction>
+ <LogoHeader />
+
+ <SubTitle>
+ <i18n.Translate>Digital cash payment</i18n.Translate>
+ </SubTitle>
+
+ <ShowImportantMessage state={state} />
+
+ <section>
+ {state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
+ Amounts.isNonZero(state.totalFees) && (
+ <Part
+ big
+ title={<i18n.Translate>Total to pay</i18n.Translate>}
+ text={<Amount value={state.payStatus.amountEffective} />}
+ kind="negative"
+ />
+ )}
+ <Part
+ big
+ title={<i18n.Translate>Purchase amount</i18n.Translate>}
+ text={<Amount value={state.payStatus.amountRaw} />}
+ kind="neutral"
+ />
+ {Amounts.isNonZero(state.totalFees) && (
+ <Fragment>
+ <Part
+ big
+ title={<i18n.Translate>Fee</i18n.Translate>}
+ text={<Amount value={state.totalFees} />}
+ kind="negative"
+ />
+ </Fragment>
+ )}
+ <Part
+ title={<i18n.Translate>Merchant</i18n.Translate>}
+ text={contractTerms.merchant.name}
+ kind="neutral"
+ />
+ <Part
+ title={<i18n.Translate>Purchase</i18n.Translate>}
+ text={contractTerms.summary}
+ kind="neutral"
+ />
+ {contractTerms.order_id && (
+ <Part
+ title={<i18n.Translate>Receipt</i18n.Translate>}
+ text={`#${contractTerms.order_id}`}
+ kind="neutral"
+ />
+ )}
+ {contractTerms.products && contractTerms.products.length > 0 && (
+ <ProductList products={contractTerms.products} />
+ )}
+ </section>
+ <ButtonsSection
+ state={state}
+ goToWalletManualWithdraw={state.goToWalletManualWithdraw}
+ />
+ <section>
+ <Link upperCased onClick={state.goBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Link>
+ </section>
+ </WalletAction>
+ );
+}
+
+export function ProductList({ products }: { products: Product[] }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <SmallLightText style={{ margin: ".5em" }}>
+ <i18n.Translate>List of products</i18n.Translate>
+ </SmallLightText>
+ <dl>
+ {products.map((p, i) => {
+ if (p.price) {
+ const pPrice = Amounts.parseOrThrow(p.price);
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img
+ src={p.image ? p.image : undefined}
+ style={{ width: 32, height: 32 }}
+ />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}{" "}
+ <span style={{ color: "gray" }}>
+ {Amounts.stringify(pPrice)}
+ </span>
+ </dt>
+ <dd>
+ <b>
+ {Amounts.stringify(
+ Amounts.mult(pPrice, p.quantity ?? 1).amount,
+ )}
+ </b>
+ </dd>
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img src={p.image} style={{ width: 32, height: 32 }} />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}
+ </dt>
+ <dd>
+ <i18n.Translate>Total</i18n.Translate>
+ {` `}
+ {p.price ? (
+ `${Amounts.stringifyValue(
+ Amounts.mult(
+ Amounts.parseOrThrow(p.price),
+ p.quantity ?? 1,
+ ).amount,
+ )} ${p}`
+ ) : (
+ <i18n.Translate>free</i18n.Translate>
+ )}
+ </dd>
+ </div>
+ </div>
+ );
+ })}
+ </dl>
+ </Fragment>
+ );
+}
+
+function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
+ const { i18n } = useTranslationContext();
+ const { payStatus } = state;
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ if (payStatus.paid) {
+ if (payStatus.contractTerms.fulfillment_url) {
+ return (
+ <SuccessBox>
+ <i18n.Translate>
+ Already paid, you are going to be redirected to{" "}
+ <a href={payStatus.contractTerms.fulfillment_url}>
+ {payStatus.contractTerms.fulfillment_url}
+ </a>
+ </i18n.Translate>
+ </SuccessBox>
+ );
+ }
+ return (
+ <SuccessBox>
+ <i18n.Translate>Already paid</i18n.Translate>
+ </SuccessBox>
+ );
+ }
+ return (
+ <WarningBox>
+ <i18n.Translate>Already claimed</i18n.Translate>
+ </WarningBox>
+ );
+ }
+
+ if (state.status == "confirmed") {
+ const { payResult, payHandler } = state;
+ if (payHandler.error) {
+ return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
+ }
+ if (payResult.type === ConfirmPayResultType.Done) {
+ return (
+ <SuccessBox>
+ <h3>
+ <i18n.Translate>Payment complete</i18n.Translate>
+ </h3>
+ <p>
+ {!payResult.contractTerms.fulfillment_message ? (
+ payResult.contractTerms.fulfillment_url ? (
+ <i18n.Translate>
+ You are going to be redirected to $
+ {payResult.contractTerms.fulfillment_url}
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>You can close this page.</i18n.Translate>
+ )
+ ) : (
+ payResult.contractTerms.fulfillment_message
+ )}
+ </p>
+ </SuccessBox>
+ );
+ }
+ }
+ return <Fragment />;
+}
+
+function PayWithMobile({ state }: { state: State.Ready }): 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)}>
+ {!showQR ? (
+ <i18n.Translate>Pay with a mobile phone</i18n.Translate>
+ ) : (
+ <i18n.Translate>Hide QR</i18n.Translate>
+ )}
+ </LinkSuccess>
+ {showQR && (
+ <div>
+ <QR text={privateUri} />
+ <i18n.Translate>
+ Scan the QR code or
+ <a href={privateUri}>
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </div>
+ )}
+ </section>
+ );
+}
+
+function ButtonsSection({
+ state,
+ goToWalletManualWithdraw,
+}: {
+ state: SupportedStates;
+ goToWalletManualWithdraw: (currency: string) => Promise<void>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ if (state.status === "ready") {
+ const { payStatus } = state;
+ if (payStatus.status === PreparePayResultType.PaymentPossible) {
+ return (
+ <Fragment>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.payHandler.onClick}
+ >
+ <i18n.Translate>
+ Pay {<Amount value={payStatus.amountEffective} />}
+ </i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile state={state} />
+ </Fragment>
+ );
+ }
+ if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+ let BalanceMessage = "";
+ if (!state.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;
+ 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.`;
+ } else {
+ BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
+ }
+ }
+ return (
+ <Fragment>
+ <section>
+ <WarningBox>{BalanceMessage}</WarningBox>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={() => goToWalletManualWithdraw(state.amount.currency)}
+ >
+ <i18n.Translate>Withdraw digital cash</i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile state={state} />
+ </Fragment>
+ );
+ }
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ return (
+ <Fragment>
+ <section>
+ {payStatus.paid &&
+ state.payStatus.contractTerms.fulfillment_message && (
+ <Part
+ title={<i18n.Translate>Merchant message</i18n.Translate>}
+ text={state.payStatus.contractTerms.fulfillment_message}
+ kind="neutral"
+ />
+ )}
+ </section>
+ {!payStatus.paid && <PayWithMobile state={state} />}
+ </Fragment>
+ );
+ }
+ }
+
+ if (state.status === "confirmed") {
+ if (state.payResult.type === ConfirmPayResultType.Pending) {
+ return (
+ <section>
+ <div>
+ <p>
+ <i18n.Translate>Processing</i18n.Translate>...
+ </p>
+ </div>
+ </section>
+ );
+ }
+ }
+
+ return <Fragment />;
+}