This commit is contained in:
Sebastian 2023-01-04 11:24:58 -03:00
parent 7d02e42123
commit 24cac493dd
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
21 changed files with 1006 additions and 817 deletions

View File

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

View File

@ -65,23 +65,25 @@ export const BasicExample = (): VNode => (
</a>
</p>
<Banner
elements={[
{
icon: <SignalWifiOffIcon color="gray" />,
description: (
<Typography>
You have lost connection to the internet. This app is offline.
</Typography>
),
},
]}
// elements={[
// {
// icon: <SignalWifiOffIcon color="gray" />,
// description: (
// <Typography>
// You have lost connection to the internet. This app is offline.
// </Typography>
// ),
// },
// ]}
confirm={{
label: "turn on wifi",
action: async () => {
return;
},
}}
/>
>
<div />
</Banner>
</Wrapper>
</Fragment>
);
@ -92,31 +94,33 @@ export const PendingOperation = (): VNode => (
<Banner
title="PENDING TRANSACTIONS"
style={{ backgroundColor: "lightcyan", padding: 8 }}
elements={[
{
icon: (
<Avatar
style={{
border: "solid blue 1px",
color: "blue",
boxSizing: "border-box",
}}
>
P
</Avatar>
),
description: (
<Fragment>
<Typography inline bold>
EUR 37.95
</Typography>
&nbsp;
<Typography inline>- 5 feb 2022</Typography>
</Fragment>
),
},
]}
/>
// elements={[
// {
// icon: (
// <Avatar
// style={{
// border: "solid blue 1px",
// color: "blue",
// boxSizing: "border-box",
// }}
// >
// P
// </Avatar>
// ),
// description: (
// <Fragment>
// <Typography inline bold>
// EUR 37.95
// </Typography>
// &nbsp;
// <Typography inline>- 5 feb 2022</Typography>
// </Fragment>
// ),
// },
// ]}
>
asd
</Banner>
</Wrapper>
</Fragment>
);

View File

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

View File

@ -0,0 +1,153 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
AmountJson,
Amounts,
PreparePayResult,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "./Amount.js";
import { Part } from "./Part.js";
import { QR } from "./QR.js";
import { LinkSuccess, WarningBox } from "./styled/index.js";
import { useTranslationContext } from "../context/translation.js";
import { Button } from "../mui/Button.js";
import { ButtonHandler } from "../mui/handlers.js";
import { assertUnreachable } from "../utils/index.js";
interface Props {
payStatus: PreparePayResult;
payHandler: ButtonHandler | undefined;
balance: AmountJson | undefined;
uri: string;
amount: AmountJson;
goToWalletManualWithdraw: (currency: string) => Promise<void>;
}
export function PaymentButtons({
payStatus,
uri,
payHandler,
balance,
amount,
goToWalletManualWithdraw,
}: Props): VNode {
const { i18n } = useTranslationContext();
if (payStatus.status === PreparePayResultType.PaymentPossible) {
const privateUri = `${uri}&n=${payStatus.noncePriv}`;
return (
<Fragment>
<section>
<Button
variant="contained"
color="success"
onClick={payHandler?.onClick}
>
<i18n.Translate>
Pay &nbsp;
{<Amount value={amount} />}
</i18n.Translate>
</Button>
</section>
<PayWithMobile uri={privateUri} />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
let BalanceMessage = "";
if (!balance) {
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
} else {
const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
if (balanceShouldBeEnough) {
BalanceMessage = i18n.str`Could not find enough coins to pay. Even if you have enough ${balance.currency} some restriction may apply.`;
} else {
BalanceMessage = i18n.str`Your current balance is not enough.`;
}
}
const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
return (
<Fragment>
<section>
<WarningBox>{BalanceMessage}</WarningBox>
</section>
<section>
<Button
variant="contained"
color="success"
onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
>
<i18n.Translate>Get digital cash</i18n.Translate>
</Button>
</section>
<PayWithMobile uri={uriPrivate} />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
return (
<Fragment>
<section>
{payStatus.paid && payStatus.contractTerms.fulfillment_message && (
<Part
title={<i18n.Translate>Merchant message</i18n.Translate>}
text={payStatus.contractTerms.fulfillment_message}
kind="neutral"
/>
)}
</section>
{!payStatus.paid && <PayWithMobile uri={uri} />}
</Fragment>
);
}
assertUnreachable(payStatus);
}
function PayWithMobile({ uri }: { uri: string }): VNode {
const { i18n } = useTranslationContext();
const [showQR, setShowQR] = useState<boolean>(false);
return (
<section>
<LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
{!showQR ? (
<i18n.Translate>Pay with a mobile phone</i18n.Translate>
) : (
<i18n.Translate>Hide QR</i18n.Translate>
)}
</LinkSuccess>
{showQR && (
<div>
<QR text={uri} />
<i18n.Translate>
Scan the QR code or &nbsp;
<a href={uri}>
<i18n.Translate>click here</i18n.Translate>
</a>
</i18n.Translate>
</div>
)}
</section>
);
}

View File

@ -26,6 +26,7 @@ import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Avatar } from "../mui/Avatar.js";
import { Grid } from "../mui/Grid.js";
import { Typography } from "../mui/Typography.js";
import Banner from "./Banner.js";
import { Time } from "./Time.js";
@ -34,6 +35,11 @@ interface Props extends JSX.HTMLAttributes {
goToTransaction: (id: string) => Promise<void>;
}
/**
* this cache will save the tx from the previous render
*/
const cache = { tx: [] as Transaction[] };
export function PendingTransactions({ goToTransaction }: Props): VNode {
const api = useBackendContext();
const state = useAsyncAsHook(() =>
@ -49,12 +55,13 @@ export function PendingTransactions({ goToTransaction }: Props): VNode {
const transactions =
!state || state.hasError
? []
? cache.tx
: state.response.transactions.filter((t) => t.pending);
if (!state || state.hasError || !transactions.length) {
if (!transactions.length) {
return <Fragment />;
}
cache.tx = transactions;
return (
<PendingTransactionsView
goToTransaction={goToTransaction}
@ -72,46 +79,67 @@ export function PendingTransactionsView({
}): VNode {
const { i18n } = useTranslationContext();
return (
<Banner
titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
<div
style={{
backgroundColor: "lightcyan",
maxHeight: 150,
padding: 8,
flexGrow: 1,
maxWidth: 500,
overflowY: transactions.length > 3 ? "scroll" : "hidden",
display: "flex",
justifyContent: "center",
}}
elements={transactions.map((t) => {
const amount = Amounts.parseOrThrow(t.amountEffective);
return {
icon: (
<Avatar
style={{
border: "solid blue 1px",
color: "blue",
boxSizing: "border-box",
>
<Banner
titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
style={{
backgroundColor: "lightcyan",
maxHeight: 150,
padding: 8,
flexGrow: 1,
maxWidth: 500,
overflowY: transactions.length > 3 ? "scroll" : "hidden",
}}
>
{transactions.map((t, i) => {
const amount = Amounts.parseOrThrow(t.amountEffective);
return (
<Grid
container
item
xs={1}
key={i}
wrap="nowrap"
role="button"
spacing={1}
alignItems="center"
onClick={() => {
goToTransaction(t.transactionId);
}}
>
{t.type.substring(0, 1)}
</Avatar>
),
action: () => goToTransaction(t.transactionId),
description: (
<Fragment>
<Typography inline bold>
{amount.currency} {Amounts.stringifyValue(amount)}
</Typography>
&nbsp;-&nbsp;
<Time
timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
format="dd MMMM yyyy"
/>
</Fragment>
),
};
})}
/>
<Grid item xs={"auto"}>
<Avatar
style={{
border: "solid blue 1px",
color: "blue",
boxSizing: "border-box",
}}
>
{t.type.substring(0, 1)}
</Avatar>
</Grid>
<Grid item>
<Typography inline bold>
{amount.currency} {Amounts.stringifyValue(amount)}
</Typography>
&nbsp;-&nbsp;
<Time
timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
format="dd MMMM yyyy"
/>
</Grid>
</Grid>
);
})}
</Banner>
</div>
);
}

View File

@ -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>
);
}

View File

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

View File

@ -29,7 +29,7 @@ const initial = wxApi;
const Context = createContext<Type>(initial);
type Props = Partial<WxApiType> & {
type Props = Partial<Type> & {
children: ComponentChildren;
};

View File

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

View File

@ -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 &nbsp;
<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 &nbsp;
{<Amount value={amount} />}
</i18n.Translate>
</Button>
</section>
<PayWithMobile uri={privateUri} />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
let BalanceMessage = "";
if (!balance) {
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
} else {
const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
if (balanceShouldBeEnough) {
BalanceMessage = i18n.str`Could not find enough coins to pay. Even if you have enough ${balance.currency} some restriction may apply.`;
} else {
BalanceMessage = i18n.str`Your current balance is not enough.`;
}
}
const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
return (
<Fragment>
<section>
<WarningBox>{BalanceMessage}</WarningBox>
</section>
<section>
<Button
variant="contained"
color="success"
onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
>
<i18n.Translate>Get digital cash</i18n.Translate>
</Button>
</section>
<PayWithMobile uri={uriPrivate} />
</Fragment>
);
}
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
return (
<Fragment>
<section>
{payStatus.paid && payStatus.contractTerms.fulfillment_message && (
<Part
title={<i18n.Translate>Merchant message</i18n.Translate>}
text={payStatus.contractTerms.fulfillment_message}
kind="neutral"
/>
)}
</section>
{!payStatus.paid && <PayWithMobile uri={uri} />}
</Fragment>
);
}
assertUnreachable(payStatus);
}

View File

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

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

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

View File

@ -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>
);
};

View File

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

View File

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

View File

@ -144,7 +144,6 @@ function useUrlState<T>(
}
export function useComponentState({
currency,
onBack,
onComplete,
onPaymentRequired,

View File

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

View File

@ -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>
);
}

View File

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