aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/components')
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.stories.tsx76
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx153
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx102
-rw-r--r--packages/taler-wallet-webextension/src/components/ProductList.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx2
6 files changed, 359 insertions, 103 deletions
diff --git a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
index 39012480b..60b100478 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
@@ -65,23 +65,25 @@ export const BasicExample = (): VNode => (
</a>
</p>
<Banner
- elements={[
- {
- icon: <SignalWifiOffIcon color="gray" />,
- description: (
- <Typography>
- You have lost connection to the internet. This app is offline.
- </Typography>
- ),
- },
- ]}
+ // elements={[
+ // {
+ // icon: <SignalWifiOffIcon color="gray" />,
+ // description: (
+ // <Typography>
+ // You have lost connection to the internet. This app is offline.
+ // </Typography>
+ // ),
+ // },
+ // ]}
confirm={{
label: "turn on wifi",
action: async () => {
return;
},
}}
- />
+ >
+ <div />
+ </Banner>
</Wrapper>
</Fragment>
);
@@ -92,31 +94,33 @@ export const PendingOperation = (): VNode => (
<Banner
title="PENDING TRANSACTIONS"
style={{ backgroundColor: "lightcyan", padding: 8 }}
- elements={[
- {
- icon: (
- <Avatar
- style={{
- border: "solid blue 1px",
- color: "blue",
- boxSizing: "border-box",
- }}
- >
- P
- </Avatar>
- ),
- description: (
- <Fragment>
- <Typography inline bold>
- EUR 37.95
- </Typography>
- &nbsp;
- <Typography inline>- 5 feb 2022</Typography>
- </Fragment>
- ),
- },
- ]}
- />
+ // elements={[
+ // {
+ // icon: (
+ // <Avatar
+ // style={{
+ // border: "solid blue 1px",
+ // color: "blue",
+ // boxSizing: "border-box",
+ // }}
+ // >
+ // P
+ // </Avatar>
+ // ),
+ // description: (
+ // <Fragment>
+ // <Typography inline bold>
+ // EUR 37.95
+ // </Typography>
+ // &nbsp;
+ // <Typography inline>- 5 feb 2022</Typography>
+ // </Fragment>
+ // ),
+ // },
+ // ]}
+ >
+ asd
+ </Banner>
</Wrapper>
</Fragment>
);
diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx b/packages/taler-wallet-webextension/src/components/Banner.tsx
index f95647d42..a91fd384f 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.tsx
@@ -13,21 +13,20 @@
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 { h, Fragment, VNode, JSX } from "preact";
-import { Divider } from "../mui/Divider.js";
+import { ComponentChildren, Fragment, h, JSX, VNode } from "preact";
import { Button } from "../mui/Button.js";
-import { Typography } from "../mui/Typography.js";
-import { Avatar } from "../mui/Avatar.js";
+import { Divider } from "../mui/Divider.js";
import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
titleHead?: VNode;
- elements: {
- icon?: VNode;
- description: VNode;
- action?: () => void;
- }[];
+ children: ComponentChildren;
+ // elements: {
+ // icon?: VNode;
+ // description: VNode;
+ // action?: () => void;
+ // }[];
confirm?: {
label: string;
action: () => Promise<void>;
@@ -36,8 +35,9 @@ interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
export function Banner({
titleHead,
- elements,
+ children,
confirm,
+ href,
...rest
}: Props): VNode {
return (
@@ -49,25 +49,7 @@ export function Banner({
</Grid>
)}
<Grid container columns={1}>
- {elements.map((e, i) => (
- <Grid
- container
- item
- xs={1}
- key={i}
- wrap="nowrap"
- spacing={1}
- alignItems="center"
- onClick={e.action}
- >
- {e.icon && (
- <Grid item xs={"auto"}>
- <Avatar>{e.icon}</Avatar>
- </Grid>
- )}
- <Grid item>{e.description}</Grid>
- </Grid>
- ))}
+ {children}
</Grid>
{confirm && (
<Grid container justifyContent="flex-end" spacing={8}>
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..def1e16eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -0,0 +1,153 @@
+/*
+ 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,
+ PreparePayResult,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Amount } from "./Amount.js";
+import { Part } from "./Part.js";
+import { QR } from "./QR.js";
+import { LinkSuccess, WarningBox } from "./styled/index.js";
+import { useTranslationContext } from "../context/translation.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
+
+interface Props {
+ payStatus: PreparePayResult;
+ payHandler: ButtonHandler | undefined;
+ balance: AmountJson | undefined;
+ uri: string;
+ amount: AmountJson;
+ goToWalletManualWithdraw: (currency: string) => Promise<void>;
+}
+
+export function PaymentButtons({
+ payStatus,
+ uri,
+ payHandler,
+ balance,
+ amount,
+ goToWalletManualWithdraw,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ if (payStatus.status === PreparePayResultType.PaymentPossible) {
+ const privateUri = `${uri}&n=${payStatus.noncePriv}`;
+
+ return (
+ <Fragment>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={payHandler?.onClick}
+ >
+ <i18n.Translate>
+ Pay &nbsp;
+ {<Amount value={amount} />}
+ </i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile uri={privateUri} />
+ </Fragment>
+ );
+ }
+
+ if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+ let BalanceMessage = "";
+ if (!balance) {
+ BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
+ } else {
+ const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
+ if (balanceShouldBeEnough) {
+ 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.`;
+ }
+ }
+ const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
+
+ 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={uriPrivate} />
+ </Fragment>
+ );
+ }
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ return (
+ <Fragment>
+ <section>
+ {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>
+ );
+ }
+
+ assertUnreachable(payStatus);
+}
+
+function PayWithMobile({ uri }: { uri: string }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [showQR, setShowQR] = useState<boolean>(false);
+
+ 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={uri} />
+ <i18n.Translate>
+ Scan the QR code or &nbsp;
+ <a href={uri}>
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </div>
+ )}
+ </section>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
index 85b43fb4e..e41ff2836 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -26,6 +26,7 @@ import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Avatar } from "../mui/Avatar.js";
+import { Grid } from "../mui/Grid.js";
import { Typography } from "../mui/Typography.js";
import Banner from "./Banner.js";
import { Time } from "./Time.js";
@@ -34,6 +35,11 @@ interface Props extends JSX.HTMLAttributes {
goToTransaction: (id: string) => Promise<void>;
}
+/**
+ * this cache will save the tx from the previous render
+ */
+const cache = { tx: [] as Transaction[] };
+
export function PendingTransactions({ goToTransaction }: Props): VNode {
const api = useBackendContext();
const state = useAsyncAsHook(() =>
@@ -49,12 +55,13 @@ export function PendingTransactions({ goToTransaction }: Props): VNode {
const transactions =
!state || state.hasError
- ? []
+ ? cache.tx
: state.response.transactions.filter((t) => t.pending);
- if (!state || state.hasError || !transactions.length) {
+ if (!transactions.length) {
return <Fragment />;
}
+ cache.tx = transactions;
return (
<PendingTransactionsView
goToTransaction={goToTransaction}
@@ -72,46 +79,67 @@ export function PendingTransactionsView({
}): VNode {
const { i18n } = useTranslationContext();
return (
- <Banner
- titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
+ <div
style={{
backgroundColor: "lightcyan",
- maxHeight: 150,
- padding: 8,
- flexGrow: 1,
- maxWidth: 500,
- overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ display: "flex",
+ justifyContent: "center",
}}
- elements={transactions.map((t) => {
- const amount = Amounts.parseOrThrow(t.amountEffective);
- return {
- icon: (
- <Avatar
- style={{
- border: "solid blue 1px",
- color: "blue",
- boxSizing: "border-box",
+ >
+ <Banner
+ titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
+ style={{
+ backgroundColor: "lightcyan",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1,
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ }}
+ >
+ {transactions.map((t, i) => {
+ const amount = Amounts.parseOrThrow(t.amountEffective);
+ return (
+ <Grid
+ container
+ item
+ xs={1}
+ key={i}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToTransaction(t.transactionId);
}}
>
- {t.type.substring(0, 1)}
- </Avatar>
- ),
- action: () => goToTransaction(t.transactionId),
- description: (
- <Fragment>
- <Typography inline bold>
- {amount.currency} {Amounts.stringifyValue(amount)}
- </Typography>
- &nbsp;-&nbsp;
- <Time
- timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
- format="dd MMMM yyyy"
- />
- </Fragment>
- ),
- };
- })}
- />
+ <Grid item xs={"auto"}>
+ <Avatar
+ style={{
+ border: "solid blue 1px",
+ color: "blue",
+ boxSizing: "border-box",
+ }}
+ >
+ {t.type.substring(0, 1)}
+ </Avatar>
+ </Grid>
+
+ <Grid item>
+ <Typography inline bold>
+ {amount.currency} {Amounts.stringifyValue(amount)}
+ </Typography>
+ &nbsp;-&nbsp;
+ <Time
+ timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
+ format="dd MMMM yyyy"
+ />
+ </Grid>
+ </Grid>
+ );
+ })}
+ </Banner>
+ </div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/ProductList.tsx b/packages/taler-wallet-webextension/src/components/ProductList.tsx
new file mode 100644
index 000000000..a78733179
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ProductList.tsx
@@ -0,0 +1,89 @@
+/*
+ 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, Product } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { SmallLightText } from "./styled/index.js";
+import { useTranslationContext } from "../context/translation.js";
+
+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>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 7a3c27c73..8e98f75eb 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -159,7 +159,7 @@ export const Middle = styled.div`
height: 100%;
`;
-export const PopupBox = styled.div<{ noPadding?: boolean; devMode?: boolean }>`
+export const PopupBox = styled.div<{ noPadding?: boolean }>`
height: 290px;
width: 500px;
overflow-y: visible;