diff options
Diffstat (limited to 'packages/taler-wallet-webextension')
21 files changed, 997 insertions, 808 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index ab36af376..1c26450f7 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -45,6 +45,7 @@ import warningIcon from "./svg/warning_24px.svg"; * @author sebasjm */ +// eslint-disable-next-line @typescript-eslint/ban-types type PageLocation<DynamicPart extends object> = { pattern: string; (params: DynamicPart): string; @@ -62,6 +63,7 @@ function replaceAll( return result; } +// eslint-disable-next-line @typescript-eslint/ban-types function pageDefinition<T extends object>(pattern: string): PageLocation<T> { const patternParams = pattern.match(/(:[\w?]*)/g); if (!patternParams) @@ -133,7 +135,8 @@ export const Pages = { ), }; -export function PopupNavBar({ path = "" }: { path?: string }): VNode { +export type PopupNavBarOptions = "balance" | "backup" | "dev"; +export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode { const api = useBackendContext(); const hook = useAsyncAsHook(async () => { return await api.wallet.call( @@ -146,13 +149,10 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode { const { i18n } = useTranslationContext(); return ( <NavigationHeader> - <a - href={Pages.balance} - class={path.startsWith("/balance") ? "active" : ""} - > + <a href={Pages.balance} class={path === "balance" ? "active" : ""}> <i18n.Translate>Balance</i18n.Translate> </a> - <a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}> + <a href={Pages.backup} class={path === "backup" ? "active" : ""}> <i18n.Translate>Backup</i18n.Translate> </a> <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}> @@ -185,8 +185,8 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode { </NavigationHeader> ); } - -export function WalletNavBar({ path = "" }: { path?: string }): VNode { +export type WalletNavBarOptions = "balance" | "backup" | "dev"; +export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); @@ -196,21 +196,16 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode { {}, ); }); - const attentionCount = !hook || hook.hasError ? 0 : hook.response.total; + const attentionCount = + (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0; return ( <NavigationHeaderHolder> <NavigationHeader> - <a - href={Pages.balance} - class={path.startsWith("/balance") ? "active" : ""} - > + <a href={Pages.balance} class={path === "balance" ? "active" : ""}> <i18n.Translate>Balance</i18n.Translate> </a> - <a - href={Pages.backup} - class={path.startsWith("/backup") ? "active" : ""} - > + <a href={Pages.backup} class={path === "backup" ? "active" : ""}> <i18n.Translate>Backup</i18n.Translate> </a> @@ -223,7 +218,7 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode { )} <JustInDevMode> - <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}> + <a href={Pages.dev} class={path === "dev" ? "active" : ""}> <i18n.Translate>Dev</i18n.Translate> </a> </JustInDevMode> 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> - - <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> + // + // <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 + {<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 + <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> - - - <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> + - + <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; diff --git a/packages/taler-wallet-webextension/src/context/backend.ts b/packages/taler-wallet-webextension/src/context/backend.ts index e00a70080..280fb266d 100644 --- a/packages/taler-wallet-webextension/src/context/backend.ts +++ b/packages/taler-wallet-webextension/src/context/backend.ts @@ -29,7 +29,7 @@ const initial = wxApi; const Context = createContext<Type>(initial); -type Props = Partial<WxApiType> & { +type Props = Partial<Type> & { children: ComponentChildren; }; diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx index 8484680bf..a53fa881a 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -23,7 +23,7 @@ import { Part } from "../../components/Part.js"; import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; import { Time } from "../../components/Time.js"; import { useTranslationContext } from "../../context/translation.js"; -import { ButtonsSection } from "../Payment/views.js"; +import { PaymentButtons } from "../../components/PaymentButtons"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { @@ -83,7 +83,7 @@ export function ReadyView( kind="neutral" /> </section> - <ButtonsSection + <PaymentButtons amount={amount} balance={balance} payStatus={payStatus} diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx index 0f6cb5c28..efc8bcfc4 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -16,35 +16,17 @@ import { AbsoluteTime, - AmountJson, Amounts, MerchantContractTerms as ContractTerms, - PreparePayResult, 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 { ErrorMessage } from "../../components/ErrorMessage.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 { PaymentButtons } from "../../components/PaymentButtons.js"; +import { Link, SuccessBox, WarningBox } from "../../components/styled/index.js"; import { Time } from "../../components/Time.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"; import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js"; import { State } from "./index.js"; @@ -77,44 +59,12 @@ export function BaseView(state: SupportedStates): VNode { ? Amounts.parseOrThrow(state.payStatus.amountEffective) : state.amount, }; - // const totalFees = Amounts.sub(price.effective, price.raw).amount; return ( - <WalletAction> - <LogoHeader /> - - <SubTitle> - <i18n.Translate>Digital cash payment</i18n.Translate> - </SubTitle> - + <Fragment> <ShowImportantMessage state={state} /> <section style={{ textAlign: "left" }}> - {/* {state.payStatus.status !== PreparePayResultType.InsufficientBalance && - Amounts.isNonZero(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(totalFees) && ( - <Fragment> - <Part - big - title={<i18n.Translate>Fee</i18n.Translate>} - text={<Amount value={totalFees} />} - kind="negative" - /> - </Fragment> - )} */} <Part title={<i18n.Translate>Purchase</i18n.Translate>} text={contractTerms.summary} @@ -125,9 +75,6 @@ export function BaseView(state: SupportedStates): VNode { text={<MerchantDetails merchant={contractTerms.merchant} />} kind="neutral" /> - {/* <pre>{JSON.stringify(price)}</pre> - <hr /> - <pre>{JSON.stringify(state.payStatus, undefined, 2)}</pre> */} <Part title={<i18n.Translate>Details</i18n.Translate>} text={ @@ -166,7 +113,7 @@ export function BaseView(state: SupportedStates): VNode { /> )} </section> - <ButtonsSection + <PaymentButtons amount={state.amount} balance={state.balance} payStatus={state.payStatus} @@ -179,75 +126,6 @@ export function BaseView(state: SupportedStates): VNode { <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> ); } @@ -284,124 +162,3 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode { return <Fragment />; } - -export 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 - <a href={uri}> - <i18n.Translate>click here</i18n.Translate> - </a> - </i18n.Translate> - </div> - )} - </section> - ); -} - -interface ButtonSectionProps { - payStatus: PreparePayResult; - payHandler: ButtonHandler | undefined; - balance: AmountJson | undefined; - uri: string; - amount: AmountJson; - goToWalletManualWithdraw: (currency: string) => Promise<void>; -} - -export function ButtonsSection({ - payStatus, - uri, - payHandler, - balance, - amount, - goToWalletManualWithdraw, -}: ButtonSectionProps): 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 - {<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); -} diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx index 4b5ff70dd..a55bc43dd 100644 --- a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx @@ -23,7 +23,7 @@ import { Part } from "../../components/Part.js"; import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; -import { ProductList } from "../Payment/views.js"; +import { ProductList } from "../../components/ProductList.js"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index 5c35151c8..9dbe24b7e 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -14,12 +14,12 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { ExchangeTosStatus } 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 { SelectList } from "../../components/SelectList.js"; @@ -27,17 +27,14 @@ import { Input, Link, LinkSuccess, - SubTitle, SvgIcon, - WalletAction, } from "../../components/styled/index.js"; +import { TermsOfService } from "../../components/TermsOfService/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; import editIcon from "../../svg/edit_24px.svg"; import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; -import { TermsOfService } from "../../components/TermsOfService/index.js"; import { State } from "./index.js"; -import { ExchangeTosStatus } from "@gnu-taler/taler-util"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -68,12 +65,7 @@ export function SuccessView(state: State.Success): VNode { const currentTosVersionIsAccepted = state.currentExchange.tosStatus === ExchangeTosStatus.Accepted; return ( - <WalletAction> - <LogoHeader /> - <SubTitle> - <i18n.Translate>Digital cash withdrawal</i18n.Translate> - </SubTitle> - + <Fragment> {state.doWithdrawal.error && ( <ErrorTalerOperation title={ @@ -161,7 +153,7 @@ export function SuccessView(state: State.Success): VNode { <i18n.Translate>Cancel</i18n.Translate> </Link> </section> - </WalletAction> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx index 8186c6790..9cae0d048 100644 --- a/packages/taler-wallet-webextension/src/popup/Application.tsx +++ b/packages/taler-wallet-webextension/src/popup/Application.tsx @@ -21,7 +21,7 @@ */ import { createHashHistory } from "history"; -import { Fragment, h, VNode } from "preact"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; import Router, { route, Route } from "preact-router"; import { Match } from "preact-router/match"; import { useEffect, useState } from "preact/hooks"; @@ -34,15 +34,28 @@ import { useTranslationContext, } from "../context/translation.js"; import { useTalerActionURL } from "../hooks/useTalerActionURL.js"; -import { Pages, PopupNavBar } from "../NavigationBar.js"; +import { PopupNavBarOptions, Pages, PopupNavBar } from "../NavigationBar.js"; import { platform } from "../platform/api.js"; import { BackupPage } from "../wallet/BackupPage.js"; import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js"; import { BalancePage } from "./BalancePage.js"; import { TalerActionFound } from "./TalerActionFound.js"; -function CheckTalerActionComponent(): VNode { - const [action] = useTalerActionURL(); +export function Application(): VNode { + return ( + <TranslationProvider> + <DevContextProvider> + <IoCProviderForRuntime> + <ApplicationView /> + </IoCProviderForRuntime> + </DevContextProvider> + </TranslationProvider> + ); +} +function ApplicationView(): VNode { + const hash_history = createHashHistory(); + + const [action, setDismissed] = useTalerActionURL(); const actionUri = action?.uri; @@ -52,116 +65,110 @@ function CheckTalerActionComponent(): VNode { } }, [actionUri]); - return <Fragment />; -} + async function redirectToTxInfo(tid: string): Promise<void> { + redirectTo(Pages.balanceTransaction({ tid })); + } -export function Application(): VNode { - const hash_history = createHashHistory(); return ( - <TranslationProvider> - <DevContextProvider> - {({ devMode }: { devMode: boolean }) => ( - <IoCProviderForRuntime> - <PendingTransactions - goToTransaction={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) + <Router history={hash_history}> + <Route + path={Pages.balance} + component={() => ( + <PopupTemplate path="balance" goToTransaction={redirectToTxInfo}> + <BalancePage + goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))} + goToWalletDeposit={(currency: string) => + redirectTo(Pages.sendCash({ amount: `${currency}:0` })) + } + goToWalletHistory={(currency: string) => + redirectTo(Pages.balanceHistory({ currency })) } /> - <Match> - {({ path }: { path: string }) => <PopupNavBar path={path} />} - </Match> - <CheckTalerActionComponent /> - <PopupBox devMode={devMode}> - <Router history={hash_history}> - <Route - path={Pages.balance} - component={BalancePage} - goToWalletManualWithdraw={() => - redirectTo(Pages.receiveCash({})) - } - goToWalletDeposit={(currency: string) => - redirectTo(Pages.sendCash({ amount: `${currency}:0` })) - } - goToWalletHistory={(currency: string) => - redirectTo(Pages.balanceHistory({ currency })) - } - /> - - <Route - path={Pages.cta.pattern} - component={function Action({ action }: { action: string }) { - const [, setDismissed] = useTalerActionURL(); - - return ( - <TalerActionFound - url={decodeURIComponent(action)} - onDismiss={() => { - setDismissed(true); - return redirectTo(Pages.balance); - }} - /> - ); - }} - /> - - <Route - path={Pages.backup} - component={BackupPage} - onAddProvider={() => redirectTo(Pages.backupProviderAdd)} - /> - <Route - path={Pages.backupProviderDetail.pattern} - component={ProviderDetailPage} - onBack={() => redirectTo(Pages.backup)} - /> - - <Route - path={Pages.balanceTransaction.pattern} - component={RedirectToWalletPage} - /> - <Route - path={Pages.ctaWithdrawManual.pattern} - component={RedirectToWalletPage} - /> - <Route - path={Pages.balanceDeposit.pattern} - component={RedirectToWalletPage} - /> - <Route - path={Pages.balanceHistory.pattern} - component={RedirectToWalletPage} - /> - <Route - path={Pages.backupProviderAdd} - component={RedirectToWalletPage} - /> - <Route - path={Pages.receiveCash.pattern} - component={RedirectToWalletPage} - /> - <Route - path={Pages.sendCash.pattern} - component={RedirectToWalletPage} - /> - <Route path={Pages.qr} component={RedirectToWalletPage} /> - <Route path={Pages.settings} component={RedirectToWalletPage} /> - <Route - path={Pages.settingsExchangeAdd.pattern} - component={RedirectToWalletPage} - /> - <Route path={Pages.dev} component={RedirectToWalletPage} /> - <Route - path={Pages.notifications} - component={RedirectToWalletPage} - /> - - <Route default component={Redirect} to={Pages.balance} /> - </Router> - </PopupBox> - </IoCProviderForRuntime> + </PopupTemplate> )} - </DevContextProvider> - </TranslationProvider> + /> + + <Route + path={Pages.cta.pattern} + component={function Action({ action }: { action: string }) { + // const [, setDismissed] = useTalerActionURL(); + + return ( + <PopupTemplate> + <TalerActionFound + url={decodeURIComponent(action)} + onDismiss={() => { + setDismissed(true); + return redirectTo(Pages.balance); + }} + /> + </PopupTemplate> + ); + }} + /> + + <Route + path={Pages.backup} + component={() => ( + <PopupTemplate path="backup" goToTransaction={redirectToTxInfo}> + <BackupPage + onAddProvider={() => redirectTo(Pages.backupProviderAdd)} + /> + </PopupTemplate> + )} + /> + <Route + path={Pages.backupProviderDetail.pattern} + component={({ pid }: { pid: string }) => ( + <PopupTemplate path="backup"> + <ProviderDetailPage + onPayProvider={(uri: string) => + redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) + } + onWithdraw={(amount: string) => + redirectTo(Pages.receiveCash({ amount })) + } + pid={pid} + onBack={() => redirectTo(Pages.backup)} + /> + </PopupTemplate> + )} + /> + + <Route + path={Pages.balanceTransaction.pattern} + component={RedirectToWalletPage} + /> + <Route + path={Pages.ctaWithdrawManual.pattern} + component={RedirectToWalletPage} + /> + <Route + path={Pages.balanceDeposit.pattern} + component={RedirectToWalletPage} + /> + <Route + path={Pages.balanceHistory.pattern} + component={RedirectToWalletPage} + /> + <Route path={Pages.backupProviderAdd} component={RedirectToWalletPage} /> + <Route + path={Pages.receiveCash.pattern} + component={RedirectToWalletPage} + /> + <Route path={Pages.sendCash.pattern} component={RedirectToWalletPage} /> + <Route path={Pages.ctaPay} component={RedirectToWalletPage} /> + <Route path={Pages.qr} component={RedirectToWalletPage} /> + <Route path={Pages.settings} component={RedirectToWalletPage} /> + <Route + path={Pages.settingsExchangeAdd.pattern} + component={RedirectToWalletPage} + /> + <Route path={Pages.dev} component={RedirectToWalletPage} /> + <Route path={Pages.notifications} component={RedirectToWalletPage} /> + + <Route default component={Redirect} to={Pages.balance} /> + </Router> ); } @@ -195,3 +202,24 @@ function Redirect({ to }: { to: string }): null { }); return null; } + +function PopupTemplate({ + path, + children, + goToTransaction, +}: { + path?: PopupNavBarOptions; + children: ComponentChildren; + goToTransaction?: (id: string) => Promise<void>; +}): VNode { + return ( + <Fragment> + {/* <CheckTalerActionComponent /> */} + {goToTransaction ? ( + <PendingTransactions goToTransaction={goToTransaction} /> + ) : undefined} + <PopupNavBar path={path} /> + <PopupBox>{children}</PopupBox> + </Fragment> + ); +} diff --git a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts index c9327b8e6..82d11a15a 100644 --- a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts +++ b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts @@ -69,7 +69,7 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary { } else if (ArrayBuffer.isView(requestBody)) { myBody = requestBody; } else if (typeof requestBody === "object") { - myBody = JSON.stringify(myBody); + myBody = JSON.stringify(requestBody); } else { throw Error("unsupported request body type"); } @@ -127,8 +127,6 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary { }); } - // FIXME: "Content-Type: application/json" goes here, - // after Sebastian suggestion. postJson( url: string, body: any, diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx index 8834b8084..a7b8a4d06 100644 --- a/packages/taler-wallet-webextension/src/stories.tsx +++ b/packages/taler-wallet-webextension/src/stories.tsx @@ -20,7 +20,11 @@ */ import { Fragment, FunctionComponent, h } from "preact"; import { LogoHeader } from "./components/LogoHeader.js"; -import { PopupBox, WalletBox } from "./components/styled/index.js"; +import { + PopupBox, + WalletAction, + WalletBox, +} from "./components/styled/index.js"; import { strings } from "./i18n/strings.js"; import { PopupNavBar, WalletNavBar } from "./NavigationBar.js"; @@ -72,7 +76,7 @@ function getWrapperForGroup(group: string): FunctionComponent { return function WalletWrapper({ children }: any) { return ( <Fragment> - <WalletBox>{children}</WalletBox> + <WalletAction>{children}</WalletAction> </Fragment> ); }; diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts index c2d7c10a8..ad4eabf15 100644 --- a/packages/taler-wallet-webextension/src/utils/index.ts +++ b/packages/taler-wallet-webextension/src/utils/index.ts @@ -74,7 +74,7 @@ export async function queryToSlashKeys<T>(url: string): Promise<T> { return timeout(3000, query); } -export type StateFunc<S> = (p: S) => VNode; +export type StateFunc<S> = (p: S) => VNode | null; export type StateViewMap<StateType extends { status: string }> = { [S in StateType as S["status"]]: StateFunc<S>; diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts index 94020069b..10fcd84ce 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts @@ -32,7 +32,6 @@ import { } from "./views.js"; export interface Props { - currency: string; onBack: () => Promise<void>; onComplete: (pid: string) => Promise<void>; onPaymentRequired: (uri: string) => Promise<void>; diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts index 32c48be91..1b30ed0cd 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts @@ -144,7 +144,6 @@ function useUrlState<T>( } export function useComponentState({ - currency, onBack, onComplete, onPaymentRequired, diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts index 9abb672fa..3241a3ab0 100644 --- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts @@ -26,7 +26,6 @@ import { Props } from "./index.js"; import { useComponentState } from "./state.js"; const props: Props = { - currency: "KUDOS", onBack: nullFunction, onComplete: nullFunction, onPaymentRequired: nullFunction, diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index d150ebfaf..8b77e152c 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -20,352 +20,452 @@ * @author sebasjm */ +import { TranslatedString } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; -import { Fragment, h, VNode } from "preact"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; import Router, { route, Route } from "preact-router"; -import Match from "preact-router/match"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect } from "preact/hooks"; import { LogoHeader } from "../components/LogoHeader.js"; import PendingTransactions from "../components/PendingTransactions.js"; -import { SuccessBox, WalletBox } from "../components/styled/index.js"; +import { + SubTitle, + WalletAction, + WalletBox, +} from "../components/styled/index.js"; import { DevContextProvider } from "../context/devContext.js"; import { IoCProviderForRuntime } from "../context/iocContext.js"; import { TranslationProvider, useTranslationContext, } from "../context/translation.js"; +import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js"; +import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js"; +import { InvoicePayPage } from "../cta/InvoicePay/index.js"; import { PaymentPage } from "../cta/Payment/index.js"; +import { RecoveryPage } from "../cta/Recovery/index.js"; import { RefundPage } from "../cta/Refund/index.js"; import { TipPage } from "../cta/Tip/index.js"; +import { TransferCreatePage } from "../cta/TransferCreate/index.js"; +import { TransferPickupPage } from "../cta/TransferPickup/index.js"; import { WithdrawPageFromParams, WithdrawPageFromURI, } from "../cta/Withdraw/index.js"; -import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js"; -import { Pages, WalletNavBar } from "../NavigationBar.js"; -import { DeveloperPage } from "./DeveloperPage.js"; +import { WalletNavBarOptions, Pages, WalletNavBar } from "../NavigationBar.js"; +import { platform } from "../platform/api.js"; +import { AddBackupProviderPage } from "./AddBackupProvider/index.js"; import { BackupPage } from "./BackupPage.js"; import { DepositPage } from "./DepositPage/index.js"; +import { DestinationSelectionPage } from "./DestinationSelection/index.js"; +import { DeveloperPage } from "./DeveloperPage.js"; import { ExchangeAddPage } from "./ExchangeAddPage.js"; import { HistoryPage } from "./History.js"; +import { NotificationsPage } from "./Notifications/index.js"; import { ProviderDetailPage } from "./ProviderDetailPage.js"; +import { QrReaderPage } from "./QrReader.js"; import { SettingsPage } from "./Settings.js"; import { TransactionPage } from "./Transaction.js"; import { WelcomePage } from "./Welcome.js"; -import { QrReaderPage } from "./QrReader.js"; -import { platform } from "../platform/api.js"; -import { DestinationSelectionPage } from "./DestinationSelection/index.js"; -import { ExchangeSelectionPage } from "./ExchangeSelection/index.js"; -import { TransferCreatePage } from "../cta/TransferCreate/index.js"; -import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js"; -import { TransferPickupPage } from "../cta/TransferPickup/index.js"; -import { InvoicePayPage } from "../cta/InvoicePay/index.js"; -import { RecoveryPage } from "../cta/Recovery/index.js"; -import { AddBackupProviderPage } from "./AddBackupProvider/index.js"; -import { NotificationsPage } from "./Notifications/index.js"; export function Application(): VNode { - const [globalNotification, setGlobalNotification] = useState< - VNode | undefined - >(undefined); - const hash_history = createHashHistory(); - function clearNotification(): void { - setGlobalNotification(undefined); - } - function clearNotificationWhenMovingOut(): void { - // const movingOutFromNotification = - // globalNotification && e.url !== globalNotification.to; - if (globalNotification) { - //&& movingOutFromNotification) { - setGlobalNotification(undefined); - } - } const { i18n } = useTranslationContext(); + const hash_history = createHashHistory(); + async function redirectToTxInfo(tid: string): Promise<void> { + redirectTo(Pages.balanceTransaction({ tid })); + } return ( <TranslationProvider> <DevContextProvider> <IoCProviderForRuntime> - {/* <Match/> won't work in the first render if <Router /> is not called first */} - {/* https://github.com/preactjs/preact-router/issues/415 */} <Router history={hash_history}> - <Match default> - {({ path }: { path: string }) => { - if (path && path.startsWith("/cta")) return; - return ( - <Fragment> - <LogoHeader /> - <WalletNavBar path={path} /> - {shouldShowPendingOperations(path) && ( - <div - style={{ - backgroundColor: "lightcyan", - display: "flex", - justifyContent: "center", - }} - > - <PendingTransactions - goToTransaction={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - </div> - )} - </Fragment> - ); - }} - </Match> - </Router> - <WalletBox> - {globalNotification && ( - <SuccessBox onClick={clearNotification}> - <div>{globalNotification}</div> - </SuccessBox> - )} - <Router - history={hash_history} - onChange={clearNotificationWhenMovingOut} - > - <Route path={Pages.welcome} component={WelcomePage} /> - - {/** - * BALANCE - */} + <Route + path={Pages.welcome} + component={() => ( + <WalletTemplate> + <WelcomePage /> + </WalletTemplate> + )} + /> - <Route - path={Pages.balanceHistory.pattern} - component={HistoryPage} - goToWalletDeposit={(currency: string) => - redirectTo(Pages.sendCash({ amount: `${currency}:0` })) - } - goToWalletManualWithdraw={(currency?: string) => - redirectTo( - Pages.receiveCash({ - amount: !currency ? undefined : `${currency}:0`, - }), - ) - } - /> - <Route path={Pages.exchanges} component={ExchangeSelectionPage} /> - <Route - path={Pages.sendCash.pattern} - type="send" - component={DestinationSelectionPage} - goToWalletBankDeposit={(amount: string) => - redirectTo(Pages.balanceDeposit({ amount })) - } - goToWalletWalletSend={(amount: string) => - redirectTo(Pages.ctaTransferCreate({ amount })) - } - /> - <Route - path={Pages.receiveCash.pattern} - type="get" - component={DestinationSelectionPage} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.ctaWithdrawManual({ amount })) - } - goToWalletWalletInvoice={(amount?: string) => - redirectTo(Pages.ctaInvoiceCreate({ amount })) - } - /> + <Route + path={Pages.qr} + component={() => ( + <WalletTemplate goToTransaction={redirectToTxInfo}> + <QrReaderPage + onDetected={(talerActionUrl: string) => { + platform.openWalletURIFromPopup(talerActionUrl); + }} + /> + </WalletTemplate> + )} + /> - <Route - path={Pages.balanceTransaction.pattern} - component={TransactionPage} - goToWalletHistory={(currency?: string) => - redirectTo(Pages.balanceHistory({ currency })) - } - /> + <Route + path={Pages.settings} + component={() => ( + <WalletTemplate goToTransaction={redirectToTxInfo}> + <SettingsPage /> + </WalletTemplate> + )} + /> + <Route + path={Pages.notifications} + component={() => ( + <WalletTemplate> + <NotificationsPage /> + </WalletTemplate> + )} + /> + {/** + * SETTINGS + */} + <Route + path={Pages.settingsExchangeAdd.pattern} + component={() => ( + <WalletTemplate> + <ExchangeAddPage onBack={() => redirectTo(Pages.balance)} /> + </WalletTemplate> + )} + /> - <Route - path={Pages.balanceDeposit.pattern} - component={DepositPage} - onCancel={(currency: string) => { - redirectTo(Pages.balanceHistory({ currency })); - }} - onSuccess={(currency: string) => { - redirectTo(Pages.balanceHistory({ currency })); - setGlobalNotification( - <i18n.Translate> - All done, your transaction is in progress - </i18n.Translate>, - ); - }} - /> - {/** - * PENDING - */} - <Route - path={Pages.qr} - component={QrReaderPage} - onDetected={(talerActionUrl: string) => { - platform.openWalletURIFromPopup(talerActionUrl); - }} - /> + <Route + path={Pages.balanceHistory.pattern} + component={() => ( + <WalletTemplate + path="balance" + goToTransaction={redirectToTxInfo} + > + <HistoryPage + goToWalletDeposit={(currency: string) => + redirectTo(Pages.sendCash({ amount: `${currency}:0` })) + } + goToWalletManualWithdraw={(currency?: string) => + redirectTo( + Pages.receiveCash({ + amount: !currency ? undefined : `${currency}:0`, + }), + ) + } + /> + </WalletTemplate> + )} + /> + <Route + path={Pages.sendCash.pattern} + component={({ amount }: { amount?: string }) => ( + <WalletTemplate path="balance"> + <DestinationSelectionPage + type="send" + amount={amount} + goToWalletBankDeposit={(amount: string) => + redirectTo(Pages.balanceDeposit({ amount })) + } + goToWalletWalletSend={(amount: string) => + redirectTo(Pages.ctaTransferCreate({ amount })) + } + /> + </WalletTemplate> + )} + /> + <Route + path={Pages.receiveCash.pattern} + component={({ amount }: { amount?: string }) => ( + <WalletTemplate path="balance"> + <DestinationSelectionPage + type="get" + amount={amount} + goToWalletManualWithdraw={(amount?: string) => + redirectTo(Pages.ctaWithdrawManual({ amount })) + } + goToWalletWalletInvoice={(amount?: string) => + redirectTo(Pages.ctaInvoiceCreate({ amount })) + } + /> + </WalletTemplate> + )} + /> - <Route path={Pages.settings} component={SettingsPage} /> - <Route path={Pages.notifications} component={NotificationsPage} /> + <Route + path={Pages.balanceTransaction.pattern} + component={({ tid }: { tid: string }) => ( + <WalletTemplate path="balance"> + <TransactionPage + tid={tid} + goToWalletHistory={(currency?: string) => + redirectTo(Pages.balanceHistory({ currency })) + } + /> + </WalletTemplate> + )} + /> - {/** - * BACKUP - */} - <Route - path={Pages.backup} - component={BackupPage} - onAddProvider={() => redirectTo(Pages.backupProviderAdd)} - /> - <Route - path={Pages.backupProviderDetail.pattern} - component={ProviderDetailPage} - onPayProvider={(uri: string) => - redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) - } - onWithdraw={(amount: string) => - redirectTo(Pages.receiveCash({ amount })) - } - onBack={() => redirectTo(Pages.backup)} - /> - <Route - path={Pages.backupProviderAdd} - component={AddBackupProviderPage} - onPaymentRequired={(uri: string) => - redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) - } - onComplete={(pid: string) => - redirectTo(Pages.backupProviderDetail({ pid })) - } - onBack={() => redirectTo(Pages.backup)} - /> + <Route + path={Pages.balanceDeposit.pattern} + component={() => ( + <WalletTemplate path="balance"> + <DepositPage + onCancel={(currency: string) => { + redirectTo(Pages.balanceHistory({ currency })); + }} + onSuccess={(currency: string) => { + redirectTo(Pages.balanceHistory({ currency })); + }} + /> + </WalletTemplate> + )} + /> - {/** - * SETTINGS - */} - <Route - path={Pages.settingsExchangeAdd.pattern} - component={ExchangeAddPage} - onBack={() => redirectTo(Pages.balance)} - /> + <Route + path={Pages.backup} + component={() => ( + <WalletTemplate + path="backup" + goToTransaction={redirectToTxInfo} + > + <BackupPage + onAddProvider={() => redirectTo(Pages.backupProviderAdd)} + /> + </WalletTemplate> + )} + /> + <Route + path={Pages.backupProviderDetail.pattern} + component={({ pid }: { pid: string }) => ( + <WalletTemplate> + <ProviderDetailPage + pid={pid} + onPayProvider={(uri: string) => + redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) + } + onWithdraw={(amount: string) => + redirectTo(Pages.receiveCash({ amount })) + } + onBack={() => redirectTo(Pages.backup)} + /> + </WalletTemplate> + )} + /> + <Route + path={Pages.backupProviderAdd} + component={() => ( + <WalletTemplate> + <AddBackupProviderPage + onPaymentRequired={(uri: string) => + redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`) + } + onComplete={(pid: string) => + redirectTo(Pages.backupProviderDetail({ pid })) + } + onBack={() => redirectTo(Pages.backup)} + /> + </WalletTemplate> + )} + /> - {/** - * DEV - */} + {/** + * DEV + */} + <Route + path={Pages.dev} + component={() => ( + <WalletTemplate path="dev" goToTransaction={redirectToTxInfo}> + <DeveloperPage /> + </WalletTemplate> + )} + /> - <Route path={Pages.dev} component={DeveloperPage} /> + {/** + * CALL TO ACTION + */} + <Route + path={Pages.ctaPay} + component={({ talerPayUri }: { talerPayUri: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash payment`}> + <PaymentPage + talerPayUri={talerPayUri} + goToWalletManualWithdraw={(amount?: string) => + redirectTo(Pages.receiveCash({ amount })) + } + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaRefund} + component={({ talerRefundUri }: { talerRefundUri: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash refund`}> + <RefundPage + talerRefundUri={talerRefundUri} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaTips} + component={({ talerTipUri }: { talerTipUri: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash tip`}> + <TipPage + talerTipUri={talerTipUri} + onCancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaWithdraw} + component={({ + talerWithdrawUri, + }: { + talerWithdrawUri: string; + }) => ( + <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> + <WithdrawPageFromURI + talerWithdrawUri={talerWithdrawUri} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaWithdrawManual.pattern} + component={({ amount }: { amount: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}> + <WithdrawPageFromParams + amount={amount} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaDeposit} + component={({ + amountStr, + talerDepositUri, + }: { + amountStr: string; + talerDepositUri: string; + }) => ( + <CallToActionTemplate title={i18n.str`Digital cash deposit`}> + <DepositPageCTA + amountStr={amountStr} + talerDepositUri={talerDepositUri} + cancel={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaInvoiceCreate.pattern} + component={({ amount }: { amount: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash invoice`}> + <InvoiceCreatePage + amount={amount} + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaTransferCreate.pattern} + component={({ amount }: { amount: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash transfer`}> + <TransferCreatePage + amount={amount} + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaInvoicePay} + component={({ talerPayPullUri }: { talerPayPullUri: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash invoice`}> + <InvoicePayPage + talerPayPullUri={talerPayPullUri} + goToWalletManualWithdraw={(amount?: string) => + redirectTo(Pages.receiveCash({ amount })) + } + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaTransferPickup} + component={({ talerPayPushUri }: { talerPayPushUri: string }) => ( + <CallToActionTemplate title={i18n.str`Digital cash transfer`}> + <TransferPickupPage + talerPayPushUri={talerPayPushUri} + onClose={() => redirectTo(Pages.balance)} + onSuccess={(tid: string) => + redirectTo(Pages.balanceTransaction({ tid })) + } + /> + </CallToActionTemplate> + )} + /> + <Route + path={Pages.ctaRecovery} + component={({ + talerRecoveryUri, + }: { + talerRecoveryUri: string; + }) => ( + <CallToActionTemplate title={i18n.str`Digital cash recovery`}> + <RecoveryPage + talerRecoveryUri={talerRecoveryUri} + onCancel={() => redirectTo(Pages.balance)} + onSuccess={() => redirectTo(Pages.backup)} + /> + </CallToActionTemplate> + )} + /> - {/** - * CALL TO ACTION - */} - <Route - path={Pages.ctaPay} - component={PaymentPage} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.receiveCash({ amount })) - } - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaRefund} - component={RefundPage} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaTips} - component={TipPage} - onCancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaWithdraw} - component={WithdrawPageFromURI} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaWithdrawManual.pattern} - component={WithdrawPageFromParams} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaDeposit} - component={DepositPageCTA} - cancel={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaInvoiceCreate.pattern} - component={InvoiceCreatePage} - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaTransferCreate.pattern} - component={TransferCreatePage} - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaInvoicePay} - component={InvoicePayPage} - goToWalletManualWithdraw={(amount?: string) => - redirectTo(Pages.receiveCash({ amount })) - } - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaTransferPickup} - component={TransferPickupPage} - onClose={() => redirectTo(Pages.balance)} - onSuccess={(tid: string) => - redirectTo(Pages.balanceTransaction({ tid })) - } - /> - <Route - path={Pages.ctaRecovery} - component={RecoveryPage} - onCancel={() => redirectTo(Pages.balance)} - onSuccess={() => redirectTo(Pages.backup)} - /> + {/** + * NOT FOUND + * all redirects should be at the end + */} + <Route + path={Pages.balance} + component={() => <Redirect to={Pages.balanceHistory({})} />} + /> - {/** - * NOT FOUND - * all redirects should be at the end - */} - <Route - path={Pages.balance} - component={Redirect} - to={Pages.balanceHistory({})} - /> - - <Route - default - component={Redirect} - to={Pages.balanceHistory({})} - /> - </Router> - </WalletBox> + <Route + default + component={() => <Redirect to={Pages.balanceHistory({})} />} + /> + </Router> </IoCProviderForRuntime> </DevContextProvider> </TranslationProvider> @@ -403,3 +503,40 @@ function shouldShowPendingOperations(url: string): boolean { Pages.backup, ].some((p) => matchesRoute(url, p)); } + +function CallToActionTemplate({ + title, + children, +}: { + title: TranslatedString; + children: ComponentChildren; +}): VNode { + return ( + <WalletAction> + <LogoHeader /> + <SubTitle>{title}</SubTitle> + {children} + </WalletAction> + ); +} + +function WalletTemplate({ + path, + children, + goToTransaction, +}: { + path?: WalletNavBarOptions; + children: ComponentChildren; + goToTransaction?: (id: string) => Promise<void>; +}): VNode { + return ( + <Fragment> + <LogoHeader /> + <WalletNavBar path={path} /> + {goToTransaction ? ( + <PendingTransactions goToTransaction={goToTransaction} /> + ) : undefined} + <WalletBox>{children}</WalletBox> + </Fragment> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index 4805c03ca..74e7ce611 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -92,6 +92,7 @@ type CoinsInfo = CoinDumpJson["coins"]; type CalculatedCoinfInfo = { ageKeysCount: number | undefined; denom_value: number; + denom_fraction: number; //remain_value: number; status: string; from_refresh: boolean; @@ -151,7 +152,8 @@ export function View({ } prev[cur.exchange_base_url].push({ ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length, - denom_value: parseFloat(Amounts.stringifyValue(denom)), + denom_value: denom.value, + denom_fraction: denom.fraction, // remain_value: parseFloat( // Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)), // ), @@ -340,7 +342,10 @@ export function View({ {Object.keys(money_by_exchange).map((ex, idx) => { const allcoins = money_by_exchange[ex]; allcoins.sort((a, b) => { - return b.denom_value - a.denom_value; + if (b.denom_value !== a.denom_value) { + return b.denom_value - a.denom_value; + } + return b.denom_fraction - a.denom_fraction; }); const coins = allcoins.reduce( @@ -407,11 +412,31 @@ function ShowAllCoins({ const { i18n } = useTranslationContext(); const [collapsedSpent, setCollapsedSpent] = useState(true); const [collapsedUnspent, setCollapsedUnspent] = useState(false); - const total = coins.usable.reduce((prev, cur) => prev + cur.denom_value, 0); + const totalUsable = coins.usable.reduce( + (prev, cur) => + Amounts.add(prev, { + currency: "NONE", + fraction: cur.denom_fraction, + value: cur.denom_value, + }).amount, + Amounts.zeroOfCurrency("NONE"), + ); + const totalSpent = coins.spent.reduce( + (prev, cur) => + Amounts.add(prev, { + currency: "NONE", + fraction: cur.denom_fraction, + value: cur.denom_value, + }).amount, + Amounts.zeroOfCurrency("NONE"), + ); return ( <Fragment> <p> - <b>{ex}</b>: {total} {currencies[ex]} + <b>{ex}</b>: {Amounts.stringifyValue(totalUsable)} {currencies[ex]} + </p> + <p> + spent: {Amounts.stringifyValue(totalSpent)} {currencies[ex]} </p> <p onClick={() => setCollapsedUnspent(true)}> <b> |