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 * @author sebasjm
*/ */
// eslint-disable-next-line @typescript-eslint/ban-types
type PageLocation<DynamicPart extends object> = { type PageLocation<DynamicPart extends object> = {
pattern: string; pattern: string;
(params: DynamicPart): string; (params: DynamicPart): string;
@ -62,6 +63,7 @@ function replaceAll(
return result; return result;
} }
// eslint-disable-next-line @typescript-eslint/ban-types
function pageDefinition<T extends object>(pattern: string): PageLocation<T> { function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
const patternParams = pattern.match(/(:[\w?]*)/g); const patternParams = pattern.match(/(:[\w?]*)/g);
if (!patternParams) 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 api = useBackendContext();
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.wallet.call( return await api.wallet.call(
@ -146,13 +149,10 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<NavigationHeader> <NavigationHeader>
<a <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<a href={Pages.backup} class={path.startsWith("/backup") ? "active" : ""}> <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate> <i18n.Translate>Backup</i18n.Translate>
</a> </a>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}> <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
@ -185,8 +185,8 @@ export function PopupNavBar({ path = "" }: { path?: string }): VNode {
</NavigationHeader> </NavigationHeader>
); );
} }
export type WalletNavBarOptions = "balance" | "backup" | "dev";
export function WalletNavBar({ path = "" }: { path?: string }): VNode { export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const api = useBackendContext(); 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 ( return (
<NavigationHeaderHolder> <NavigationHeaderHolder>
<NavigationHeader> <NavigationHeader>
<a <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
href={Pages.balance}
class={path.startsWith("/balance") ? "active" : ""}
>
<i18n.Translate>Balance</i18n.Translate> <i18n.Translate>Balance</i18n.Translate>
</a> </a>
<a <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
href={Pages.backup}
class={path.startsWith("/backup") ? "active" : ""}
>
<i18n.Translate>Backup</i18n.Translate> <i18n.Translate>Backup</i18n.Translate>
</a> </a>
@ -223,7 +218,7 @@ export function WalletNavBar({ path = "" }: { path?: string }): VNode {
)} )}
<JustInDevMode> <JustInDevMode>
<a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}> <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate> <i18n.Translate>Dev</i18n.Translate>
</a> </a>
</JustInDevMode> </JustInDevMode>

View File

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

View File

@ -13,21 +13,20 @@
You should have received a copy of the GNU General Public License along with 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/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { h, Fragment, VNode, JSX } from "preact"; import { ComponentChildren, Fragment, h, JSX, VNode } from "preact";
import { Divider } from "../mui/Divider.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Typography } from "../mui/Typography.js"; import { Divider } from "../mui/Divider.js";
import { Avatar } from "../mui/Avatar.js";
import { Grid } from "../mui/Grid.js"; import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js"; import { Paper } from "../mui/Paper.js";
interface Props extends JSX.HTMLAttributes<HTMLDivElement> { interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
titleHead?: VNode; titleHead?: VNode;
elements: { children: ComponentChildren;
icon?: VNode; // elements: {
description: VNode; // icon?: VNode;
action?: () => void; // description: VNode;
}[]; // action?: () => void;
// }[];
confirm?: { confirm?: {
label: string; label: string;
action: () => Promise<void>; action: () => Promise<void>;
@ -36,8 +35,9 @@ interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
export function Banner({ export function Banner({
titleHead, titleHead,
elements, children,
confirm, confirm,
href,
...rest ...rest
}: Props): VNode { }: Props): VNode {
return ( return (
@ -49,25 +49,7 @@ export function Banner({
</Grid> </Grid>
)} )}
<Grid container columns={1}> <Grid container columns={1}>
{elements.map((e, i) => ( {children}
<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>
))}
</Grid> </Grid>
{confirm && ( {confirm && (
<Grid container justifyContent="flex-end" spacing={8}> <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 { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Avatar } from "../mui/Avatar.js"; import { Avatar } from "../mui/Avatar.js";
import { Grid } from "../mui/Grid.js";
import { Typography } from "../mui/Typography.js"; import { Typography } from "../mui/Typography.js";
import Banner from "./Banner.js"; import Banner from "./Banner.js";
import { Time } from "./Time.js"; import { Time } from "./Time.js";
@ -34,6 +35,11 @@ interface Props extends JSX.HTMLAttributes {
goToTransaction: (id: string) => Promise<void>; 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 { export function PendingTransactions({ goToTransaction }: Props): VNode {
const api = useBackendContext(); const api = useBackendContext();
const state = useAsyncAsHook(() => const state = useAsyncAsHook(() =>
@ -49,12 +55,13 @@ export function PendingTransactions({ goToTransaction }: Props): VNode {
const transactions = const transactions =
!state || state.hasError !state || state.hasError
? [] ? cache.tx
: state.response.transactions.filter((t) => t.pending); : state.response.transactions.filter((t) => t.pending);
if (!state || state.hasError || !transactions.length) { if (!transactions.length) {
return <Fragment />; return <Fragment />;
} }
cache.tx = transactions;
return ( return (
<PendingTransactionsView <PendingTransactionsView
goToTransaction={goToTransaction} goToTransaction={goToTransaction}
@ -72,46 +79,67 @@ export function PendingTransactionsView({
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<Banner <div
titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
style={{ style={{
backgroundColor: "lightcyan", backgroundColor: "lightcyan",
maxHeight: 150, display: "flex",
padding: 8, justifyContent: "center",
flexGrow: 1,
maxWidth: 500,
overflowY: transactions.length > 3 ? "scroll" : "hidden",
}} }}
elements={transactions.map((t) => { >
const amount = Amounts.parseOrThrow(t.amountEffective); <Banner
return { titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
icon: ( style={{
<Avatar backgroundColor: "lightcyan",
style={{ maxHeight: 150,
border: "solid blue 1px", padding: 8,
color: "blue", flexGrow: 1,
boxSizing: "border-box", 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)} <Grid item xs={"auto"}>
</Avatar> <Avatar
), style={{
action: () => goToTransaction(t.transactionId), border: "solid blue 1px",
description: ( color: "blue",
<Fragment> boxSizing: "border-box",
<Typography inline bold> }}
{amount.currency} {Amounts.stringifyValue(amount)} >
</Typography> {t.type.substring(0, 1)}
&nbsp;-&nbsp; </Avatar>
<Time </Grid>
timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
format="dd MMMM yyyy" <Grid item>
/> <Typography inline bold>
</Fragment> {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%; height: 100%;
`; `;
export const PopupBox = styled.div<{ noPadding?: boolean; devMode?: boolean }>` export const PopupBox = styled.div<{ noPadding?: boolean }>`
height: 290px; height: 290px;
width: 500px; width: 500px;
overflow-y: visible; overflow-y: visible;

View File

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

View File

@ -23,7 +23,7 @@ import { Part } from "../../components/Part.js";
import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { Time } from "../../components/Time.js"; import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { ButtonsSection } from "../Payment/views.js"; import { PaymentButtons } from "../../components/PaymentButtons";
import { State } from "./index.js"; import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@ -83,7 +83,7 @@ export function ReadyView(
kind="neutral" kind="neutral"
/> />
</section> </section>
<ButtonsSection <PaymentButtons
amount={amount} amount={amount}
balance={balance} balance={balance}
payStatus={payStatus} payStatus={payStatus}

View File

@ -16,35 +16,17 @@
import { import {
AbsoluteTime, AbsoluteTime,
AmountJson,
Amounts, Amounts,
MerchantContractTerms as ContractTerms, MerchantContractTerms as ContractTerms,
PreparePayResult,
PreparePayResultType, PreparePayResultType,
Product,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; 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 { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js"; import { PaymentButtons } from "../../components/PaymentButtons.js";
import { import { Link, SuccessBox, WarningBox } from "../../components/styled/index.js";
Link,
LinkSuccess,
SmallLightText,
SubTitle,
SuccessBox,
WalletAction,
WarningBox,
} from "../../components/styled/index.js";
import { Time } from "../../components/Time.js"; import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.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 { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js"; import { State } from "./index.js";
@ -77,44 +59,12 @@ export function BaseView(state: SupportedStates): VNode {
? Amounts.parseOrThrow(state.payStatus.amountEffective) ? Amounts.parseOrThrow(state.payStatus.amountEffective)
: state.amount, : state.amount,
}; };
// const totalFees = Amounts.sub(price.effective, price.raw).amount;
return ( return (
<WalletAction> <Fragment>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital cash payment</i18n.Translate>
</SubTitle>
<ShowImportantMessage state={state} /> <ShowImportantMessage state={state} />
<section style={{ textAlign: "left" }}> <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 <Part
title={<i18n.Translate>Purchase</i18n.Translate>} title={<i18n.Translate>Purchase</i18n.Translate>}
text={contractTerms.summary} text={contractTerms.summary}
@ -125,9 +75,6 @@ export function BaseView(state: SupportedStates): VNode {
text={<MerchantDetails merchant={contractTerms.merchant} />} text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral" kind="neutral"
/> />
{/* <pre>{JSON.stringify(price)}</pre>
<hr />
<pre>{JSON.stringify(state.payStatus, undefined, 2)}</pre> */}
<Part <Part
title={<i18n.Translate>Details</i18n.Translate>} title={<i18n.Translate>Details</i18n.Translate>}
text={ text={
@ -166,7 +113,7 @@ export function BaseView(state: SupportedStates): VNode {
/> />
)} )}
</section> </section>
<ButtonsSection <PaymentButtons
amount={state.amount} amount={state.amount}
balance={state.balance} balance={state.balance}
payStatus={state.payStatus} payStatus={state.payStatus}
@ -179,75 +126,6 @@ export function BaseView(state: SupportedStates): VNode {
<i18n.Translate>Cancel</i18n.Translate> <i18n.Translate>Cancel</i18n.Translate>
</Link> </Link>
</section> </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> </Fragment>
); );
} }
@ -284,124 +162,3 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
return <Fragment />; 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 { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import { ProductList } from "../Payment/views.js"; import { ProductList } from "../../components/ProductList.js";
import { State } from "./index.js"; import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { 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/> 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 { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js"; import { Amount } from "../../components/Amount.js";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js"; import { QR } from "../../components/QR.js";
import { SelectList } from "../../components/SelectList.js"; import { SelectList } from "../../components/SelectList.js";
@ -27,17 +27,14 @@ import {
Input, Input,
Link, Link,
LinkSuccess, LinkSuccess,
SubTitle,
SvgIcon, SvgIcon,
WalletAction,
} from "../../components/styled/index.js"; } from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import editIcon from "../../svg/edit_24px.svg"; import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js"; import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { State } from "./index.js"; import { State } from "./index.js";
import { ExchangeTosStatus } from "@gnu-taler/taler-util";
export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -68,12 +65,7 @@ export function SuccessView(state: State.Success): VNode {
const currentTosVersionIsAccepted = const currentTosVersionIsAccepted =
state.currentExchange.tosStatus === ExchangeTosStatus.Accepted; state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
return ( return (
<WalletAction> <Fragment>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital cash withdrawal</i18n.Translate>
</SubTitle>
{state.doWithdrawal.error && ( {state.doWithdrawal.error && (
<ErrorTalerOperation <ErrorTalerOperation
title={ title={
@ -161,7 +153,7 @@ export function SuccessView(state: State.Success): VNode {
<i18n.Translate>Cancel</i18n.Translate> <i18n.Translate>Cancel</i18n.Translate>
</Link> </Link>
</section> </section>
</WalletAction> </Fragment>
); );
} }

View File

@ -21,7 +21,7 @@
*/ */
import { createHashHistory } from "history"; 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 Router, { route, Route } from "preact-router";
import { Match } from "preact-router/match"; import { Match } from "preact-router/match";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -34,15 +34,28 @@ import {
useTranslationContext, useTranslationContext,
} from "../context/translation.js"; } from "../context/translation.js";
import { useTalerActionURL } from "../hooks/useTalerActionURL.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 { platform } from "../platform/api.js";
import { BackupPage } from "../wallet/BackupPage.js"; import { BackupPage } from "../wallet/BackupPage.js";
import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js"; import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
import { BalancePage } from "./BalancePage.js"; import { BalancePage } from "./BalancePage.js";
import { TalerActionFound } from "./TalerActionFound.js"; import { TalerActionFound } from "./TalerActionFound.js";
function CheckTalerActionComponent(): VNode { export function Application(): VNode {
const [action] = useTalerActionURL(); return (
<TranslationProvider>
<DevContextProvider>
<IoCProviderForRuntime>
<ApplicationView />
</IoCProviderForRuntime>
</DevContextProvider>
</TranslationProvider>
);
}
function ApplicationView(): VNode {
const hash_history = createHashHistory();
const [action, setDismissed] = useTalerActionURL();
const actionUri = action?.uri; const actionUri = action?.uri;
@ -52,116 +65,110 @@ function CheckTalerActionComponent(): VNode {
} }
}, [actionUri]); }, [actionUri]);
return <Fragment />; async function redirectToTxInfo(tid: string): Promise<void> {
} redirectTo(Pages.balanceTransaction({ tid }));
}
export function Application(): VNode {
const hash_history = createHashHistory();
return ( return (
<TranslationProvider> <Router history={hash_history}>
<DevContextProvider> <Route
{({ devMode }: { devMode: boolean }) => ( path={Pages.balance}
<IoCProviderForRuntime> component={() => (
<PendingTransactions <PopupTemplate path="balance" goToTransaction={redirectToTxInfo}>
goToTransaction={(tid: string) => <BalancePage
redirectTo(Pages.balanceTransaction({ tid })) goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))}
goToWalletDeposit={(currency: string) =>
redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
}
goToWalletHistory={(currency: string) =>
redirectTo(Pages.balanceHistory({ currency }))
} }
/> />
<Match> </PopupTemplate>
{({ 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>
)} )}
</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; 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)) { } else if (ArrayBuffer.isView(requestBody)) {
myBody = requestBody; myBody = requestBody;
} else if (typeof requestBody === "object") { } else if (typeof requestBody === "object") {
myBody = JSON.stringify(myBody); myBody = JSON.stringify(requestBody);
} else { } else {
throw Error("unsupported request body type"); 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( postJson(
url: string, url: string,
body: any, body: any,

View File

@ -20,7 +20,11 @@
*/ */
import { Fragment, FunctionComponent, h } from "preact"; import { Fragment, FunctionComponent, h } from "preact";
import { LogoHeader } from "./components/LogoHeader.js"; 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 { strings } from "./i18n/strings.js";
import { PopupNavBar, WalletNavBar } from "./NavigationBar.js"; import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
@ -72,7 +76,7 @@ function getWrapperForGroup(group: string): FunctionComponent {
return function WalletWrapper({ children }: any) { return function WalletWrapper({ children }: any) {
return ( return (
<Fragment> <Fragment>
<WalletBox>{children}</WalletBox> <WalletAction>{children}</WalletAction>
</Fragment> </Fragment>
); );
}; };

View File

@ -74,7 +74,7 @@ export async function queryToSlashKeys<T>(url: string): Promise<T> {
return timeout(3000, query); 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 }> = { export type StateViewMap<StateType extends { status: string }> = {
[S in StateType as S["status"]]: StateFunc<S>; [S in StateType as S["status"]]: StateFunc<S>;

View File

@ -32,7 +32,6 @@ import {
} from "./views.js"; } from "./views.js";
export interface Props { export interface Props {
currency: string;
onBack: () => Promise<void>; onBack: () => Promise<void>;
onComplete: (pid: string) => Promise<void>; onComplete: (pid: string) => Promise<void>;
onPaymentRequired: (uri: string) => Promise<void>; onPaymentRequired: (uri: string) => Promise<void>;

View File

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

View File

@ -26,7 +26,6 @@ import { Props } from "./index.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
const props: Props = { const props: Props = {
currency: "KUDOS",
onBack: nullFunction, onBack: nullFunction,
onComplete: nullFunction, onComplete: nullFunction,
onPaymentRequired: nullFunction, onPaymentRequired: nullFunction,

View File

@ -20,352 +20,452 @@
* @author sebasjm * @author sebasjm
*/ */
import { TranslatedString } from "@gnu-taler/taler-util";
import { createHashHistory } from "history"; 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 Router, { route, Route } from "preact-router";
import Match from "preact-router/match"; import { useEffect } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { LogoHeader } from "../components/LogoHeader.js"; import { LogoHeader } from "../components/LogoHeader.js";
import PendingTransactions from "../components/PendingTransactions.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 { DevContextProvider } from "../context/devContext.js";
import { IoCProviderForRuntime } from "../context/iocContext.js"; import { IoCProviderForRuntime } from "../context/iocContext.js";
import { import {
TranslationProvider, TranslationProvider,
useTranslationContext, useTranslationContext,
} from "../context/translation.js"; } 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 { PaymentPage } from "../cta/Payment/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js";
import { RefundPage } from "../cta/Refund/index.js"; import { RefundPage } from "../cta/Refund/index.js";
import { TipPage } from "../cta/Tip/index.js"; import { TipPage } from "../cta/Tip/index.js";
import { TransferCreatePage } from "../cta/TransferCreate/index.js";
import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import { import {
WithdrawPageFromParams, WithdrawPageFromParams,
WithdrawPageFromURI, WithdrawPageFromURI,
} from "../cta/Withdraw/index.js"; } from "../cta/Withdraw/index.js";
import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js"; import { WalletNavBarOptions, Pages, WalletNavBar } from "../NavigationBar.js";
import { Pages, WalletNavBar } from "../NavigationBar.js"; import { platform } from "../platform/api.js";
import { DeveloperPage } from "./DeveloperPage.js"; import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
import { BackupPage } from "./BackupPage.js"; import { BackupPage } from "./BackupPage.js";
import { DepositPage } from "./DepositPage/index.js"; import { DepositPage } from "./DepositPage/index.js";
import { DestinationSelectionPage } from "./DestinationSelection/index.js";
import { DeveloperPage } from "./DeveloperPage.js";
import { ExchangeAddPage } from "./ExchangeAddPage.js"; import { ExchangeAddPage } from "./ExchangeAddPage.js";
import { HistoryPage } from "./History.js"; import { HistoryPage } from "./History.js";
import { NotificationsPage } from "./Notifications/index.js";
import { ProviderDetailPage } from "./ProviderDetailPage.js"; import { ProviderDetailPage } from "./ProviderDetailPage.js";
import { QrReaderPage } from "./QrReader.js";
import { SettingsPage } from "./Settings.js"; import { SettingsPage } from "./Settings.js";
import { TransactionPage } from "./Transaction.js"; import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.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 { 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 { i18n } = useTranslationContext();
const hash_history = createHashHistory();
async function redirectToTxInfo(tid: string): Promise<void> {
redirectTo(Pages.balanceTransaction({ tid }));
}
return ( return (
<TranslationProvider> <TranslationProvider>
<DevContextProvider> <DevContextProvider>
<IoCProviderForRuntime> <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}> <Router history={hash_history}>
<Match default> <Route
{({ path }: { path: string }) => { path={Pages.welcome}
if (path && path.startsWith("/cta")) return; component={() => (
return ( <WalletTemplate>
<Fragment> <WelcomePage />
<LogoHeader /> </WalletTemplate>
<WalletNavBar path={path} /> )}
{shouldShowPendingOperations(path) && ( />
<div
style={{ <Route
backgroundColor: "lightcyan", path={Pages.qr}
display: "flex", component={() => (
justifyContent: "center", <WalletTemplate goToTransaction={redirectToTxInfo}>
}} <QrReaderPage
> onDetected={(talerActionUrl: string) => {
<PendingTransactions platform.openWalletURIFromPopup(talerActionUrl);
goToTransaction={(tid: string) => }}
redirectTo(Pages.balanceTransaction({ tid })) />
} </WalletTemplate>
/> )}
</div> />
)}
</Fragment> <Route
); path={Pages.settings}
}} component={() => (
</Match> <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> </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> </IoCProviderForRuntime>
</DevContextProvider> </DevContextProvider>
</TranslationProvider> </TranslationProvider>
@ -403,3 +503,40 @@ function shouldShowPendingOperations(url: string): boolean {
Pages.backup, Pages.backup,
].some((p) => matchesRoute(url, p)); ].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 = { type CalculatedCoinfInfo = {
ageKeysCount: number | undefined; ageKeysCount: number | undefined;
denom_value: number; denom_value: number;
denom_fraction: number;
//remain_value: number; //remain_value: number;
status: string; status: string;
from_refresh: boolean; from_refresh: boolean;
@ -151,7 +152,8 @@ export function View({
} }
prev[cur.exchange_base_url].push({ prev[cur.exchange_base_url].push({
ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length, ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
denom_value: parseFloat(Amounts.stringifyValue(denom)), denom_value: denom.value,
denom_fraction: denom.fraction,
// remain_value: parseFloat( // remain_value: parseFloat(
// Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)), // Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
// ), // ),
@ -340,7 +342,10 @@ export function View({
{Object.keys(money_by_exchange).map((ex, idx) => { {Object.keys(money_by_exchange).map((ex, idx) => {
const allcoins = money_by_exchange[ex]; const allcoins = money_by_exchange[ex];
allcoins.sort((a, b) => { 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( const coins = allcoins.reduce(
@ -407,11 +412,31 @@ function ShowAllCoins({
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [collapsedSpent, setCollapsedSpent] = useState(true); const [collapsedSpent, setCollapsedSpent] = useState(true);
const [collapsedUnspent, setCollapsedUnspent] = useState(false); 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 ( return (
<Fragment> <Fragment>
<p> <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>
<p onClick={() => setCollapsedUnspent(true)}> <p onClick={() => setCollapsedUnspent(true)}>
<b> <b>