p2p tx rendering

This commit is contained in:
Sebastian 2022-08-31 00:20:35 -03:00
parent 7dc66c2441
commit d84424202d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
33 changed files with 1548 additions and 159 deletions

View File

@ -471,7 +471,7 @@ async function getMergeReserveInfo(
export async function acceptPeerPushPayment(
ws: InternalWalletState,
req: AcceptPeerPushPaymentRequest,
) {
): Promise<void> {
const peerInc = await ws.db
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
.runReadOnly(async (tx) => {

View File

@ -108,16 +108,20 @@ export const Pages = {
"/settings/exchange/add/:currency?",
),
invoice: pageDefinition<{ amount?: string }>("/invoice/:amount?"),
send: pageDefinition<{ amount?: string }>("/send/:amount?"),
cta: pageDefinition<{ action: string }>("/cta/:action"),
ctaPay: "/cta/pay",
ctaRefund: "/cta/refund",
ctaTips: "/cta/tip",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
"/cta/invoice/create/:amount?",
),
ctaTransferCreate: pageDefinition<{ amount?: string }>(
"/cta/transfer/create/:amount?",
),
ctaInvoicePay: "/cta/invoice/pay",
ctaTransferPickup: "/cta/transfer/pickup",
ctaWithdrawManual: pageDefinition<{ amount?: string }>(
"/cta/manual-withdraw/:amount?",
),

View File

@ -113,8 +113,58 @@ export function TransactionItem(props: { tx: Transaction }): VNode {
pending={tx.pending}
/>
);
default:
throw Error("unsupported transaction type");
case TransactionType.PeerPullCredit:
return (
<TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title={"Invoice credit"}
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
iconPath={"I"}
pending={tx.pending}
/>
);
case TransactionType.PeerPullDebit:
return (
<TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
title={"Invoice debit"}
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
iconPath={"I"}
pending={tx.pending}
/>
);
case TransactionType.PeerPushCredit:
return (
<TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"credit"}
title={"Transfer credit"}
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
iconPath={"T"}
pending={tx.pending}
/>
);
case TransactionType.PeerPushDebit:
return (
<TransactionLayout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
title={"Transfer debit"}
timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)}
iconPath={"T"}
pending={tx.pending}
/>
);
default: {
const pe: never = tx;
throw Error(`unsupported transaction type ${pe}`);
}
}
}

View File

@ -0,0 +1,79 @@
/*
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 { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView, ShowQrView } from "./views.js";
import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
export interface Props {
amount: string;
}
export type State =
| State.Loading
| State.LoadingUriError
| State.ShowQr
| State.Ready;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingUriError {
status: "loading-uri";
error: HookError;
}
export interface BaseInfo {
error: undefined;
}
export interface ShowQr extends BaseInfo {
status: "show-qr";
talerUri: string;
close: () => void;
}
export interface Ready extends BaseInfo {
status: "ready";
showQr: ButtonHandler;
copyToClipboard: ButtonHandler;
subject: TextFieldHandler;
toBeReceived: AmountJson,
chosenAmount: AmountJson,
exchangeUrl: string;
invalid: boolean;
error: undefined;
operationError?: TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"show-qr": ShowQrView,
"ready": ReadyView,
};
export const InvoiceCreatePage = compose("InvoiceCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping)

View File

@ -0,0 +1,113 @@
/*
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, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState(
{ amount: amountStr }: Props,
api: typeof wxApi,
): State {
const amount = Amounts.parseOrThrow(amountStr)
const [subject, setSubject] = useState("");
const [talerUri, setTalerUri] = useState("")
const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0")
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (!hook) {
return {
status: "loading",
error: undefined,
}
}
if (hook.hasError) {
return {
status: "loading-uri",
error: hook,
};
}
if (talerUri) {
return {
status: "show-qr",
talerUri,
error: undefined,
close: () => { null },
// chosenAmount: amount,
// toBeReceived: amount,
}
}
const exchanges = hook.response.exchanges.filter(e => e.currency === amount.currency);
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }), {} as Record<string, string>)
const selected = exchanges[Number(exchangeIdx)];
async function accept(): Promise<string> {
try {
const resp = await api.initiatePeerPullPayment({
amount: Amounts.stringify(amount),
exchangeBaseUrl: selected.exchangeBaseUrl,
partialContractTerms: {
summary: subject
}
})
return resp.talerUri
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
}
console.error(e)
throw Error("error trying to accept")
}
}
return {
status: "ready",
subject: {
error: !subject ? "cant be empty" : undefined,
value: subject,
onInput: async (e) => setSubject(e)
},
invalid: !subject || Amounts.isZero(amount),
exchangeUrl: selected.exchangeBaseUrl,
copyToClipboard: {
onClick: async () => {
const uri = await accept();
navigator.clipboard.writeText(uri || "");
}
},
showQr: {
onClick: async () => {
const uri = await accept();
setTalerUri(uri)
}
},
chosenAmount: amount,
toBeReceived: amount,
error: undefined,
operationError
}
}

View File

@ -0,0 +1,57 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../../test-utils.js";
import { ReadyView, ShowQrView } from "./views.js";
export default {
title: "wallet/invoice create",
};
export const ShowQr = createExample(ShowQrView, {
talerUri:
"taler://pay-pull/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
close: () => {
null;
},
});
export const Ready = createExample(ReadyView, {
chosenAmount: {
currency: "ARS",
value: 1,
fraction: 0,
},
toBeReceived: {
currency: "ARS",
value: 1,
fraction: 0,
},
exchangeUrl: "https://exchange.taler.ar",
subject: {
value: "some subject",
onInput: async () => {
null;
},
},
copyToClipboard: {},
showQr: {},
});

View File

@ -0,0 +1,150 @@
/*
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 { h, VNode } from "preact";
import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import {
Link,
SubTitle,
SvgIcon,
WalletAction,
} from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.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";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
);
}
export function ShowQrView({ talerUri, close }: State.ShowQr): VNode {
const { i18n } = useTranslationContext();
return (
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital invoice</i18n.Translate>
</SubTitle>
<section>
<p>Scan this QR code with the wallet</p>
<QR text={talerUri} />
</section>
<section>
<Link upperCased onClick={close}>
<i18n.Translate>Close</i18n.Translate>
</Link>
</section>
</WalletAction>
);
}
export function ReadyView({
invalid,
exchangeUrl,
subject,
showQr,
operationError,
copyToClipboard,
toBeReceived,
chosenAmount,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital invoice</i18n.Translate>
</SubTitle>
<section style={{ textAlign: "left" }}>
<TextField
label="Subject"
variant="filled"
error={!!subject.error}
required
fullWidth
value={subject.value}
onChange={subject.onInput}
/>
<Part
title={
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<i18n.Translate>Exchange</i18n.Translate>
<Link>
<SvgIcon
title="Edit"
dangerouslySetInnerHTML={{ __html: editIcon }}
color="black"
/>
</Link>
</div>
}
text={<ExchangeDetails exchange={exchangeUrl} />}
kind="neutral"
big
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<InvoiceDetails
amount={{
effective: toBeReceived,
raw: chosenAmount,
}}
/>
}
/>
</section>
<section>
<p>How do you want to send the invoice?</p>
<Grid item container columns={1} spacing={1}>
<Grid item xs={1}>
<Button disabled={invalid} onClick={copyToClipboard.onClick}>
Copy request URI to clipboard
</Button>
</Grid>
<Grid item xs={1}>
<Button disabled={invalid} onClick={showQr.onClick}>
Show QR
</Button>
</Grid>
</Grid>
</section>
</WalletAction>
);
}

View File

@ -14,17 +14,17 @@
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 { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView } from "./views.js";
import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { AmountJson } from "@gnu-taler/taler-util";
import { TextFieldHandler } from "../../mui/handlers.js";
import { LoadingUriView, ReadyView } from "./views.js";
export interface Props {
p: string;
talerPayPullUri: string;
}
export type State =
@ -49,9 +49,10 @@ export namespace State {
}
export interface Ready extends BaseInfo {
status: "ready";
amount: AmountJson;
subject: TextFieldHandler;
amount: AmountJson,
error: undefined;
accept: ButtonHandler;
operationError?: TalerErrorDetail;
}
}
@ -62,5 +63,5 @@ const viewMapping: StateViewMap<State> = {
};
export const InvoicePage = compose("InvoicePage", (p: Props) => useComponentState(p, wxApi), viewMapping)
export const InvoicePayPage = compose("InvoicePayPage", (p: Props) => useComponentState(p, wxApi), viewMapping)

View File

@ -14,21 +14,23 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts } from "@gnu-taler/taler-util";
import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState(
{ p }: Props,
{ talerPayPullUri }: Props,
api: typeof wxApi,
): State {
const [subject, setSubject] = useState("");
const amount = Amounts.parseOrThrow("ARS:0")
const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0")
const hook = useAsyncAsHook(async () => {
return await api.checkPeerPullPayment({
talerUri: talerPayPullUri
})
}, [])
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (!hook) {
return {
@ -43,24 +45,30 @@ export function useComponentState(
};
}
const exchanges = hook.response.exchanges;
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>)
const selected = exchanges[Number(exchangeIdx)];
const { amount, peerPullPaymentIncomingId } = hook.response
async function accept(): Promise<void> {
try {
const resp = await api.acceptPeerPullPayment({
peerPullPaymentIncomingId
})
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
}
console.error(e)
throw Error("error trying to accept")
}
}
return {
status: "ready",
exchange: {
list: exchangeMap,
value: exchangeIdx,
onChange: async (v) => {
setExchangeIdx(v)
}
},
subject: {
value: subject,
onInput: async (e) => setSubject(e)
},
amount,
amount: Amounts.parseOrThrow(amount),
error: undefined,
accept: {
onClick: accept
},
operationError
}
}

View File

@ -23,7 +23,14 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js";
export default {
title: "wallet/invoice",
title: "wallet/invoice payment",
};
export const Ready = createExample(ReadyView, {});
export const Ready = createExample(ReadyView, {
amount: {
currency: "ARS",
value: 1,
fraction: 0,
},
accept: {},
});

View File

@ -0,0 +1,85 @@
/*
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 { h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import {
Link,
SubTitle,
SvgIcon,
WalletAction,
} from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.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";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
);
}
export function ReadyView({
operationError,
accept,
amount,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital invoice</i18n.Translate>
</SubTitle>
{operationError && (
<ErrorTalerOperation
title={
<i18n.Translate>
Could not finish the payment operation
</i18n.Translate>
}
error={operationError}
/>
)}
<section style={{ textAlign: "left" }}>
<Part
title={<i18n.Translate>Amount</i18n.Translate>}
text={<Amount value={amount} />}
/>
</section>
<section>
<Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>Pay</i18n.Translate>
</Button>
</section>
</WalletAction>
);
}

View File

@ -0,0 +1,78 @@
/*
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 { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView, ShowQrView } from "./views.js";
import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
export interface Props {
amount: string;
}
export type State =
| State.Loading
| State.LoadingUriError
| State.ShowQr
| State.Ready;
export namespace State {
export interface Loading {
status: "loading";
error: undefined;
}
export interface LoadingUriError {
status: "loading-uri";
error: HookError;
}
export interface BaseInfo {
error: undefined;
}
export interface ShowQr extends BaseInfo {
status: "show-qr";
talerUri: string;
close: () => void;
}
export interface Ready extends BaseInfo {
status: "ready";
showQr: ButtonHandler;
invalid: boolean;
copyToClipboard: ButtonHandler;
toBeReceived: AmountJson,
chosenAmount: AmountJson,
subject: TextFieldHandler,
error: undefined;
operationError?: TalerErrorDetail;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"show-qr": ShowQrView,
"ready": ReadyView,
};
export const TransferCreatePage = compose("TransferCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping)

View File

@ -0,0 +1,86 @@
/*
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, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState(
{ amount: amountStr }: Props,
api: typeof wxApi,
): State {
const amount = Amounts.parseOrThrow(amountStr)
const [subject, setSubject] = useState("");
const [talerUri, setTalerUri] = useState("")
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (talerUri) {
return {
status: "show-qr",
talerUri,
error: undefined,
close: () => { null },
}
}
async function accept(): Promise<string> {
try {
const resp = await api.initiatePeerPushPayment({
amount: Amounts.stringify(amount),
partialContractTerms: {
summary: subject
}
})
return resp.talerUri
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
}
console.error(e)
throw Error("error trying to accept")
}
}
return {
status: "ready",
invalid: !subject || Amounts.isZero(amount),
subject: {
value: subject,
onInput: async (e) => setSubject(e)
},
copyToClipboard: {
onClick: async () => {
const uri = await accept();
navigator.clipboard.writeText(uri || "");
}
},
showQr: {
onClick: async () => {
const uri = await accept();
setTalerUri(uri)
}
},
chosenAmount: amount,
toBeReceived: amount,
error: undefined,
operationError
}
}

View File

@ -0,0 +1,56 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample } from "../../test-utils.js";
import { ReadyView, ShowQrView } from "./views.js";
export default {
title: "wallet/transfer create",
};
export const ShowQr = createExample(ShowQrView, {
talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
close: () => {
null;
},
});
export const Ready = createExample(ReadyView, {
chosenAmount: {
currency: "ARS",
value: 1,
fraction: 0,
},
toBeReceived: {
currency: "ARS",
value: 1,
fraction: 0,
},
copyToClipboard: {},
showQr: {},
subject: {
value: "the subject",
onInput: async () => {
null;
},
},
});

View File

@ -14,24 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
describe("test description", () => {
it("should assert", () => {
expect([]).deep.equals([])
});
})
export function useComponentState(
{ p }: Props,
api: typeof wxApi,
): State {
const [subject, setSubject] = useState("");
const amount = Amounts.parseOrThrow("ARS:0")
return {
status: "ready",
subject: {
value: subject,
onInput: async (e) => setSubject(e)
},
amount,
error: undefined,
}
}

View File

@ -0,0 +1,117 @@
/*
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 { h, VNode } from "preact";
import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
import { QR } from "../../components/QR.js";
import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
import { TextField } from "../../mui/TextField.js";
import { TransferDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
);
}
export function ShowQrView({ talerUri, close }: State.ShowQr): VNode {
const { i18n } = useTranslationContext();
return (
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital invoice</i18n.Translate>
</SubTitle>
<section>
<p>Scan this QR code with the wallet</p>
<QR text={talerUri} />
</section>
<section>
<Link upperCased onClick={close}>
<i18n.Translate>Close</i18n.Translate>
</Link>
</section>
</WalletAction>
);
}
export function ReadyView({
subject,
toBeReceived,
chosenAmount,
showQr,
copyToClipboard,
invalid,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital cash transfer</i18n.Translate>
</SubTitle>
<section style={{ textAlign: "left" }}>
<TextField
label="Subject"
variant="filled"
error={!!subject.error}
required
fullWidth
value={subject.value}
onChange={subject.onInput}
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<TransferDetails
amount={{
effective: toBeReceived,
raw: chosenAmount,
}}
/>
}
/>
</section>
<section>
<p>How do you want to transfer?</p>
<Grid item container columns={1} spacing={1}>
<Grid item xs={1}>
<Button disabled={invalid} onClick={copyToClipboard.onClick}>
Copy transfer URI to clipboard
</Button>
</Grid>
<Grid item xs={1}>
<Button disabled={invalid} onClick={showQr.onClick}>
Show QR
</Button>
</Grid>
</Grid>
</section>
</WalletAction>
);
}

View File

@ -14,17 +14,17 @@
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 { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { LoadingUriView, ReadyView } from "./views.js";
import * as wxApi from "../../wxApi.js";
import { useComponentState } from "./state.js";
import { AmountJson } from "@gnu-taler/taler-util";
import { SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
import { LoadingUriView, ReadyView } from "./views.js";
export interface Props {
p: string;
talerPayPushUri: string;
}
export type State =
@ -49,10 +49,10 @@ export namespace State {
}
export interface Ready extends BaseInfo {
status: "ready";
amount: AmountJson;
exchange: SelectFieldHandler,
subject: TextFieldHandler,
amount: AmountJson,
error: undefined;
accept: ButtonHandler;
operationError?: TalerErrorDetail;
}
}
@ -63,5 +63,5 @@ const viewMapping: StateViewMap<State> = {
};
export const SendPage = compose("SendPage", (p: Props) => useComponentState(p, wxApi), viewMapping)
export const TransferPickupPage = compose("TransferPickupPage", (p: Props) => useComponentState(p, wxApi), viewMapping)

View File

@ -0,0 +1,72 @@
/*
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, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState(
{ talerPayPushUri }: Props,
api: typeof wxApi,
): State {
const hook = useAsyncAsHook(async () => {
return await api.checkPeerPushPayment({
talerUri: talerPayPushUri,
})
}, [])
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (!hook) {
return {
status: "loading",
error: undefined,
}
}
if (hook.hasError) {
return {
status: "loading-uri",
error: hook,
};
}
const { amount, peerPushPaymentIncomingId } = hook.response
async function accept(): Promise<void> {
try {
const resp = await api.acceptPeerPushPayment({
peerPushPaymentIncomingId
})
} catch (e) {
if (e instanceof TalerError) {
setOperationError(e.errorDetail)
}
console.error(e)
throw Error("error trying to accept")
}
}
return {
status: "ready",
amount: Amounts.parseOrThrow(amount),
error: undefined,
accept: {
onClick: accept
},
operationError
}
}

View File

@ -23,7 +23,14 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js";
export default {
title: "wallet/invoice",
title: "wallet/transfer pickup",
};
export const Ready = createExample(ReadyView, {});
export const Ready = createExample(ReadyView, {
amount: {
currency: "ARS",
value: 1,
fraction: 0,
},
accept: {},
});

View File

@ -0,0 +1,31 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
describe("test description", () => {
it("should assert", () => {
expect([]).deep.equals([])
});
})

View File

@ -14,16 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Amounts } from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Amount } from "../../components/Amount.js";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js";
import { SelectList } from "../../components/SelectList.js";
import { Input } from "../../components/styled/index.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
import { SubTitle, WalletAction } from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@ -37,22 +36,39 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
const Container = styled.div``;
export function ReadyView({ amount, exchange, subject }: State.Ready): VNode {
export function ReadyView({
accept,
amount,
operationError,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<Container>
<p>Sending {Amounts.stringify(amount)}</p>
<TextField
label="Subject"
variant="filled"
required
value={subject.value}
onChange={subject.onInput}
/>
<p>to:</p>
<Button>Scan QR code</Button>
</Container>
<WalletAction>
<LogoHeader />
<SubTitle>
<i18n.Translate>Digital cash transfer</i18n.Translate>
</SubTitle>
{operationError && (
<ErrorTalerOperation
title={
<i18n.Translate>
Could not finish the pickup operation
</i18n.Translate>
}
error={operationError}
/>
)}
<section style={{ textAlign: "left" }}>
<Part
title={<i18n.Translate>Amount</i18n.Translate>}
text={<Amount value={amount} />}
/>
</section>
<section>
<Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>Pickup</i18n.Translate>
</Button>
</section>
</WalletAction>
);
}

View File

@ -25,5 +25,9 @@ import * as a4 from "./Refund/stories.jsx";
import * as a5 from "./Tip/stories.jsx";
import * as a6 from "./Withdraw/stories.jsx";
import * as a7 from "./TermsOfServiceSection.stories.js";
import * as a8 from "./InvoiceCreate/stories.js";
import * as a9 from "./InvoicePay/stories.js";
import * as a10 from "./TransferCreate/stories.js";
import * as a11 from "./TransferPickup/stories.js";
export default [a1, a3, a4, a5, a6, a7];
export default [a1, a3, a4, a5, a6, a7, a8, a9, a10, a11];

View File

@ -201,11 +201,33 @@ function openWalletURIFromPopup(talerUri: string): void {
`static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`,
);
break;
default:
case TalerUriType.TalerPayPull:
url = chrome.runtime.getURL(
`static/wallet.html#/cta/invoice/pay?talerPayPullUri=${talerUri}`,
);
break;
case TalerUriType.TalerPayPush:
url = chrome.runtime.getURL(
`static/wallet.html#/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
);
break;
case TalerUriType.TalerNotifyReserve:
logger.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
`Response with HTTP 402 the Taler header but it is deprecated ${talerUri}`,
);
break;
case TalerUriType.Unknown:
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
);
return;
default: {
const error: never = uriType;
logger.warn(
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
);
return;
}
}
chrome.tabs.create({ active: true, url }, () => {

View File

@ -181,10 +181,11 @@ function getContentForExample(item: ExampleItem | undefined): () => VNode {
item.component,
item.name,
);
if (!example)
if (!example) {
return function ExampleNotFoundMessage() {
return <div>example not found</div>;
};
}
return () => example.render(example.render.args);
}
@ -314,7 +315,9 @@ function ErrorReport({
children: ComponentChild;
selected: ExampleItem | undefined;
}): VNode {
const [error] = useErrorBoundary();
const [error, resetError] = useErrorBoundary();
//if there is an error, reset when unloading this component
useEffect(() => (error ? resetError : undefined));
if (error) {
return (
<div>

View File

@ -60,8 +60,10 @@ import {
DestinationSelectionSendCash,
} from "./DestinationSelection.js";
import { ExchangeSelectionPage } from "./ExchangeSelection/index.js";
import { InvoicePage } from "./Invoice/index.js";
import { SendPage } from "./Send/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";
export function Application(): VNode {
const [globalNotification, setGlobalNotification] = useState<
@ -153,7 +155,7 @@ export function Application(): VNode {
redirectTo(Pages.balanceDeposit({ amount }))
}
goToWalletWalletSend={(amount: string) =>
redirectTo(Pages.send({ amount }))
redirectTo(Pages.ctaTransferCreate({ amount }))
}
/>
<Route
@ -163,11 +165,9 @@ export function Application(): VNode {
redirectTo(Pages.ctaWithdrawManual({ amount }))
}
goToWalletWalletInvoice={(amount?: string) =>
redirectTo(Pages.invoice({ amount }))
redirectTo(Pages.ctaInvoiceCreate({ amount }))
}
/>
<Route path={Pages.invoice.pattern} component={InvoicePage} />
<Route path={Pages.send.pattern} component={SendPage} />
<Route
path={Pages.balanceTransaction.pattern}
@ -275,6 +275,20 @@ export function Application(): VNode {
component={DepositPageCTA}
cancel={() => redirectTo(Pages.balance)}
/>
<Route
path={Pages.ctaInvoiceCreate.pattern}
component={InvoiceCreatePage}
/>
<Route
path={Pages.ctaTransferCreate.pattern}
component={TransferCreatePage}
/>
<Route path={Pages.ctaInvoicePay} component={InvoicePayPage} />
<Route
path={Pages.ctaTransferPickup}
component={TransferPickupPage}
/>
{/**
* NOT FOUND

View File

@ -25,6 +25,10 @@ import {
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionPeerPullCredit,
TransactionPeerPullDebit,
TransactionPeerPushCredit,
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
TransactionTip,
@ -118,6 +122,31 @@ const exampleData = {
},
refundPending: undefined,
} as TransactionRefund,
push_credit: {
...commonTransaction(),
type: TransactionType.PeerPushCredit,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPushCredit,
push_debit: {
...commonTransaction(),
type: TransactionType.PeerPushDebit,
talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPushDebit,
pull_credit: {
...commonTransaction(),
type: TransactionType.PeerPullCredit,
talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPullCredit,
pull_debit: {
...commonTransaction(),
type: TransactionType.PeerPullDebit,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPullDebit,
};
export const NoBalance = createExample(TestedComponent, {
@ -327,3 +356,21 @@ export const FiveOfficialCurrenciesWithHighValue = createExample(
],
},
);
export const PeerToPeer = createExample(TestedComponent, {
transactions: [
exampleData.pull_credit,
exampleData.pull_debit,
exampleData.push_credit,
exampleData.push_debit,
],
balances: [
{
available: "USD:10",
pendingIncoming: "USD:0",
pendingOutgoing: "USD:0",
hasPendingTransactions: false,
requiresUserInput: false,
},
],
});

View File

@ -1,56 +0,0 @@
/*
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 } from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { h, VNode } from "preact";
import { LoadingError } from "../../components/LoadingError.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
return (
<LoadingError
title={<i18n.Translate>Could not load</i18n.Translate>}
error={error}
/>
);
}
const Container = styled.div``;
export function ReadyView({ amount, subject }: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<Container>
<p>Creating an invoice of {Amounts.stringify(amount)}</p>
<TextField
label="Subject"
variant="filled"
required
value={subject.value}
onChange={subject.onInput}
/>
<p>to:</p>
<Button>Scan QR code</Button>
</Container>
);
}

View File

@ -25,6 +25,10 @@ import {
TransactionCommon,
TransactionDeposit,
TransactionPayment,
TransactionPeerPullCredit,
TransactionPeerPullDebit,
TransactionPeerPushCredit,
TransactionPeerPushDebit,
TransactionRefresh,
TransactionRefund,
TransactionTip,
@ -139,6 +143,30 @@ const exampleData = {
},
refundPending: undefined,
} as TransactionRefund,
push_credit: {
...commonTransaction,
type: TransactionType.PeerPushCredit,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPushCredit,
push_debit: {
...commonTransaction,
type: TransactionType.PeerPushDebit,
talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPushDebit,
pull_credit: {
...commonTransaction,
type: TransactionType.PeerPullCredit,
talerUri:
"taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPullCredit,
pull_debit: {
...commonTransaction,
type: TransactionType.PeerPullDebit,
exchangeBaseUrl: "https://exchange.taler.net",
} as TransactionPeerPullDebit,
};
const transactionError = {
@ -498,3 +526,19 @@ export const RefundError = createExample(TestedComponent, {
export const RefundPending = createExample(TestedComponent, {
transaction: { ...exampleData.refund, pending: true },
});
export const InvoiceCredit = createExample(TestedComponent, {
transaction: { ...exampleData.pull_credit },
});
export const InvoiceDebit = createExample(TestedComponent, {
transaction: { ...exampleData.pull_debit },
});
export const TransferCredit = createExample(TestedComponent, {
transaction: { ...exampleData.push_credit },
});
export const TransferDebit = createExample(TestedComponent, {
transaction: { ...exampleData.push_debit },
});

View File

@ -45,6 +45,7 @@ import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js";
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
import { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
import {
CenteredDialog,
@ -557,6 +558,172 @@ export function TransactionView({
);
}
function ShowQrWithCopy({ text }: { text: string }): VNode {
const [showing, setShowing] = useState(false);
async function copy(): Promise<void> {
navigator.clipboard.writeText(text);
}
async function toggle(): Promise<void> {
setShowing((s) => !s);
}
if (showing) {
return (
<div>
<QR text={text} />
<Button onClick={copy}>copy</Button>
<Button onClick={toggle}>hide qr</Button>
</div>
);
}
return (
<div>
<div>{text.substring(0, 64)}...</div>
<Button onClick={copy}>copy</Button>
<Button onClick={toggle}>show qr</Button>
</div>
);
}
if (transaction.type === TransactionType.PeerPullCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
total={total}
kind="positive"
>
Invoice
</Header>
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>URI</i18n.Translate>}
text={<ShowQrWithCopy text={transaction.talerUri} />}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<InvoiceDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPullDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
total={total}
kind="negative"
>
Invoice
</Header>
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<InvoiceDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPushDebit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Debit`}
total={total}
kind="negative"
>
Transfer
</Header>
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>URI</i18n.Translate>}
text={<QR text={transaction.talerUri} />}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<TransferDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.PeerPushCredit) {
const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
<Header
timestamp={transaction.timestamp}
type={i18n.str`Credit`}
total={total}
kind="positive"
>
Transfer
</Header>
<Part
title={<i18n.Translate>Exchange</i18n.Translate>}
text={transaction.exchangeBaseUrl}
kind="neutral"
/>
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={
<TransferDetails
amount={{
effective: Amounts.parseOrThrow(transaction.amountEffective),
raw: Amounts.parseOrThrow(transaction.amountRaw),
}}
/>
}
/>
</TransactionTemplate>
);
}
return <div />;
}
@ -736,6 +903,88 @@ export interface AmountWithFee {
raw: AmountJson;
}
export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>Invoice</td>
<td>
<Amount value={amount.raw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>Transaction fees</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>Total</td>
<td>
<Amount value={amount.effective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
const fee = Amounts.sub(amount.raw, amount.effective).amount;
const maxFrac = [amount.raw, amount.effective, fee]
.map((a) => Amounts.maxFractionalDigits(a))
.reduce((c, p) => Math.max(c, p), 0);
return (
<PurchaseDetailsTable>
<tr>
<td>Transfer</td>
<td>
<Amount value={amount.raw} maxFracSize={maxFrac} />
</td>
</tr>
{Amounts.isNonZero(fee) && (
<tr>
<td>Transaction fees</td>
<td>
<Amount value={fee} negative maxFracSize={maxFrac} />
</td>
</tr>
)}
<tr>
<td colSpan={2}>
<hr />
</td>
</tr>
<tr>
<td>Total</td>
<td>
<Amount value={amount.effective} maxFracSize={maxFrac} />
</td>
</tr>
</PurchaseDetailsTable>
);
}
export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();

View File

@ -24,12 +24,18 @@
import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalResult,
AcceptPeerPullPaymentRequest,
AcceptPeerPushPaymentRequest,
AcceptTipRequest,
AcceptWithdrawalResponse,
AddExchangeRequest,
AmountString,
ApplyRefundResponse,
BalancesResponse,
CheckPeerPullPaymentRequest,
CheckPeerPullPaymentResponse,
CheckPeerPushPaymentRequest,
CheckPeerPushPaymentResponse,
CoinDumpJson,
ConfirmPayResult,
CoreApiResponse,
@ -41,6 +47,10 @@ import {
GetExchangeWithdrawalInfo,
GetFeeForDepositRequest,
GetWithdrawalDetailsForUriRequest,
InitiatePeerPullPaymentRequest,
InitiatePeerPullPaymentResponse,
InitiatePeerPushPaymentRequest,
InitiatePeerPushPaymentResponse,
KnownBankAccounts,
Logger,
NotificationType,
@ -473,3 +483,24 @@ export function onUpdateNotification(
};
return platform.listenToWalletBackground(onNewMessage);
}
export function initiatePeerPushPayment(req: InitiatePeerPushPaymentRequest): Promise<InitiatePeerPushPaymentResponse> {
return callBackend("initiatePeerPushPayment", req);
}
export function checkPeerPushPayment(req: CheckPeerPushPaymentRequest): Promise<CheckPeerPushPaymentResponse> {
return callBackend("checkPeerPushPayment", req);
}
export function acceptPeerPushPayment(req: AcceptPeerPushPaymentRequest): Promise<void> {
return callBackend("acceptPeerPushPayment", req);
}
export function initiatePeerPullPayment(req: InitiatePeerPullPaymentRequest): Promise<InitiatePeerPullPaymentResponse> {
return callBackend("initiatePeerPullPayment", req);
}
export function checkPeerPullPayment(req: CheckPeerPullPaymentRequest): Promise<CheckPeerPullPaymentResponse> {
return callBackend("checkPeerPullPayment", req);
}
export function acceptPeerPullPayment(req: AcceptPeerPullPaymentRequest): Promise<void> {
return callBackend("acceptPeerPullPayment", req);
}

View File

@ -276,15 +276,35 @@ function parseTalerUriAndRedirect(tabId: number, talerUri: string): void {
tabId,
`/cta/refund?talerRefundUri=${talerUri}`,
);
case TalerUriType.TalerPayPull:
return platform.redirectTabToWalletPage(
tabId,
`/cta/invoice/pay?talerPayPullUri=${talerUri}`,
);
case TalerUriType.TalerPayPush:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
);
case TalerUriType.TalerNotifyReserve:
// FIXME: Is this still useful?
// handleNotifyReserve(w);
break;
default:
logger.warn(
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
`Response with HTTP 402 the Taler header but it is deprecated ${talerUri}`,
);
break;
case TalerUriType.Unknown:
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
);
return;
default: {
const error: never = uriType;
logger.warn(
`Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
);
return;
}
}
}