fix #7522
This commit is contained in:
parent
7d02e42123
commit
24cac493dd
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -29,7 +29,7 @@ const initial = wxApi;
|
||||
|
||||
const Context = createContext<Type>(initial);
|
||||
|
||||
type Props = Partial<WxApiType> & {
|
||||
type Props = Partial<Type> & {
|
||||
children: ComponentChildren;
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -144,7 +144,6 @@ function useUrlState<T>(
|
||||
}
|
||||
|
||||
export function useComponentState({
|
||||
currency,
|
||||
onBack,
|
||||
onComplete,
|
||||
onPaymentRequired,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
<Route
|
||||
path={Pages.welcome}
|
||||
component={() => (
|
||||
<WalletTemplate>
|
||||
<WelcomePage />
|
||||
</WalletTemplate>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Pages.qr}
|
||||
component={() => (
|
||||
<WalletTemplate goToTransaction={redirectToTxInfo}>
|
||||
<QrReaderPage
|
||||
onDetected={(talerActionUrl: string) => {
|
||||
platform.openWalletURIFromPopup(talerActionUrl);
|
||||
}}
|
||||
/>
|
||||
</WalletTemplate>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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.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.balanceTransaction.pattern}
|
||||
component={({ tid }: { tid: string }) => (
|
||||
<WalletTemplate path="balance">
|
||||
<TransactionPage
|
||||
tid={tid}
|
||||
goToWalletHistory={(currency?: string) =>
|
||||
redirectTo(Pages.balanceHistory({ currency }))
|
||||
}
|
||||
/>
|
||||
</WalletTemplate>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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
|
||||
*/}
|
||||
<Route
|
||||
path={Pages.dev}
|
||||
component={() => (
|
||||
<WalletTemplate path="dev" goToTransaction={redirectToTxInfo}>
|
||||
<DeveloperPage />
|
||||
</WalletTemplate>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/**
|
||||
* 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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/**
|
||||
* 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>
|
||||
{globalNotification && (
|
||||
<SuccessBox onClick={clearNotification}>
|
||||
<div>{globalNotification}</div>
|
||||
</SuccessBox>
|
||||
)}
|
||||
<Router
|
||||
history={hash_history}
|
||||
onChange={clearNotificationWhenMovingOut}
|
||||
>
|
||||
<Route path={Pages.welcome} component={WelcomePage} />
|
||||
|
||||
{/**
|
||||
* BALANCE
|
||||
*/}
|
||||
|
||||
<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.balanceTransaction.pattern}
|
||||
component={TransactionPage}
|
||||
goToWalletHistory={(currency?: string) =>
|
||||
redirectTo(Pages.balanceHistory({ currency }))
|
||||
}
|
||||
/>
|
||||
|
||||
<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.settings} component={SettingsPage} />
|
||||
<Route path={Pages.notifications} component={NotificationsPage} />
|
||||
|
||||
{/**
|
||||
* 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)}
|
||||
/>
|
||||
|
||||
{/**
|
||||
* SETTINGS
|
||||
*/}
|
||||
<Route
|
||||
path={Pages.settingsExchangeAdd.pattern}
|
||||
component={ExchangeAddPage}
|
||||
onBack={() => redirectTo(Pages.balance)}
|
||||
/>
|
||||
|
||||
{/**
|
||||
* DEV
|
||||
*/}
|
||||
|
||||
<Route path={Pages.dev} component={DeveloperPage} />
|
||||
|
||||
{/**
|
||||
* 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({})}
|
||||
/>
|
||||
|
||||
<Route
|
||||
default
|
||||
component={Redirect}
|
||||
to={Pages.balanceHistory({})}
|
||||
/>
|
||||
</Router>
|
||||
</WalletBox>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user