invoice and transfer details

This commit is contained in:
Sebastian 2022-08-31 11:46:39 -03:00
parent d84424202d
commit e759684fd0
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
23 changed files with 168 additions and 58 deletions

View File

@ -25,6 +25,7 @@ import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/h
export interface Props { export interface Props {
amount: string; amount: string;
onClose: () => Promise<void>;
} }
export type State = export type State =
@ -47,11 +48,11 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
cancel: ButtonHandler;
} }
export interface ShowQr extends BaseInfo { export interface ShowQr extends BaseInfo {
status: "show-qr"; status: "show-qr";
talerUri: string; talerUri: string;
close: () => void;
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";

View File

@ -22,7 +22,7 @@ import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ amount: amountStr }: Props, { amount: amountStr, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const amount = Amounts.parseOrThrow(amountStr) const amount = Amounts.parseOrThrow(amountStr)
@ -53,7 +53,9 @@ export function useComponentState(
status: "show-qr", status: "show-qr",
talerUri, talerUri,
error: undefined, error: undefined,
close: () => { null }, cancel: {
onClick: onClose
}
// chosenAmount: amount, // chosenAmount: amount,
// toBeReceived: amount, // toBeReceived: amount,
} }
@ -105,6 +107,9 @@ export function useComponentState(
setTalerUri(uri) setTalerUri(uri)
} }
}, },
cancel: {
onClick: onClose
},
chosenAmount: amount, chosenAmount: amount,
toBeReceived: amount, toBeReceived: amount,
error: undefined, error: undefined,

View File

@ -29,9 +29,7 @@ export default {
export const ShowQr = createExample(ShowQrView, { export const ShowQr = createExample(ShowQrView, {
talerUri: talerUri:
"taler://pay-pull/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", "taler://pay-pull/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
close: () => { cancel: {},
null;
},
}); });
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {
@ -40,6 +38,7 @@ export const Ready = createExample(ReadyView, {
value: 1, value: 1,
fraction: 0, fraction: 0,
}, },
cancel: {},
toBeReceived: { toBeReceived: {
currency: "ARS", currency: "ARS",
value: 1, value: 1,

View File

@ -15,6 +15,7 @@
*/ */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
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 { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
@ -44,7 +45,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
); );
} }
export function ShowQrView({ talerUri, close }: State.ShowQr): VNode { export function ShowQrView({ talerUri, cancel }: State.ShowQr): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<WalletAction> <WalletAction>
@ -57,7 +58,7 @@ export function ShowQrView({ talerUri, close }: State.ShowQr): VNode {
<QR text={talerUri} /> <QR text={talerUri} />
</section> </section>
<section> <section>
<Link upperCased onClick={close}> <Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Close</i18n.Translate> <i18n.Translate>Close</i18n.Translate>
</Link> </Link>
</section> </section>
@ -70,6 +71,7 @@ export function ReadyView({
exchangeUrl, exchangeUrl,
subject, subject,
showQr, showQr,
cancel,
operationError, operationError,
copyToClipboard, copyToClipboard,
toBeReceived, toBeReceived,
@ -83,6 +85,16 @@ export function ReadyView({
<SubTitle> <SubTitle>
<i18n.Translate>Digital invoice</i18n.Translate> <i18n.Translate>Digital invoice</i18n.Translate>
</SubTitle> </SubTitle>
{operationError && (
<ErrorTalerOperation
title={
<i18n.Translate>
Could not finish the invoice creation
</i18n.Translate>
}
error={operationError}
/>
)}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<TextField <TextField
label="Subject" label="Subject"
@ -145,6 +157,11 @@ export function ReadyView({
</Grid> </Grid>
</Grid> </Grid>
</section> </section>
<section>
<Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Cancel</i18n.Translate>
</Link>
</section>
</WalletAction> </WalletAction>
); );
} }

View File

@ -14,7 +14,7 @@
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 { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { AbsoluteTime, AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
@ -25,6 +25,7 @@ import { LoadingUriView, ReadyView } from "./views.js";
export interface Props { export interface Props {
talerPayPullUri: string; talerPayPullUri: string;
onClose: () => Promise<void>;
} }
export type State = export type State =
@ -46,10 +47,13 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
cancel: ButtonHandler;
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
amount: AmountJson, amount: AmountJson,
summary: string | undefined,
expiration: AbsoluteTime | undefined,
error: undefined; error: undefined;
accept: ButtonHandler; accept: ButtonHandler;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;

View File

@ -14,7 +14,7 @@
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 { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; import { AbsoluteTime, Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -22,7 +22,7 @@ import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayPullUri }: Props, { talerPayPullUri, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
@ -45,13 +45,18 @@ export function useComponentState(
}; };
} }
const { amount, peerPullPaymentIncomingId } = hook.response const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response
const amount: string = contractTerms?.amount
const summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.acceptPeerPullPayment({ const resp = await api.acceptPeerPullPayment({
peerPullPaymentIncomingId peerPullPaymentIncomingId
}) })
await onClose()
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail)
@ -69,6 +74,11 @@ export function useComponentState(
accept: { accept: {
onClick: accept onClick: accept
}, },
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
cancel: {
onClick: onClose
},
operationError operationError
} }
} }

View File

@ -33,4 +33,5 @@ export const Ready = createExample(ReadyView, {
fraction: 0, fraction: 0,
}, },
accept: {}, accept: {},
cancel: {},
}); });

View File

@ -20,19 +20,10 @@ 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 { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js"; import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { import { Time } from "../../components/Time.js";
Link,
SubTitle,
SvgIcon,
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 { Grid } from "../../mui/Grid.js";
import { TextField } from "../../mui/TextField.js";
import editIcon from "../../svg/edit_24px.svg";
import { ExchangeDetails, InvoiceDetails } from "../../wallet/Transaction.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 {
@ -48,7 +39,10 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ export function ReadyView({
operationError, operationError,
cancel,
accept, accept,
expiration,
summary,
amount, amount,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -70,16 +64,32 @@ export function ReadyView({
/> />
)} )}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={<div>{summary}</div>}
/>
<Part <Part
title={<i18n.Translate>Amount</i18n.Translate>} title={<i18n.Translate>Amount</i18n.Translate>}
text={<Amount value={amount} />} text={<Amount value={amount} />}
/> />
<Part
title={<i18n.Translate>Valid until</i18n.Translate>}
text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
kind="neutral"
/>
</section> </section>
<section> <section>
<Button variant="contained" color="success" onClick={accept.onClick}> <Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>Pay</i18n.Translate> <i18n.Translate>
Pay &nbsp; {<Amount value={amount} />}
</i18n.Translate>
</Button> </Button>
</section> </section>
<section>
<Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Cancel</i18n.Translate>
</Link>
</section>
</WalletAction> </WalletAction>
); );
} }

View File

@ -30,7 +30,7 @@ import { AcceptedView, IgnoredView, LoadingUriView, ReadyView } from "./views.js
export interface Props { export interface Props {
talerTipUri?: string; talerTipUri?: string;
cancel: () => Promise<void>; onCancel: () => Promise<void>;
} }
export type State = export type State =
@ -58,6 +58,7 @@ export namespace State {
amount: AmountJson; amount: AmountJson;
exchangeBaseUrl: string; exchangeBaseUrl: string;
error: undefined; error: undefined;
cancel: ButtonHandler;
} }
export interface Ignored extends BaseInfo { export interface Ignored extends BaseInfo {
@ -70,7 +71,6 @@ export namespace State {
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
accept: ButtonHandler; accept: ButtonHandler;
cancel: () => Promise<void>;
} }
} }

View File

@ -22,7 +22,7 @@ import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerTipUri, cancel }: Props, { talerTipUri, onCancel }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const [tipIgnored, setTipIgnored] = useState(false); const [tipIgnored, setTipIgnored] = useState(false);
@ -58,6 +58,9 @@ export function useComponentState(
exchangeBaseUrl: tip.exchangeBaseUrl, exchangeBaseUrl: tip.exchangeBaseUrl,
amount: Amounts.parseOrThrow(tip.tipAmountEffective), amount: Amounts.parseOrThrow(tip.tipAmountEffective),
error: undefined, error: undefined,
cancel: {
onClick: onCancel
}
} }
if (tipIgnored) { if (tipIgnored) {
@ -80,7 +83,6 @@ export function useComponentState(
accept: { accept: {
onClick: doAccept, onClick: doAccept,
}, },
cancel,
}; };
} }

View File

@ -30,7 +30,7 @@ describe("Tip CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: undefined, cancel: async () => { null } }, { useComponentState({ talerTipUri: undefined, onCancel: async () => { null } }, {
prepareTip: async () => ({}), prepareTip: async () => ({}),
acceptTip: async () => ({}), acceptTip: async () => ({}),
} as any), } as any),
@ -62,7 +62,7 @@ describe("Tip CTA states", () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", cancel: async () => { null } }, { useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, {
prepareTip: async () => prepareTip: async () =>
({ ({
accepted: tipAccepted, accepted: tipAccepted,
@ -114,7 +114,7 @@ describe("Tip CTA states", () => {
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", cancel: async () => { null } }, { useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, {
prepareTip: async () => prepareTip: async () =>
({ ({
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
@ -160,7 +160,7 @@ describe("Tip CTA states", () => {
it("should render accepted if the tip has been used previously", async () => { it("should render accepted if the tip has been used previously", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() => mountHook(() =>
useComponentState({ talerTipUri: "taler://tip/asd", cancel: async () => { null } }, { useComponentState({ talerTipUri: "taler://tip/asd", onCancel: async () => { null } }, {
prepareTip: async () => prepareTip: async () =>
({ ({
accepted: true, accepted: true,

View File

@ -14,17 +14,18 @@
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 { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js"; import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView, ShowQrView } from "./views.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js"; import { useComponentState } from "./state.js";
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { LoadingUriView, ReadyView, ShowQrView } from "./views.js";
import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
export interface Props { export interface Props {
amount: string; amount: string;
onClose: () => Promise<void>;
} }
export type State = export type State =
@ -47,11 +48,11 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
cancel: ButtonHandler;
} }
export interface ShowQr extends BaseInfo { export interface ShowQr extends BaseInfo {
status: "show-qr"; status: "show-qr";
talerUri: string; talerUri: string;
close: () => void;
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";

View File

@ -21,7 +21,7 @@ import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ amount: amountStr }: Props, { amount: amountStr, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const amount = Amounts.parseOrThrow(amountStr) const amount = Amounts.parseOrThrow(amountStr)
@ -30,13 +30,14 @@ export function useComponentState(
const [talerUri, setTalerUri] = useState("") const [talerUri, setTalerUri] = useState("")
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (talerUri) { if (talerUri) {
return { return {
status: "show-qr", status: "show-qr",
talerUri, talerUri,
error: undefined, error: undefined,
close: () => { null }, cancel: {
onClick: onClose,
},
} }
} }
@ -62,7 +63,11 @@ export function useComponentState(
return { return {
status: "ready", status: "ready",
invalid: !subject || Amounts.isZero(amount), invalid: !subject || Amounts.isZero(amount),
cancel: {
onClick: onClose,
},
subject: { subject: {
error: !subject ? "cant be empty" : undefined,
value: subject, value: subject,
onInput: async (e) => setSubject(e) onInput: async (e) => setSubject(e)
}, },

View File

@ -29,9 +29,7 @@ export default {
export const ShowQr = createExample(ShowQrView, { export const ShowQr = createExample(ShowQrView, {
talerUri: talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
close: () => { cancel: {},
null;
},
}); });
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {
@ -40,6 +38,7 @@ export const Ready = createExample(ReadyView, {
value: 1, value: 1,
fraction: 0, fraction: 0,
}, },
cancel: {},
toBeReceived: { toBeReceived: {
currency: "ARS", currency: "ARS",
value: 1, value: 1,

View File

@ -15,6 +15,7 @@
*/ */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
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 { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
@ -38,7 +39,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
); );
} }
export function ShowQrView({ talerUri, close }: State.ShowQr): VNode { export function ShowQrView({ talerUri, cancel }: State.ShowQr): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<WalletAction> <WalletAction>
@ -51,7 +52,7 @@ export function ShowQrView({ talerUri, close }: State.ShowQr): VNode {
<QR text={talerUri} /> <QR text={talerUri} />
</section> </section>
<section> <section>
<Link upperCased onClick={close}> <Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Close</i18n.Translate> <i18n.Translate>Close</i18n.Translate>
</Link> </Link>
</section> </section>
@ -64,7 +65,9 @@ export function ReadyView({
toBeReceived, toBeReceived,
chosenAmount, chosenAmount,
showQr, showQr,
operationError,
copyToClipboard, copyToClipboard,
cancel,
invalid, invalid,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -74,6 +77,16 @@ export function ReadyView({
<SubTitle> <SubTitle>
<i18n.Translate>Digital cash transfer</i18n.Translate> <i18n.Translate>Digital cash transfer</i18n.Translate>
</SubTitle> </SubTitle>
{operationError && (
<ErrorTalerOperation
title={
<i18n.Translate>
Could not finish the transfer creation
</i18n.Translate>
}
error={operationError}
/>
)}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<TextField <TextField
label="Subject" label="Subject"
@ -112,6 +125,13 @@ export function ReadyView({
</Grid> </Grid>
</Grid> </Grid>
</section> </section>
<section>
<section>
<Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Cancel</i18n.Translate>
</Link>
</section>
</section>
</WalletAction> </WalletAction>
); );
} }

View File

@ -14,7 +14,7 @@
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 { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { AbsoluteTime, AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
@ -25,6 +25,7 @@ import { LoadingUriView, ReadyView } from "./views.js";
export interface Props { export interface Props {
talerPayPushUri: string; talerPayPushUri: string;
onClose: () => Promise<void>;
} }
export type State = export type State =
@ -46,10 +47,13 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
cancel: ButtonHandler;
} }
export interface Ready extends BaseInfo { export interface Ready extends BaseInfo {
status: "ready"; status: "ready";
amount: AmountJson, amount: AmountJson,
summary: string | undefined;
expiration: AbsoluteTime | undefined;
error: undefined; error: undefined;
accept: ButtonHandler; accept: ButtonHandler;
operationError?: TalerErrorDetail; operationError?: TalerErrorDetail;

View File

@ -14,7 +14,7 @@
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 { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; import { AbsoluteTime, Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
@ -22,7 +22,7 @@ import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayPushUri }: Props, { talerPayPushUri, onClose }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
@ -45,13 +45,18 @@ export function useComponentState(
}; };
} }
const { amount, peerPushPaymentIncomingId } = hook.response const { amount: purseAmount, contractTerms, peerPushPaymentIncomingId } = hook.response
const amount: string = contractTerms?.amount
const summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration
async function accept(): Promise<void> { async function accept(): Promise<void> {
try { try {
const resp = await api.acceptPeerPushPayment({ const resp = await api.acceptPeerPushPayment({
peerPushPaymentIncomingId peerPushPaymentIncomingId
}) })
await onClose()
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
setOperationError(e.errorDetail) setOperationError(e.errorDetail)
@ -67,6 +72,11 @@ export function useComponentState(
accept: { accept: {
onClick: accept onClick: accept
}, },
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
cancel: {
onClick: onClose
},
operationError operationError
} }
} }

View File

@ -33,4 +33,5 @@ export const Ready = createExample(ReadyView, {
fraction: 0, fraction: 0,
}, },
accept: {}, accept: {},
cancel: {},
}); });

View File

@ -20,7 +20,8 @@ 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 { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js"; import { Part } from "../../components/Part.js";
import { SubTitle, WalletAction } from "../../components/styled/index.js"; import { Link, SubTitle, WalletAction } from "../../components/styled/index.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 { Button } from "../../mui/Button.js";
import { State } from "./index.js"; import { State } from "./index.js";
@ -38,7 +39,10 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ export function ReadyView({
accept, accept,
summary,
expiration,
amount, amount,
cancel,
operationError, operationError,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -59,16 +63,32 @@ export function ReadyView({
/> />
)} )}
<section style={{ textAlign: "left" }}> <section style={{ textAlign: "left" }}>
<Part
title={<i18n.Translate>Subject</i18n.Translate>}
text={<div>{summary}</div>}
/>
<Part <Part
title={<i18n.Translate>Amount</i18n.Translate>} title={<i18n.Translate>Amount</i18n.Translate>}
text={<Amount value={amount} />} text={<Amount value={amount} />}
/> />
<Part
title={<i18n.Translate>Valid until</i18n.Translate>}
text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
kind="neutral"
/>
</section> </section>
<section> <section>
<Button variant="contained" color="success" onClick={accept.onClick}> <Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>Pickup</i18n.Translate> <i18n.Translate>
Receive &nbsp; {<Amount value={amount} />}
</i18n.Translate>
</Button> </Button>
</section> </section>
<section>
<Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Cancel</i18n.Translate>
</Link>
</section>
</WalletAction> </WalletAction>
); );
} }

View File

@ -258,7 +258,7 @@ export function Application(): VNode {
<Route <Route
path={Pages.ctaTips} path={Pages.ctaTips}
component={TipPage} component={TipPage}
cancel={() => redirectTo(Pages.balance)} onCancel={() => redirectTo(Pages.balance)}
/> />
<Route <Route
path={Pages.ctaWithdraw} path={Pages.ctaWithdraw}
@ -278,16 +278,22 @@ export function Application(): VNode {
<Route <Route
path={Pages.ctaInvoiceCreate.pattern} path={Pages.ctaInvoiceCreate.pattern}
component={InvoiceCreatePage} component={InvoiceCreatePage}
onClose={() => redirectTo(Pages.balance)}
/> />
<Route <Route
path={Pages.ctaTransferCreate.pattern} path={Pages.ctaTransferCreate.pattern}
component={TransferCreatePage} component={TransferCreatePage}
onClose={() => redirectTo(Pages.balance)}
/>
<Route
path={Pages.ctaInvoicePay}
component={InvoicePayPage}
onClose={() => redirectTo(Pages.balance)}
/> />
<Route path={Pages.ctaInvoicePay} component={InvoicePayPage} />
<Route <Route
path={Pages.ctaTransferPickup} path={Pages.ctaTransferPickup}
component={TransferPickupPage} component={TransferPickupPage}
onClose={() => redirectTo(Pages.balance)}
/> />
{/** {/**

View File

@ -317,7 +317,6 @@ export function createDenominationPairTimeline(left: FeeDescription[], right: Fe
//now both lists are non empty and (starts,ends) at the same time //now both lists are non empty and (starts,ends) at the same time
while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) { while (li < left.length && ri < right.length && Amounts.cmp(left[li].value, right[ri].value) === 0) {
// console.log('start', li, ri, left[li], right[ri])
if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) { if (AbsoluteTime.cmp(left[li].from, timeCut) !== 0 && AbsoluteTime.cmp(right[ri].from, timeCut) !== 0) {
// timeCut comes from the latest "until" (expiration from the previous) // timeCut comes from the latest "until" (expiration from the previous)
@ -325,7 +324,6 @@ export function createDenominationPairTimeline(left: FeeDescription[], right: Fe
// it should be the same as the "from" from one of the latest left or right // it should be the same as the "from" from one of the latest left or right
// otherwise it means that there is missing a gap object in the middle // otherwise it means that there is missing a gap object in the middle
// the list is not complete and the behavior is undefined // the list is not complete and the behavior is undefined
console.log(li, ri, timeCut)
throw Error('one of the list is not completed: list[i].until !== list[i+1].from') throw Error('one of the list is not completed: list[i].until !== list[i+1].from')
} }

View File

@ -692,8 +692,6 @@ describe("Denomination timeline pair creation", () => {
const pairs = createDenominationPairTimeline(left, right) const pairs = createDenominationPairTimeline(left, right)
console.log(JSON.stringify(pairs, undefined, 2))
}) })
}) })
}) })

View File

@ -228,7 +228,6 @@ interface ListOfKnownCurrencies {
*/ */
export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> { export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
return callBackend("listCurrencies", {}).then((result) => { return callBackend("listCurrencies", {}).then((result) => {
console.log("result list", result);
const auditors = result.trustedAuditors.map( const auditors = result.trustedAuditors.map(
(a: Record<string, string>) => ({ (a: Record<string, string>) => ({
name: a.currency, name: a.currency,