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> | 
