tip and refund stories and test
This commit is contained in:
parent
e5c9f588e4
commit
939729004a
@ -24,35 +24,13 @@
|
||||
* Imports.
|
||||
*/
|
||||
|
||||
import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
amountToPretty,
|
||||
ConfirmPayResult,
|
||||
ConfirmPayResultType,
|
||||
ContractTerms,
|
||||
NotificationType,
|
||||
PreparePayResult,
|
||||
PreparePayResultType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { TalerError } from "@gnu-taler/taler-wallet-core";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
|
||||
import { Loading } from "../components/Loading.js";
|
||||
import { LoadingError } from "../components/LoadingError.js";
|
||||
import { LogoHeader } from "../components/LogoHeader.js";
|
||||
import { Part } from "../components/Part.js";
|
||||
import {
|
||||
ErrorBox,
|
||||
SubTitle,
|
||||
SuccessBox,
|
||||
WalletAction,
|
||||
WarningBox,
|
||||
} from "../components/styled/index.js";
|
||||
import { SubTitle, WalletAction } from "../components/styled/index.js";
|
||||
import { useTranslationContext } from "../context/translation.js";
|
||||
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||
import * as wxApi from "../wxApi.js";
|
||||
import { HookError } from "../hooks/useAsyncAsHook.js";
|
||||
|
||||
interface Props {
|
||||
talerDepositUri?: string;
|
||||
@ -102,7 +80,7 @@ export function View({ state }: ViewProps): VNode {
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash deposit</i18n.Translate>
|
||||
<i18n.Translate>Digital cash refund</i18n.Translate>
|
||||
</SubTitle>
|
||||
</WalletAction>
|
||||
);
|
||||
|
@ -32,7 +32,7 @@ type Subs = {
|
||||
[key in NotificationType]?: VoidFunction
|
||||
}
|
||||
|
||||
class SubsHandler {
|
||||
export class SubsHandler {
|
||||
private subs: Subs = {};
|
||||
|
||||
constructor() {
|
||||
|
@ -353,7 +353,7 @@ export function View({
|
||||
);
|
||||
}
|
||||
|
||||
function ProductList({ products }: { products: Product[] }): VNode {
|
||||
export function ProductList({ products }: { products: Product[] }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -19,7 +19,7 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { OrderShortInfo } from "@gnu-taler/taler-util";
|
||||
import { Amounts } from "@gnu-taler/taler-util";
|
||||
import { createExample } from "../test-utils.js";
|
||||
import { View as TestedComponent } from "./Refund.js";
|
||||
|
||||
@ -30,46 +30,70 @@ export default {
|
||||
};
|
||||
|
||||
export const Complete = createExample(TestedComponent, {
|
||||
applyResult: {
|
||||
amountEffectivePaid: "USD:10",
|
||||
amountRefundGone: "USD:0",
|
||||
amountRefundGranted: "USD:2",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
info: {
|
||||
summary: "tasty cold beer",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
} as Partial<OrderShortInfo> as any,
|
||||
pendingAtExchange: false,
|
||||
proposalId: "proposal123",
|
||||
},
|
||||
});
|
||||
|
||||
export const Partial = createExample(TestedComponent, {
|
||||
applyResult: {
|
||||
amountEffectivePaid: "USD:10",
|
||||
amountRefundGone: "USD:1",
|
||||
amountRefundGranted: "USD:2",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
info: {
|
||||
summary: "tasty cold beer",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
} as Partial<OrderShortInfo> as any,
|
||||
pendingAtExchange: false,
|
||||
proposalId: "proposal123",
|
||||
state: {
|
||||
status: "completed",
|
||||
amount: Amounts.parseOrThrow("USD:1"),
|
||||
hook: undefined,
|
||||
merchantName: "the merchant",
|
||||
products: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
export const InProgress = createExample(TestedComponent, {
|
||||
applyResult: {
|
||||
amountEffectivePaid: "USD:10",
|
||||
amountRefundGone: "USD:1",
|
||||
amountRefundGranted: "USD:2",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
info: {
|
||||
summary: "tasty cold beer",
|
||||
contractTermsHash: "QWEASDZXC",
|
||||
} as Partial<OrderShortInfo> as any,
|
||||
pendingAtExchange: true,
|
||||
proposalId: "proposal123",
|
||||
state: {
|
||||
status: "in-progress",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow("USD:1"),
|
||||
merchantName: "the merchant",
|
||||
products: undefined,
|
||||
progress: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
export const Ready = createExample(TestedComponent, {
|
||||
state: {
|
||||
status: "ready",
|
||||
hook: undefined,
|
||||
accept: {},
|
||||
ignore: {},
|
||||
|
||||
amount: Amounts.parseOrThrow("USD:1"),
|
||||
merchantName: "the merchant",
|
||||
products: [],
|
||||
orderId: "abcdef",
|
||||
},
|
||||
});
|
||||
|
||||
import beer from "../../static-dev/beer.png";
|
||||
|
||||
export const WithAProductList = createExample(TestedComponent, {
|
||||
state: {
|
||||
status: "ready",
|
||||
hook: undefined,
|
||||
accept: {},
|
||||
ignore: {},
|
||||
amount: Amounts.parseOrThrow("USD:1"),
|
||||
merchantName: "the merchant",
|
||||
products: [
|
||||
{
|
||||
description: "beer",
|
||||
image: beer,
|
||||
quantity: 2,
|
||||
},
|
||||
{
|
||||
description: "t-shirt",
|
||||
price: "EUR:1",
|
||||
quantity: 5,
|
||||
},
|
||||
],
|
||||
orderId: "abcdef",
|
||||
},
|
||||
});
|
||||
|
||||
export const Ignored = createExample(TestedComponent, {
|
||||
state: {
|
||||
status: "ignored",
|
||||
hook: undefined,
|
||||
merchantName: "the merchant",
|
||||
},
|
||||
});
|
||||
|
243
packages/taler-wallet-webextension/src/cta/Refund.test.ts
Normal file
243
packages/taler-wallet-webextension/src/cta/Refund.test.ts
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 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 { Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util";
|
||||
import { expect } from "chai";
|
||||
import { mountHook } from "../test-utils.js";
|
||||
import { SubsHandler } from "./Pay.test.js";
|
||||
import { useComponentState } from "./Refund.jsx";
|
||||
|
||||
// onUpdateNotification: subscriptions.saveSubscription,
|
||||
|
||||
describe("Refund CTA states", () => {
|
||||
it("should tell the user that the URI is missing", async () => {
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState(undefined, {
|
||||
prepareRefund: async () => ({}),
|
||||
applyRefund: async () => ({}),
|
||||
onUpdateNotification: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
|
||||
expect(status).equals('loading')
|
||||
if (!hook) expect.fail();
|
||||
if (!hook.hasError) expect.fail();
|
||||
if (hook.operational) expect.fail();
|
||||
expect(hook.message).eq("ERROR_NO-URI-FOR-REFUND");
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should be ready after loading", async () => {
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://refund/asdasdas", {
|
||||
prepareRefund: async () => ({
|
||||
total: 0,
|
||||
applied: 0,
|
||||
failed: 0,
|
||||
amountEffectivePaid: 'EUR:2',
|
||||
info: {
|
||||
contractTermsHash: '123',
|
||||
merchant: {
|
||||
name: 'the merchant name'
|
||||
},
|
||||
orderId: 'orderId1',
|
||||
summary: 'the sumary'
|
||||
}
|
||||
} as PrepareRefundResult as any),
|
||||
applyRefund: async () => ({}),
|
||||
onUpdateNotification: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'ready') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.accept.onClick).not.undefined;
|
||||
expect(state.ignore.onClick).not.undefined;
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
expect(state.orderId).eq('orderId1');
|
||||
expect(state.products).undefined;
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should be ignored after clicking the ignore button", async () => {
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://refund/asdasdas", {
|
||||
prepareRefund: async () => ({
|
||||
total: 0,
|
||||
applied: 0,
|
||||
failed: 0,
|
||||
amountEffectivePaid: 'EUR:2',
|
||||
info: {
|
||||
contractTermsHash: '123',
|
||||
merchant: {
|
||||
name: 'the merchant name'
|
||||
},
|
||||
orderId: 'orderId1',
|
||||
summary: 'the sumary'
|
||||
}
|
||||
} as PrepareRefundResult as any),
|
||||
applyRefund: async () => ({}),
|
||||
onUpdateNotification: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'ready') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.accept.onClick).not.undefined;
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
expect(state.orderId).eq('orderId1');
|
||||
expect(state.products).undefined;
|
||||
|
||||
if (state.ignore.onClick === undefined) expect.fail();
|
||||
state.ignore.onClick()
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'ignored') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should be in progress when doing refresh", async () => {
|
||||
let numApplied = 1;
|
||||
const subscriptions = new SubsHandler();
|
||||
|
||||
function notifyMelt(): void {
|
||||
numApplied++;
|
||||
subscriptions.notifyEvent(NotificationType.RefreshMelted)
|
||||
}
|
||||
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://refund/asdasdas", {
|
||||
prepareRefund: async () => ({
|
||||
total: 3,
|
||||
applied: numApplied,
|
||||
failed: 0,
|
||||
amountEffectivePaid: 'EUR:2',
|
||||
info: {
|
||||
contractTermsHash: '123',
|
||||
merchant: {
|
||||
name: 'the merchant name'
|
||||
},
|
||||
orderId: 'orderId1',
|
||||
summary: 'the sumary'
|
||||
}
|
||||
} as PrepareRefundResult as any),
|
||||
applyRefund: async () => ({}),
|
||||
onUpdateNotification: subscriptions.saveSubscription,
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'in-progress') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
expect(state.products).undefined;
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
||||
expect(state.progress).closeTo(1 / 3, 0.01)
|
||||
|
||||
notifyMelt()
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'in-progress') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
expect(state.products).undefined;
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
||||
expect(state.progress).closeTo(2 / 3, 0.01)
|
||||
|
||||
notifyMelt()
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== 'completed') expect.fail();
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.merchantName).eq('the merchant name');
|
||||
expect(state.products).undefined;
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
});
|
@ -21,83 +21,282 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
amountFractionalBase,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
ApplyRefundResponse,
|
||||
NotificationType,
|
||||
Product,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { SubTitle, Title } from "../components/styled/index.js";
|
||||
import { Amount } from "../components/Amount.js";
|
||||
import { Loading } from "../components/Loading.js";
|
||||
import { LoadingError } from "../components/LoadingError.js";
|
||||
import { LogoHeader } from "../components/LogoHeader.js";
|
||||
import { Part } from "../components/Part.js";
|
||||
import {
|
||||
Button,
|
||||
ButtonSuccess,
|
||||
SubTitle,
|
||||
WalletAction,
|
||||
} from "../components/styled/index.js";
|
||||
import { useTranslationContext } from "../context/translation.js";
|
||||
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||
import { ButtonHandler } from "../mui/handlers.js";
|
||||
import * as wxApi from "../wxApi.js";
|
||||
import { ProductList } from "./Pay.js";
|
||||
|
||||
interface Props {
|
||||
talerRefundUri?: string;
|
||||
}
|
||||
export interface ViewProps {
|
||||
applyResult: ApplyRefundResponse;
|
||||
state: State;
|
||||
}
|
||||
export function View({ applyResult }: ViewProps): VNode {
|
||||
export function View({ state }: ViewProps): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
return (
|
||||
<section class="main">
|
||||
<Title>GNU Taler Wallet</Title>
|
||||
<article class="fade">
|
||||
if (state.status === "loading") {
|
||||
if (!state.hook) {
|
||||
return <Loading />;
|
||||
}
|
||||
return (
|
||||
<LoadingError
|
||||
title={<i18n.Translate>Could not load refund status</i18n.Translate>}
|
||||
error={state.hook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "ignored") {
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Refund Status</i18n.Translate>
|
||||
<i18n.Translate>Digital cash refund</i18n.Translate>
|
||||
</SubTitle>
|
||||
<section>
|
||||
<p>
|
||||
<i18n.Translate>You've ignored the tip.</i18n.Translate>
|
||||
</p>
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "in-progress") {
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash refund</i18n.Translate>
|
||||
</SubTitle>
|
||||
<section>
|
||||
<p>
|
||||
<i18n.Translate>The refund is in progress.</i18n.Translate>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Total to refund</i18n.Translate>}
|
||||
text={<Amount value={state.amount} />}
|
||||
kind="negative"
|
||||
/>
|
||||
</section>
|
||||
{state.products && state.products.length ? (
|
||||
<section>
|
||||
<ProductList products={state.products} />
|
||||
</section>
|
||||
) : undefined}
|
||||
<section>
|
||||
<ProgressBar value={state.progress} />
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "completed") {
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash refund</i18n.Translate>
|
||||
</SubTitle>
|
||||
<section>
|
||||
<p>
|
||||
<i18n.Translate>this refund is already accepted.</i18n.Translate>
|
||||
</p>
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash refund</i18n.Translate>
|
||||
</SubTitle>
|
||||
<section>
|
||||
<p>
|
||||
<i18n.Translate>
|
||||
The product <em>{applyResult.info.summary}</em> has received a total
|
||||
effective refund of{" "}
|
||||
The merchant "<b>{state.merchantName}</b>" is offering you
|
||||
a refund.
|
||||
</i18n.Translate>
|
||||
<AmountView amount={applyResult.amountRefundGranted} />.
|
||||
</p>
|
||||
{applyResult.pendingAtExchange ? (
|
||||
<p>
|
||||
<i18n.Translate>
|
||||
Refund processing is still in progress.
|
||||
</i18n.Translate>
|
||||
</p>
|
||||
) : null}
|
||||
{!Amounts.isZero(applyResult.amountRefundGone) ? (
|
||||
<p>
|
||||
<i18n.Translate>
|
||||
The refund amount of{" "}
|
||||
<AmountView amount={applyResult.amountRefundGone} /> could not be
|
||||
applied.
|
||||
</i18n.Translate>
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
<section>
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Total to refund</i18n.Translate>}
|
||||
text={<Amount value={state.amount} />}
|
||||
kind="negative"
|
||||
/>
|
||||
</section>
|
||||
{state.products && state.products.length ? (
|
||||
<section>
|
||||
<ProductList products={state.products} />
|
||||
</section>
|
||||
) : undefined}
|
||||
<section>
|
||||
<ButtonSuccess onClick={state.accept.onClick}>
|
||||
<i18n.Translate>Confirm refund</i18n.Translate>
|
||||
</ButtonSuccess>
|
||||
<Button onClick={state.ignore.onClick}>
|
||||
<i18n.Translate>Ignore</i18n.Translate>
|
||||
</Button>
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
export function RefundPage({ talerRefundUri }: Props): VNode {
|
||||
const [applyResult, setApplyResult] = useState<
|
||||
ApplyRefundResponse | undefined
|
||||
>(undefined);
|
||||
const { i18n } = useTranslationContext();
|
||||
const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
|
||||
|
||||
type State = Loading | Ready | Ignored | InProgress | Completed;
|
||||
|
||||
interface Loading {
|
||||
status: "loading";
|
||||
hook: HookError | undefined;
|
||||
}
|
||||
interface Ready {
|
||||
status: "ready";
|
||||
hook: undefined;
|
||||
merchantName: string;
|
||||
products: Product[] | undefined;
|
||||
amount: AmountJson;
|
||||
accept: ButtonHandler;
|
||||
ignore: ButtonHandler;
|
||||
orderId: string;
|
||||
}
|
||||
interface Ignored {
|
||||
status: "ignored";
|
||||
hook: undefined;
|
||||
merchantName: string;
|
||||
}
|
||||
interface InProgress {
|
||||
status: "in-progress";
|
||||
hook: undefined;
|
||||
merchantName: string;
|
||||
products: Product[] | undefined;
|
||||
amount: AmountJson;
|
||||
progress: number;
|
||||
}
|
||||
interface Completed {
|
||||
status: "completed";
|
||||
hook: undefined;
|
||||
merchantName: string;
|
||||
products: Product[] | undefined;
|
||||
amount: AmountJson;
|
||||
}
|
||||
|
||||
export function useComponentState(
|
||||
talerRefundUri: string | undefined,
|
||||
api: typeof wxApi,
|
||||
): State {
|
||||
const [ignored, setIgnored] = useState(false);
|
||||
|
||||
const info = useAsyncAsHook(async () => {
|
||||
if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
|
||||
const refund = await api.prepareRefund({ talerRefundUri });
|
||||
return { refund, uri: talerRefundUri };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!talerRefundUri) return;
|
||||
const doFetch = async (): Promise<void> => {
|
||||
try {
|
||||
const result = await wxApi.applyRefund(talerRefundUri);
|
||||
setApplyResult(result);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setErrMsg(e.message);
|
||||
console.log("err message", e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
doFetch();
|
||||
}, [talerRefundUri]);
|
||||
api.onUpdateNotification([NotificationType.RefreshMelted], () => {
|
||||
info?.retry();
|
||||
});
|
||||
});
|
||||
|
||||
console.log("rendering");
|
||||
if (!info || info.hasError) {
|
||||
return {
|
||||
status: "loading",
|
||||
hook: info,
|
||||
};
|
||||
}
|
||||
|
||||
const { refund, uri } = info.response;
|
||||
|
||||
const doAccept = async (): Promise<void> => {
|
||||
await api.applyRefund(uri);
|
||||
info.retry();
|
||||
};
|
||||
|
||||
const doIgnore = async (): Promise<void> => {
|
||||
setIgnored(true);
|
||||
};
|
||||
|
||||
if (ignored) {
|
||||
return {
|
||||
status: "ignored",
|
||||
hook: undefined,
|
||||
merchantName: info.response.refund.info.merchant.name,
|
||||
};
|
||||
}
|
||||
|
||||
const pending = refund.total > refund.applied + refund.failed;
|
||||
const completed = refund.total > 0 && refund.applied === refund.total;
|
||||
|
||||
if (pending) {
|
||||
return {
|
||||
status: "in-progress",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
||||
merchantName: info.response.refund.info.merchant.name,
|
||||
products: info.response.refund.info.products,
|
||||
progress: (refund.applied + refund.failed) / refund.total,
|
||||
};
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
return {
|
||||
status: "completed",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
||||
merchantName: info.response.refund.info.merchant.name,
|
||||
products: info.response.refund.info.products,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ready",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
||||
merchantName: info.response.refund.info.merchant.name,
|
||||
products: info.response.refund.info.products,
|
||||
orderId: info.response.refund.info.orderId,
|
||||
accept: {
|
||||
onClick: doAccept,
|
||||
},
|
||||
ignore: {
|
||||
onClick: doIgnore,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function RefundPage({ talerRefundUri }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const state = useComponentState(talerRefundUri, wxApi);
|
||||
|
||||
if (!talerRefundUri) {
|
||||
return (
|
||||
@ -107,43 +306,26 @@ export function RefundPage({ talerRefundUri }: Props): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
if (errMsg) {
|
||||
return (
|
||||
<span>
|
||||
<i18n.Translate>Error: {errMsg}</i18n.Translate>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!applyResult) {
|
||||
return (
|
||||
<span>
|
||||
<i18n.Translate>Updating refund status</i18n.Translate>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <View applyResult={applyResult} />;
|
||||
return <View state={state} />;
|
||||
}
|
||||
|
||||
export function renderAmount(amount: AmountJson | string): VNode {
|
||||
let a;
|
||||
if (typeof amount === "string") {
|
||||
a = Amounts.parse(amount);
|
||||
} else {
|
||||
a = amount;
|
||||
}
|
||||
if (!a) {
|
||||
return <span>(invalid amount)</span>;
|
||||
}
|
||||
const x = a.value + a.fraction / amountFractionalBase;
|
||||
function ProgressBar({ value }: { value: number }): VNode {
|
||||
return (
|
||||
<span>
|
||||
{x} {a.currency}
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
height: 20,
|
||||
backgroundColor: "white",
|
||||
border: "solid black 1px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${value * 100}%`,
|
||||
height: "100%",
|
||||
backgroundColor: "lightgreen",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AmountView({ amount }: { amount: AmountJson | string }): VNode {
|
||||
return renderAmount(amount);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
|
||||
import { Amounts } from "@gnu-taler/taler-util";
|
||||
import { createExample } from "../test-utils.js";
|
||||
import { View as TestedComponent } from "./Tip.js";
|
||||
|
||||
@ -30,25 +30,23 @@ export default {
|
||||
};
|
||||
|
||||
export const Accepted = createExample(TestedComponent, {
|
||||
prepareTipResult: {
|
||||
accepted: true,
|
||||
merchantBaseUrl: "",
|
||||
state: {
|
||||
status: "accepted",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow("EUR:1"),
|
||||
exchangeBaseUrl: "",
|
||||
expirationTimestamp: TalerProtocolTimestamp.fromSeconds(1),
|
||||
tipAmountEffective: "USD:10",
|
||||
tipAmountRaw: "USD:5",
|
||||
walletTipId: "id",
|
||||
merchantBaseUrl: "",
|
||||
},
|
||||
});
|
||||
|
||||
export const NotYetAccepted = createExample(TestedComponent, {
|
||||
prepareTipResult: {
|
||||
accepted: false,
|
||||
export const Ready = createExample(TestedComponent, {
|
||||
state: {
|
||||
status: "ready",
|
||||
hook: undefined,
|
||||
amount: Amounts.parseOrThrow("EUR:1"),
|
||||
merchantBaseUrl: "http://merchant.url/",
|
||||
exchangeBaseUrl: "http://exchange.url/",
|
||||
expirationTimestamp: TalerProtocolTimestamp.fromSeconds(1),
|
||||
tipAmountEffective: "USD:10",
|
||||
tipAmountRaw: "USD:5",
|
||||
walletTipId: "id",
|
||||
accept: {},
|
||||
ignore: {},
|
||||
},
|
||||
});
|
||||
|
192
packages/taler-wallet-webextension/src/cta/Tip.test.ts
Normal file
192
packages/taler-wallet-webextension/src/cta/Tip.test.ts
Normal file
@ -0,0 +1,192 @@
|
||||
/*
|
||||
This file is part of GNU Taler
|
||||
(C) 2021 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 { Amounts, PrepareTipResult } from "@gnu-taler/taler-util";
|
||||
import { expect } from "chai";
|
||||
import { mountHook } from "../test-utils.js";
|
||||
import { useComponentState } from "./Tip.jsx";
|
||||
|
||||
describe("Tip CTA states", () => {
|
||||
it("should tell the user that the URI is missing", async () => {
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState(undefined, {
|
||||
prepareTip: async () => ({}),
|
||||
acceptTip: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
|
||||
expect(status).equals('loading')
|
||||
if (!hook) expect.fail();
|
||||
if (!hook.hasError) expect.fail();
|
||||
if (hook.operational) expect.fail();
|
||||
expect(hook.message).eq("ERROR_NO-URI-FOR-TIP");
|
||||
}
|
||||
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should be ready for accepting the tip", async () => {
|
||||
let tipAccepted = false;
|
||||
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://tip/asd", {
|
||||
prepareTip: async () => ({
|
||||
accepted: tipAccepted,
|
||||
exchangeBaseUrl: "exchange url",
|
||||
merchantBaseUrl: "merchant url",
|
||||
tipAmountEffective: "EUR:1",
|
||||
walletTipId: "tip_id",
|
||||
} as PrepareTipResult as any),
|
||||
acceptTip: async () => {
|
||||
tipAccepted = true
|
||||
}
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== "ready") expect.fail()
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
|
||||
expect(state.merchantBaseUrl).eq("merchant url");
|
||||
expect(state.exchangeBaseUrl).eq("exchange url");
|
||||
if (state.accept.onClick === undefined) expect.fail();
|
||||
|
||||
state.accept.onClick();
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== "accepted") expect.fail()
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
|
||||
expect(state.merchantBaseUrl).eq("merchant url");
|
||||
expect(state.exchangeBaseUrl).eq("exchange url");
|
||||
|
||||
}
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should be ignored after clicking the ignore button", async () => {
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://tip/asd", {
|
||||
prepareTip: async () => ({
|
||||
exchangeBaseUrl: "exchange url",
|
||||
merchantBaseUrl: "merchant url",
|
||||
tipAmountEffective: "EUR:1",
|
||||
walletTipId: "tip_id",
|
||||
} as PrepareTipResult as any),
|
||||
acceptTip: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== "ready") expect.fail()
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
|
||||
expect(state.merchantBaseUrl).eq("merchant url");
|
||||
expect(state.exchangeBaseUrl).eq("exchange url");
|
||||
if (state.ignore.onClick === undefined) expect.fail();
|
||||
|
||||
state.ignore.onClick();
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== "ignored") expect.fail()
|
||||
if (state.hook) expect.fail();
|
||||
|
||||
}
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
it("should render accepted if the tip has been used previously", async () => {
|
||||
|
||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||
useComponentState("taler://tip/asd", {
|
||||
prepareTip: async () => ({
|
||||
accepted: true,
|
||||
exchangeBaseUrl: "exchange url",
|
||||
merchantBaseUrl: "merchant url",
|
||||
tipAmountEffective: "EUR:1",
|
||||
walletTipId: "tip_id",
|
||||
} as PrepareTipResult as any),
|
||||
acceptTip: async () => ({})
|
||||
} as any),
|
||||
);
|
||||
|
||||
{
|
||||
const { status, hook } = getLastResultOrThrow()
|
||||
expect(status).equals('loading')
|
||||
expect(hook).undefined;
|
||||
}
|
||||
|
||||
await waitNextUpdate()
|
||||
|
||||
{
|
||||
const state = getLastResultOrThrow()
|
||||
|
||||
if (state.status !== "accepted") expect.fail()
|
||||
if (state.hook) expect.fail();
|
||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
|
||||
expect(state.merchantBaseUrl).eq("merchant url");
|
||||
expect(state.exchangeBaseUrl).eq("exchange url");
|
||||
|
||||
}
|
||||
await assertNoPendingUpdate()
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -20,98 +20,210 @@
|
||||
* @author sebasjm
|
||||
*/
|
||||
|
||||
import {
|
||||
amountFractionalBase,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
PrepareTipResult,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AmountJson, Amounts, PrepareTipResult } from "@gnu-taler/taler-util";
|
||||
import { h, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Amount } from "../components/Amount.js";
|
||||
import { Loading } from "../components/Loading.js";
|
||||
import { Title } from "../components/styled/index.js";
|
||||
import { LoadingError } from "../components/LoadingError.js";
|
||||
import { LogoHeader } from "../components/LogoHeader.js";
|
||||
import { Part } from "../components/Part.js";
|
||||
import {
|
||||
Button,
|
||||
ButtonSuccess,
|
||||
SubTitle,
|
||||
WalletAction,
|
||||
} from "../components/styled/index.js";
|
||||
import { useTranslationContext } from "../context/translation.js";
|
||||
import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||
import { ButtonHandler } from "../mui/handlers.js";
|
||||
import * as wxApi from "../wxApi.js";
|
||||
|
||||
interface Props {
|
||||
talerTipUri?: string;
|
||||
}
|
||||
export interface ViewProps {
|
||||
prepareTipResult: PrepareTipResult;
|
||||
onAccept: () => void;
|
||||
onIgnore: () => void;
|
||||
|
||||
type State = Loading | Ready | Accepted | Ignored;
|
||||
|
||||
interface Loading {
|
||||
status: "loading";
|
||||
hook: HookError | undefined;
|
||||
}
|
||||
export function View({
|
||||
prepareTipResult,
|
||||
onAccept,
|
||||
onIgnore,
|
||||
}: ViewProps): VNode {
|
||||
|
||||
interface Ignored {
|
||||
status: "ignored";
|
||||
hook: undefined;
|
||||
}
|
||||
interface Accepted {
|
||||
status: "accepted";
|
||||
hook: undefined;
|
||||
merchantBaseUrl: string;
|
||||
amount: AmountJson;
|
||||
exchangeBaseUrl: string;
|
||||
}
|
||||
interface Ready {
|
||||
status: "ready";
|
||||
hook: undefined;
|
||||
merchantBaseUrl: string;
|
||||
amount: AmountJson;
|
||||
exchangeBaseUrl: string;
|
||||
accept: ButtonHandler;
|
||||
ignore: ButtonHandler;
|
||||
}
|
||||
|
||||
export function useComponentState(
|
||||
talerTipUri: string | undefined,
|
||||
api: typeof wxApi,
|
||||
): State {
|
||||
const [tipIgnored, setTipIgnored] = useState(false);
|
||||
|
||||
const tipInfo = useAsyncAsHook(async () => {
|
||||
if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP");
|
||||
const tip = await api.prepareTip({ talerTipUri });
|
||||
return { tip };
|
||||
});
|
||||
|
||||
if (!tipInfo || tipInfo.hasError) {
|
||||
return {
|
||||
status: "loading",
|
||||
hook: tipInfo,
|
||||
};
|
||||
}
|
||||
|
||||
const { tip } = tipInfo.response;
|
||||
|
||||
const doAccept = async (): Promise<void> => {
|
||||
await api.acceptTip({ walletTipId: tip.walletTipId });
|
||||
tipInfo.retry();
|
||||
};
|
||||
|
||||
const doIgnore = async (): Promise<void> => {
|
||||
setTipIgnored(true);
|
||||
};
|
||||
|
||||
if (tipIgnored) {
|
||||
return {
|
||||
status: "ignored",
|
||||
hook: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (tip.accepted) {
|
||||
return {
|
||||
status: "accepted",
|
||||
hook: undefined,
|
||||
merchantBaseUrl: tip.merchantBaseUrl,
|
||||
exchangeBaseUrl: tip.exchangeBaseUrl,
|
||||
amount: Amounts.parseOrThrow(tip.tipAmountEffective),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ready",
|
||||
hook: undefined,
|
||||
merchantBaseUrl: tip.merchantBaseUrl,
|
||||
exchangeBaseUrl: tip.exchangeBaseUrl,
|
||||
accept: {
|
||||
onClick: doAccept,
|
||||
},
|
||||
ignore: {
|
||||
onClick: doIgnore,
|
||||
},
|
||||
amount: Amounts.parseOrThrow(tip.tipAmountEffective),
|
||||
};
|
||||
}
|
||||
|
||||
export function View({ state }: { state: State }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
if (state.status === "loading") {
|
||||
if (!state.hook) {
|
||||
return <Loading />;
|
||||
}
|
||||
return (
|
||||
<LoadingError
|
||||
title={<i18n.Translate>Could not load tip status</i18n.Translate>}
|
||||
error={state.hook}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "ignored") {
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash tip</i18n.Translate>
|
||||
</SubTitle>
|
||||
<span>
|
||||
<i18n.Translate>You've ignored the tip.</i18n.Translate>
|
||||
</span>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "accepted") {
|
||||
return (
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash tip</i18n.Translate>
|
||||
</SubTitle>
|
||||
<section>
|
||||
<i18n.Translate>
|
||||
Tip from <code>{state.merchantBaseUrl}</code> accepted. Check your
|
||||
transactions list for more details.
|
||||
</i18n.Translate>
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class="main">
|
||||
<Title>GNU Taler Wallet</Title>
|
||||
<article class="fade">
|
||||
{prepareTipResult.accepted ? (
|
||||
<span>
|
||||
<i18n.Translate>
|
||||
Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted.
|
||||
Check your transactions list for more details.
|
||||
</i18n.Translate>
|
||||
</span>
|
||||
) : (
|
||||
<div>
|
||||
<p>
|
||||
<i18n.Translate>
|
||||
The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is
|
||||
offering you a tip of{" "}
|
||||
<strong>
|
||||
<AmountView amount={prepareTipResult.tipAmountEffective} />
|
||||
</strong>{" "}
|
||||
via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
|
||||
</i18n.Translate>
|
||||
</p>
|
||||
<button onClick={onAccept}>
|
||||
<i18n.Translate>Accept tip</i18n.Translate>
|
||||
</button>
|
||||
<button onClick={onIgnore}>
|
||||
<i18n.Translate>Ignore</i18n.Translate>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</section>
|
||||
<WalletAction>
|
||||
<LogoHeader />
|
||||
|
||||
<SubTitle>
|
||||
<i18n.Translate>Digital cash tip</i18n.Translate>
|
||||
</SubTitle>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
<i18n.Translate>The merchant is offering you a tip</i18n.Translate>
|
||||
</p>
|
||||
<Part
|
||||
title={<i18n.Translate>Amount</i18n.Translate>}
|
||||
text={<Amount value={state.amount} />}
|
||||
kind="positive"
|
||||
big
|
||||
/>
|
||||
<Part
|
||||
title={<i18n.Translate>Merchant URL</i18n.Translate>}
|
||||
text={state.merchantBaseUrl}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
title={<i18n.Translate>Exchange</i18n.Translate>}
|
||||
text={state.exchangeBaseUrl}
|
||||
kind="neutral"
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<ButtonSuccess onClick={state.accept.onClick}>
|
||||
<i18n.Translate>Accept tip</i18n.Translate>
|
||||
</ButtonSuccess>
|
||||
<Button onClick={state.ignore.onClick}>
|
||||
<i18n.Translate>Ignore</i18n.Translate>
|
||||
</Button>
|
||||
</section>
|
||||
</WalletAction>
|
||||
);
|
||||
}
|
||||
|
||||
export function TipPage({ talerTipUri }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const [updateCounter, setUpdateCounter] = useState<number>(0);
|
||||
const [prepareTipResult, setPrepareTipResult] = useState<
|
||||
PrepareTipResult | undefined
|
||||
>(undefined);
|
||||
|
||||
const [tipIgnored, setTipIgnored] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!talerTipUri) return;
|
||||
const doFetch = async (): Promise<void> => {
|
||||
const p = await wxApi.prepareTip({ talerTipUri });
|
||||
setPrepareTipResult(p);
|
||||
};
|
||||
doFetch();
|
||||
}, [talerTipUri, updateCounter]);
|
||||
|
||||
const doAccept = async (): Promise<void> => {
|
||||
if (!prepareTipResult) {
|
||||
return;
|
||||
}
|
||||
await wxApi.acceptTip({ walletTipId: prepareTipResult?.walletTipId });
|
||||
setUpdateCounter(updateCounter + 1);
|
||||
};
|
||||
|
||||
const doIgnore = (): void => {
|
||||
setTipIgnored(true);
|
||||
};
|
||||
const state = useComponentState(talerTipUri, wxApi);
|
||||
|
||||
if (!talerTipUri) {
|
||||
return (
|
||||
@ -121,45 +233,5 @@ export function TipPage({ talerTipUri }: Props): VNode {
|
||||
);
|
||||
}
|
||||
|
||||
if (tipIgnored) {
|
||||
return (
|
||||
<span>
|
||||
<i18n.Translate>You've ignored the tip.</i18n.Translate>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!prepareTipResult) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
prepareTipResult={prepareTipResult}
|
||||
onAccept={doAccept}
|
||||
onIgnore={doIgnore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAmount(amount: AmountJson | string): VNode {
|
||||
let a;
|
||||
if (typeof amount === "string") {
|
||||
a = Amounts.parse(amount);
|
||||
} else {
|
||||
a = amount;
|
||||
}
|
||||
if (!a) {
|
||||
return <span>(invalid amount)</span>;
|
||||
}
|
||||
const x = a.value + a.fraction / amountFractionalBase;
|
||||
return (
|
||||
<span>
|
||||
{x} {a.currency}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AmountView({ amount }: { amount: AmountJson | string }): VNode {
|
||||
return renderAmount(amount);
|
||||
return <View state={state} />;
|
||||
}
|
||||
|
@ -515,13 +515,13 @@ export function TransactionView({
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Total tip</i18n.Translate>}
|
||||
text={<Amount value={transaction.amountEffective} />}
|
||||
text={<Amount value={transaction.amountRaw} />}
|
||||
kind="positive"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title={<i18n.Translate>Received amount</i18n.Translate>}
|
||||
text={<Amount value={transaction.amountRaw} />}
|
||||
text={<Amount value={transaction.amountEffective} />}
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
|
@ -44,6 +44,8 @@ import {
|
||||
KnownBankAccounts,
|
||||
NotificationType,
|
||||
PreparePayResult,
|
||||
PrepareRefundRequest,
|
||||
PrepareRefundResult,
|
||||
PrepareTipRequest,
|
||||
PrepareTipResult,
|
||||
RetryTransactionRequest,
|
||||
@ -405,6 +407,11 @@ export function addExchange(req: AddExchangeRequest): Promise<void> {
|
||||
return callBackend("addExchange", req);
|
||||
}
|
||||
|
||||
export function prepareRefund(req: PrepareRefundRequest): Promise<PrepareRefundResult> {
|
||||
return callBackend("prepareRefund", req);
|
||||
}
|
||||
|
||||
|
||||
export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
|
||||
return callBackend("prepareTip", req);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user