payments test case
This commit is contained in:
parent
8e29f91a56
commit
64acf8e2b1
@ -19,9 +19,13 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
Amounts,
|
||||||
|
ContractTerms,
|
||||||
|
PreparePayResultType,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
import { createExample } from "../test-utils.js";
|
import { createExample } from "../test-utils.js";
|
||||||
import { PaymentRequestView as TestedComponent } from "./Pay.js";
|
import { View as TestedComponent } from "./Pay.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "cta/pay",
|
title: "cta/pay",
|
||||||
@ -30,175 +34,323 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NoBalance = createExample(TestedComponent, {
|
export const NoBalance = createExample(TestedComponent, {
|
||||||
payStatus: {
|
state: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: "ready",
|
||||||
noncePriv: "",
|
hook: undefined,
|
||||||
proposalId: "proposal1234",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
contractTerms: {
|
balance: undefined,
|
||||||
merchant: {
|
payHandler: {
|
||||||
name: "someone",
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
},
|
||||||
amount: "USD:10",
|
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
amountRaw: "USD:10",
|
uri: "",
|
||||||
|
payStatus: {
|
||||||
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
noncePriv: "",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
contractTerms: {
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
summary: "some beers",
|
||||||
|
amount: "USD:10",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NoEnoughBalance = createExample(TestedComponent, {
|
export const NoEnoughBalance = createExample(TestedComponent, {
|
||||||
payStatus: {
|
state: {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: "ready",
|
||||||
noncePriv: "",
|
hook: undefined,
|
||||||
proposalId: "proposal1234",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
contractTerms: {
|
balance: {
|
||||||
merchant: {
|
currency: "USD",
|
||||||
name: "someone",
|
fraction: 40000000,
|
||||||
|
value: 9,
|
||||||
|
},
|
||||||
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
},
|
||||||
amount: "USD:10",
|
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
amountRaw: "USD:10",
|
uri: "",
|
||||||
},
|
payStatus: {
|
||||||
balance: {
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
currency: "USD",
|
noncePriv: "",
|
||||||
fraction: 40000000,
|
proposalId: "proposal1234",
|
||||||
value: 9,
|
contractTerms: {
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
summary: "some beers",
|
||||||
|
amount: "USD:10",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PaymentPossible = createExample(TestedComponent, {
|
export const PaymentPossible = createExample(TestedComponent, {
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
state: {
|
||||||
payStatus: {
|
status: "ready",
|
||||||
status: PreparePayResultType.PaymentPossible,
|
hook: undefined,
|
||||||
amountEffective: "USD:10",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
amountRaw: "USD:10",
|
balance: {
|
||||||
noncePriv: "",
|
currency: "USD",
|
||||||
contractTerms: {
|
fraction: 40000000,
|
||||||
nonce: "123213123",
|
value: 11,
|
||||||
merchant: {
|
},
|
||||||
name: "someone",
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
},
|
||||||
summary: "some beers",
|
totalFees: Amounts.parseOrThrow("USD:0"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
contractTermsHash: "123456",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
proposalId: "proposal1234",
|
payStatus: {
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
amountEffective: "USD:10",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
noncePriv: "",
|
||||||
|
contractTerms: {
|
||||||
|
nonce: "123213123",
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
amount: "USD:10",
|
||||||
|
summary: "some beers",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PaymentPossibleWithFee = createExample(TestedComponent, {
|
export const PaymentPossibleWithFee = createExample(TestedComponent, {
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
state: {
|
||||||
payStatus: {
|
status: "ready",
|
||||||
status: PreparePayResultType.PaymentPossible,
|
hook: undefined,
|
||||||
amountEffective: "USD:10.20",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
amountRaw: "USD:10",
|
balance: {
|
||||||
noncePriv: "",
|
currency: "USD",
|
||||||
contractTerms: {
|
fraction: 40000000,
|
||||||
nonce: "123213123",
|
value: 11,
|
||||||
merchant: {
|
},
|
||||||
name: "someone",
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
},
|
||||||
summary: "some beers",
|
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
contractTermsHash: "123456",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
proposalId: "proposal1234",
|
payStatus: {
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
amountEffective: "USD:10.20",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
noncePriv: "",
|
||||||
|
contractTerms: {
|
||||||
|
nonce: "123213123",
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
amount: "USD:10",
|
||||||
|
summary: "some beers",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
import beer from "../../static-dev/beer.png";
|
import beer from "../../static-dev/beer.png";
|
||||||
|
|
||||||
export const TicketWithAProductList = createExample(TestedComponent, {
|
export const TicketWithAProductList = createExample(TestedComponent, {
|
||||||
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
state: {
|
||||||
payStatus: {
|
status: "ready",
|
||||||
status: PreparePayResultType.PaymentPossible,
|
hook: undefined,
|
||||||
amountEffective: "USD:10",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
amountRaw: "USD:10",
|
balance: {
|
||||||
noncePriv: "",
|
currency: "USD",
|
||||||
contractTerms: {
|
fraction: 40000000,
|
||||||
nonce: "123213123",
|
value: 11,
|
||||||
merchant: {
|
},
|
||||||
name: "someone",
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
amount: "USD:10",
|
},
|
||||||
products: [
|
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||||
{
|
payResult: undefined,
|
||||||
description: "ten beers",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
price: "USD:1",
|
payStatus: {
|
||||||
quantity: 10,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
image: beer,
|
amountEffective: "USD:10.20",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
noncePriv: "",
|
||||||
|
contractTerms: {
|
||||||
|
nonce: "123213123",
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
},
|
},
|
||||||
{
|
amount: "USD:10",
|
||||||
description: "beer without image",
|
summary: "some beers",
|
||||||
price: "USD:1",
|
products: [
|
||||||
quantity: 10,
|
{
|
||||||
},
|
description: "ten beers",
|
||||||
{
|
price: "USD:1",
|
||||||
description: "one brown beer",
|
quantity: 10,
|
||||||
price: "USD:2",
|
image: beer,
|
||||||
quantity: 1,
|
},
|
||||||
image: beer,
|
{
|
||||||
},
|
description: "beer without image",
|
||||||
],
|
price: "USD:1",
|
||||||
summary: "some beers",
|
quantity: 10,
|
||||||
} as Partial<ContractTerms> as any,
|
},
|
||||||
contractTermsHash: "123456",
|
{
|
||||||
proposalId: "proposal1234",
|
description: "one brown beer",
|
||||||
|
price: "USD:2",
|
||||||
|
quantity: 1,
|
||||||
|
image: beer,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyConfirmedByOther = createExample(TestedComponent, {
|
export const AlreadyConfirmedByOther = createExample(TestedComponent, {
|
||||||
payStatus: {
|
state: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: "ready",
|
||||||
amountEffective: "USD:10",
|
hook: undefined,
|
||||||
amountRaw: "USD:10",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
contractTerms: {
|
balance: {
|
||||||
merchant: {
|
currency: "USD",
|
||||||
name: "someone",
|
fraction: 40000000,
|
||||||
|
value: 11,
|
||||||
|
},
|
||||||
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
},
|
||||||
amount: "USD:10",
|
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
contractTermsHash: "123456",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
proposalId: "proposal1234",
|
payStatus: {
|
||||||
paid: false,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
|
amountEffective: "USD:10",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
contractTerms: {
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
summary: "some beers",
|
||||||
|
amount: "USD:10",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
paid: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyPaidWithoutFulfillment = createExample(TestedComponent, {
|
export const AlreadyPaidWithoutFulfillment = createExample(TestedComponent, {
|
||||||
payStatus: {
|
state: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: "ready",
|
||||||
amountEffective: "USD:10",
|
hook: undefined,
|
||||||
amountRaw: "USD:10",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
contractTerms: {
|
balance: {
|
||||||
merchant: {
|
currency: "USD",
|
||||||
name: "someone",
|
fraction: 40000000,
|
||||||
|
value: 11,
|
||||||
|
},
|
||||||
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
summary: "some beers",
|
},
|
||||||
amount: "USD:10",
|
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||||
} as Partial<ContractTerms> as any,
|
payResult: undefined,
|
||||||
contractTermsHash: "123456",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
proposalId: "proposal1234",
|
payStatus: {
|
||||||
paid: true,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
|
amountEffective: "USD:10",
|
||||||
|
amountRaw: "USD:10",
|
||||||
|
contractTerms: {
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
summary: "some beers",
|
||||||
|
amount: "USD:10",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
paid: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlreadyPaidWithFulfillment = createExample(TestedComponent, {
|
export const AlreadyPaidWithFulfillment = createExample(TestedComponent, {
|
||||||
payStatus: {
|
state: {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: "ready",
|
||||||
amountEffective: "USD:10",
|
hook: undefined,
|
||||||
amountRaw: "USD:10",
|
amount: Amounts.parseOrThrow("USD:10"),
|
||||||
contractTerms: {
|
balance: {
|
||||||
merchant: {
|
currency: "USD",
|
||||||
name: "someone",
|
fraction: 40000000,
|
||||||
|
value: 11,
|
||||||
|
},
|
||||||
|
payHandler: {
|
||||||
|
onClick: async () => {
|
||||||
|
null;
|
||||||
},
|
},
|
||||||
fulfillment_message:
|
},
|
||||||
"congratulations! you are looking at the fulfillment message! ",
|
totalFees: Amounts.parseOrThrow("USD:0.20"),
|
||||||
summary: "some beers",
|
payResult: undefined,
|
||||||
amount: "USD:10",
|
uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
|
||||||
} as Partial<ContractTerms> as any,
|
payStatus: {
|
||||||
contractTermsHash: "123456",
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
proposalId: "proposal1234",
|
amountEffective: "USD:10",
|
||||||
paid: true,
|
amountRaw: "USD:10",
|
||||||
|
contractTerms: {
|
||||||
|
merchant: {
|
||||||
|
name: "someone",
|
||||||
|
},
|
||||||
|
fulfillment_message:
|
||||||
|
"congratulations! you are looking at the fulfillment message! ",
|
||||||
|
summary: "some beers",
|
||||||
|
amount: "USD:10",
|
||||||
|
} as Partial<ContractTerms> as any,
|
||||||
|
contractTermsHash: "123456",
|
||||||
|
proposalId: "proposal1234",
|
||||||
|
paid: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
goBack: () => null,
|
||||||
|
goToWalletManualWithdraw: () => null,
|
||||||
});
|
});
|
||||||
|
408
packages/taler-wallet-webextension/src/cta/Pay.test.ts
Normal file
408
packages/taler-wallet-webextension/src/cta/Pay.test.ts
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
/*
|
||||||
|
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 { AmountJson, Amounts, BalancesResponse, ConfirmPayResult, ConfirmPayResultType, NotificationType, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { mountHook } from "../test-utils.js";
|
||||||
|
import * as wxApi from "../wxApi.js";
|
||||||
|
import { useComponentState } from "./Pay.jsx";
|
||||||
|
|
||||||
|
const nullFunction: any = () => null;
|
||||||
|
type VoidFunction = () => void;
|
||||||
|
|
||||||
|
type Subs = {
|
||||||
|
[key in NotificationType]?: VoidFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubsHandler {
|
||||||
|
private subs: Subs = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.saveSubscription = this.saveSubscription.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSubscription(messageTypes: NotificationType[], callback: VoidFunction): VoidFunction {
|
||||||
|
messageTypes.forEach(m => {
|
||||||
|
this.subs[m] = callback;
|
||||||
|
})
|
||||||
|
return nullFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyEvent(event: NotificationType): void {
|
||||||
|
const cb = this.subs[event];
|
||||||
|
if (cb === undefined) expect.fail(`Expected to have a subscription for ${event}`);
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("Pay CTA states", () => {
|
||||||
|
it("should tell the user that the URI is missing", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState(undefined, {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
} as Partial<typeof wxApi> 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 === undefined) expect.fail()
|
||||||
|
expect(hook.hasError).true;
|
||||||
|
expect(hook.operational).false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should response with no balance", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:10',
|
||||||
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: []
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).undefined;
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10'))
|
||||||
|
expect(r.payHandler.onClick).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to pay if there is no enough balance", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:10',
|
||||||
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: 'USD:5'
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:5'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10'))
|
||||||
|
expect(r.payHandler.onClick).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to pay (without fee)", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:10',
|
||||||
|
amountEffective: 'USD:10',
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: 'USD:15'
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:0'))
|
||||||
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to pay (with fee)", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:9',
|
||||||
|
amountEffective: 'USD:10',
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: 'USD:15'
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get confirmation done after pay successfully", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:9',
|
||||||
|
amountEffective: 'USD:10',
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: 'USD:15'
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
confirmPay: async () => ({
|
||||||
|
type: ConfirmPayResultType.Done,
|
||||||
|
contractTerms: {}
|
||||||
|
} as Partial<ConfirmPayResult>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
if (r.payHandler.onClick === undefined) expect.fail();
|
||||||
|
r.payHandler.onClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'confirmed') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
|
||||||
|
expect(r.payResult.contractTerms).not.undefined;
|
||||||
|
expect(r.payHandler.onClick).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not stay in ready state after pay with error", async () => {
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: nullFunction,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:9',
|
||||||
|
amountEffective: 'USD:10',
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: 'USD:15'
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
confirmPay: async () => ({
|
||||||
|
type: ConfirmPayResultType.Pending,
|
||||||
|
lastError: { code: 1 },
|
||||||
|
} as Partial<ConfirmPayResult>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
if (r.payHandler.onClick === undefined) expect.fail();
|
||||||
|
r.payHandler.onClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
expect(r.payHandler.onClick).undefined;
|
||||||
|
if (r.payHandler.error === undefined) expect.fail();
|
||||||
|
//FIXME: error message here is bad
|
||||||
|
expect(r.payHandler.error.errorDetail.hint).eq("could not confirm payment")
|
||||||
|
expect(r.payHandler.error.errorDetail.payResult).deep.equal({
|
||||||
|
type: ConfirmPayResultType.Pending,
|
||||||
|
lastError: { code: 1 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update balance if a coins is withdraw", async () => {
|
||||||
|
const subscriptions = new SubsHandler();
|
||||||
|
let availableBalance = Amounts.parseOrThrow("USD:10");
|
||||||
|
|
||||||
|
function notifyCoinWithdrawn(newAmount: AmountJson): void {
|
||||||
|
availableBalance = Amounts.add(availableBalance, newAmount).amount
|
||||||
|
subscriptions.notifyEvent(NotificationType.CoinWithdrawn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
|
useComponentState('taller://pay', {
|
||||||
|
onUpdateNotification: subscriptions.saveSubscription,
|
||||||
|
preparePay: async () => ({
|
||||||
|
amountRaw: 'USD:9',
|
||||||
|
amountEffective: 'USD:10',
|
||||||
|
status: PreparePayResultType.PaymentPossible,
|
||||||
|
} as Partial<PreparePayResult>),
|
||||||
|
getBalance: async () => ({
|
||||||
|
balances: [{
|
||||||
|
available: Amounts.stringify(availableBalance)
|
||||||
|
}]
|
||||||
|
} as Partial<BalancesResponse>),
|
||||||
|
} as Partial<typeof wxApi> as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const { status, hook } = getLastResultOrThrow()
|
||||||
|
expect(status).equals('loading')
|
||||||
|
expect(hook).undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:10'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
|
|
||||||
|
notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitNextUpdate();
|
||||||
|
|
||||||
|
{
|
||||||
|
const r = getLastResultOrThrow()
|
||||||
|
if (r.status !== 'ready') expect.fail()
|
||||||
|
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15'));
|
||||||
|
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9'))
|
||||||
|
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1'))
|
||||||
|
expect(r.payHandler.onClick).not.undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertNoPendingUpdate()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@ -27,9 +27,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AmountJson,
|
AmountJson,
|
||||||
AmountLike,
|
|
||||||
Amounts,
|
Amounts,
|
||||||
AmountString,
|
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
ConfirmPayResultDone,
|
ConfirmPayResultDone,
|
||||||
ConfirmPayResultType,
|
ConfirmPayResultType,
|
||||||
@ -38,12 +36,14 @@ import {
|
|||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
Product,
|
Product,
|
||||||
|
TalerErrorCode,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerError } from "@gnu-taler/taler-wallet-core";
|
import { TalerError } from "@gnu-taler/taler-wallet-core";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Amount } from "../components/Amount.js";
|
import { Amount } from "../components/Amount.js";
|
||||||
import { ErrorMessage } from "../components/ErrorMessage.js";
|
import { ErrorMessage } from "../components/ErrorMessage.js";
|
||||||
|
import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { LoadingError } from "../components/LoadingError.js";
|
import { LoadingError } from "../components/LoadingError.js";
|
||||||
import { LogoHeader } from "../components/LogoHeader.js";
|
import { LogoHeader } from "../components/LogoHeader.js";
|
||||||
@ -60,7 +60,12 @@ import {
|
|||||||
WarningBox,
|
WarningBox,
|
||||||
} from "../components/styled/index.js";
|
} from "../components/styled/index.js";
|
||||||
import { useTranslationContext } from "../context/translation.js";
|
import { useTranslationContext } from "../context/translation.js";
|
||||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
import {
|
||||||
|
HookError,
|
||||||
|
useAsyncAsHook,
|
||||||
|
useAsyncAsHook2,
|
||||||
|
} from "../hooks/useAsyncAsHook.js";
|
||||||
|
import { ButtonHandler } from "../wallet/CreateManualWithdraw.js";
|
||||||
import * as wxApi from "../wxApi.js";
|
import * as wxApi from "../wxApi.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -69,47 +74,88 @@ interface Props {
|
|||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doPayment = async (
|
async function doPayment(
|
||||||
payStatus: PreparePayResult,
|
payStatus: PreparePayResult,
|
||||||
): Promise<ConfirmPayResultDone> => {
|
api: typeof wxApi,
|
||||||
|
): Promise<ConfirmPayResultDone> {
|
||||||
if (payStatus.status !== "payment-possible") {
|
if (payStatus.status !== "payment-possible") {
|
||||||
throw Error(`invalid state: ${payStatus.status}`);
|
throw TalerError.fromUncheckedDetail({
|
||||||
|
code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
|
||||||
|
hint: `payment is not possible: ${payStatus.status}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const proposalId = payStatus.proposalId;
|
const proposalId = payStatus.proposalId;
|
||||||
const res = await wxApi.confirmPay(proposalId, undefined);
|
const res = await api.confirmPay(proposalId, undefined);
|
||||||
if (res.type !== ConfirmPayResultType.Done) {
|
if (res.type !== ConfirmPayResultType.Done) {
|
||||||
throw Error("payment pending");
|
throw TalerError.fromUncheckedDetail({
|
||||||
|
code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
|
||||||
|
hint: `could not confirm payment`,
|
||||||
|
payResult: res,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const fu = res.contractTerms.fulfillment_url;
|
const fu = res.contractTerms.fulfillment_url;
|
||||||
if (fu) {
|
if (fu) {
|
||||||
document.location.href = fu;
|
document.location.href = fu;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
};
|
}
|
||||||
|
|
||||||
export function PayPage({
|
type State = Loading | Ready | Confirmed;
|
||||||
talerPayUri,
|
interface Loading {
|
||||||
goToWalletManualWithdraw,
|
status: "loading";
|
||||||
goBack,
|
hook: HookError | undefined;
|
||||||
}: Props): VNode {
|
}
|
||||||
const { i18n } = useTranslationContext();
|
interface Ready {
|
||||||
|
status: "ready";
|
||||||
|
hook: undefined;
|
||||||
|
uri: string;
|
||||||
|
amount: AmountJson;
|
||||||
|
totalFees: AmountJson;
|
||||||
|
payStatus: PreparePayResult;
|
||||||
|
balance: AmountJson | undefined;
|
||||||
|
payHandler: ButtonHandler;
|
||||||
|
payResult: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Confirmed {
|
||||||
|
status: "confirmed";
|
||||||
|
hook: undefined;
|
||||||
|
uri: string;
|
||||||
|
amount: AmountJson;
|
||||||
|
totalFees: AmountJson;
|
||||||
|
payStatus: PreparePayResult;
|
||||||
|
balance: AmountJson | undefined;
|
||||||
|
payResult: ConfirmPayResult;
|
||||||
|
payHandler: ButtonHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComponentState(
|
||||||
|
talerPayUri: string | undefined,
|
||||||
|
api: typeof wxApi,
|
||||||
|
): State {
|
||||||
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
|
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [payErrMsg, setPayErrMsg] = useState<TalerError | string | undefined>(
|
const [payErrMsg, setPayErrMsg] = useState<TalerError | undefined>(undefined);
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hook = useAsyncAsHook(async () => {
|
const hook = useAsyncAsHook2(async () => {
|
||||||
if (!talerPayUri) throw Error("Missing pay uri");
|
if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT");
|
||||||
const payStatus = await wxApi.preparePay(talerPayUri);
|
const payStatus = await api.preparePay(talerPayUri);
|
||||||
const balance = await wxApi.getBalance();
|
const balance = await api.getBalance();
|
||||||
return { payStatus, balance };
|
return { payStatus, balance, uri: talerPayUri };
|
||||||
}, [NotificationType.CoinWithdrawn]);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const payStatus =
|
api.onUpdateNotification([NotificationType.CoinWithdrawn], () => {
|
||||||
hook && !hook.hasError ? hook.response.payStatus : undefined;
|
hook?.retry();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const hookResponse = !hook || hook.hasError ? undefined : hook.response;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hookResponse) return;
|
||||||
|
const { payStatus } = hookResponse;
|
||||||
if (
|
if (
|
||||||
payStatus &&
|
payStatus &&
|
||||||
payStatus.status === PreparePayResultType.AlreadyConfirmed &&
|
payStatus.status === PreparePayResultType.AlreadyConfirmed &&
|
||||||
@ -122,74 +168,139 @@ export function PayPage({
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, [hookResponse]);
|
||||||
|
|
||||||
if (!hook) {
|
if (!hook || hook.hasError) {
|
||||||
return <Loading />;
|
return {
|
||||||
}
|
status: "loading",
|
||||||
|
hook,
|
||||||
if (hook.hasError) {
|
};
|
||||||
return (
|
|
||||||
<LoadingError
|
|
||||||
title={<i18n.Translate>Could not load pay status</i18n.Translate>}
|
|
||||||
error={hook}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const { payStatus } = hook.response;
|
||||||
|
const amount = Amounts.parseOrThrow(payStatus.amountRaw);
|
||||||
|
|
||||||
const foundBalance = hook.response.balance.balances.find(
|
const foundBalance = hook.response.balance.balances.find(
|
||||||
(b) =>
|
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
|
||||||
Amounts.parseOrThrow(b.available).currency ===
|
|
||||||
Amounts.parseOrThrow(hook.response.payStatus.amountRaw).currency,
|
|
||||||
);
|
);
|
||||||
const foundAmount = foundBalance
|
const foundAmount = foundBalance
|
||||||
? Amounts.parseOrThrow(foundBalance.available)
|
? Amounts.parseOrThrow(foundBalance.available)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const onClick = async (): Promise<void> => {
|
async function doPayment(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const res = await doPayment(hook.response.payStatus);
|
if (payStatus.status !== "payment-possible") {
|
||||||
|
throw TalerError.fromUncheckedDetail({
|
||||||
|
code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
|
||||||
|
hint: `payment is not possible: ${payStatus.status}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const res = await api.confirmPay(payStatus.proposalId, undefined);
|
||||||
|
if (res.type !== ConfirmPayResultType.Done) {
|
||||||
|
throw TalerError.fromUncheckedDetail({
|
||||||
|
code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
|
||||||
|
hint: `could not confirm payment`,
|
||||||
|
payResult: res,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const fu = res.contractTerms.fulfillment_url;
|
||||||
|
if (fu) {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
document.location.href = fu;
|
||||||
|
} else {
|
||||||
|
console.log(`should redirect to ${fu}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
setPayResult(res);
|
setPayResult(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
if (e instanceof TalerError) {
|
||||||
if (e instanceof Error) {
|
setPayErrMsg(e);
|
||||||
setPayErrMsg(e.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payDisabled =
|
||||||
|
payErrMsg ||
|
||||||
|
!foundAmount ||
|
||||||
|
payStatus.status === PreparePayResultType.InsufficientBalance;
|
||||||
|
|
||||||
|
const payHandler: ButtonHandler = {
|
||||||
|
onClick: payDisabled ? undefined : doPayment,
|
||||||
|
error: payErrMsg,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let totalFees = Amounts.getZero(amount.currency);
|
||||||
|
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
||||||
|
const amountEffective: AmountJson = Amounts.parseOrThrow(
|
||||||
|
payStatus.amountEffective,
|
||||||
|
);
|
||||||
|
totalFees = Amounts.sub(amountEffective, amount).amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payResult) {
|
||||||
|
return {
|
||||||
|
status: "ready",
|
||||||
|
hook: undefined,
|
||||||
|
uri: hook.response.uri,
|
||||||
|
amount,
|
||||||
|
totalFees,
|
||||||
|
balance: foundAmount,
|
||||||
|
payHandler,
|
||||||
|
payStatus: hook.response.payStatus,
|
||||||
|
payResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "confirmed",
|
||||||
|
hook: undefined,
|
||||||
|
uri: hook.response.uri,
|
||||||
|
amount,
|
||||||
|
totalFees,
|
||||||
|
balance: foundAmount,
|
||||||
|
payStatus: hook.response.payStatus,
|
||||||
|
payResult,
|
||||||
|
payHandler: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayPage({
|
||||||
|
talerPayUri,
|
||||||
|
goToWalletManualWithdraw,
|
||||||
|
goBack,
|
||||||
|
}: Props): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const state = useComponentState(talerPayUri, wxApi);
|
||||||
|
|
||||||
|
if (state.status === "loading") {
|
||||||
|
if (!state.hook) return <Loading />;
|
||||||
|
return (
|
||||||
|
<LoadingError
|
||||||
|
title={<i18n.Translate>Could not load pay status</i18n.Translate>}
|
||||||
|
error={state.hook}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<PaymentRequestView
|
<View
|
||||||
uri={talerPayUri!}
|
state={state}
|
||||||
payStatus={hook.response.payStatus}
|
goBack={goBack}
|
||||||
payResult={payResult}
|
|
||||||
onClick={onClick}
|
|
||||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||||
balance={foundAmount}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentRequestViewProps {
|
export function View({
|
||||||
payStatus: PreparePayResult;
|
state,
|
||||||
payResult?: ConfirmPayResult;
|
goBack,
|
||||||
onClick: () => void;
|
|
||||||
payErrMsg?: string;
|
|
||||||
uri: string;
|
|
||||||
goToWalletManualWithdraw: (s: string) => void;
|
|
||||||
balance: AmountJson | undefined;
|
|
||||||
}
|
|
||||||
export function PaymentRequestView({
|
|
||||||
uri,
|
|
||||||
payStatus,
|
|
||||||
payResult,
|
|
||||||
onClick,
|
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
balance,
|
}: {
|
||||||
}: PaymentRequestViewProps): VNode {
|
state: Ready | Confirmed;
|
||||||
|
goToWalletManualWithdraw: (currency?: string) => void;
|
||||||
|
goBack: () => void;
|
||||||
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
|
const contractTerms: ContractTerms = state.payStatus.contractTerms;
|
||||||
const contractTerms: ContractTerms = payStatus.contractTerms;
|
|
||||||
|
|
||||||
if (!contractTerms) {
|
if (!contractTerms) {
|
||||||
return (
|
return (
|
||||||
@ -203,124 +314,6 @@ export function PaymentRequestView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw);
|
|
||||||
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
|
||||||
const amountEffective: AmountJson = Amounts.parseOrThrow(
|
|
||||||
payStatus.amountEffective,
|
|
||||||
);
|
|
||||||
totalFees = Amounts.sub(amountEffective, amountRaw).amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Alternative(): VNode {
|
|
||||||
const [showQR, setShowQR] = useState<boolean>(false);
|
|
||||||
const privateUri =
|
|
||||||
payStatus.status !== PreparePayResultType.AlreadyConfirmed
|
|
||||||
? `${uri}&n=${payStatus.noncePriv}`
|
|
||||||
: uri;
|
|
||||||
if (!uri) return <Fragment />;
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
|
|
||||||
{!showQR ? (
|
|
||||||
<i18n.Translate>Pay with a mobile phone</i18n.Translate>
|
|
||||||
) : (
|
|
||||||
<i18n.Translate>Hide QR</i18n.Translate>
|
|
||||||
)}
|
|
||||||
</LinkSuccess>
|
|
||||||
{showQR && (
|
|
||||||
<div>
|
|
||||||
<QR text={privateUri} />
|
|
||||||
<i18n.Translate>
|
|
||||||
Scan the QR code or
|
|
||||||
<a href={privateUri}>
|
|
||||||
<i18n.Translate>click here</i18n.Translate>
|
|
||||||
</a>
|
|
||||||
</i18n.Translate>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonsSection(): VNode {
|
|
||||||
if (payResult) {
|
|
||||||
if (payResult.type === ConfirmPayResultType.Pending) {
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<i18n.Translate>Processing</i18n.Translate>...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Fragment />;
|
|
||||||
}
|
|
||||||
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section>
|
|
||||||
<ButtonSuccess upperCased onClick={onClick}>
|
|
||||||
<i18n.Translate>
|
|
||||||
Pay {<Amount value={payStatus.amountEffective} />}
|
|
||||||
</i18n.Translate>
|
|
||||||
</ButtonSuccess>
|
|
||||||
</section>
|
|
||||||
<Alternative />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section>
|
|
||||||
{balance ? (
|
|
||||||
<WarningBox>
|
|
||||||
<i18n.Translate>
|
|
||||||
Your balance of {<Amount value={balance} />} is not enough to
|
|
||||||
pay for this purchase
|
|
||||||
</i18n.Translate>
|
|
||||||
</WarningBox>
|
|
||||||
) : (
|
|
||||||
<WarningBox>
|
|
||||||
<i18n.Translate>
|
|
||||||
Your balance is not enough to pay for this purchase.
|
|
||||||
</i18n.Translate>
|
|
||||||
</WarningBox>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<ButtonSuccess
|
|
||||||
upperCased
|
|
||||||
onClick={() => goToWalletManualWithdraw(amountRaw.currency)}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Withdraw digital cash</i18n.Translate>
|
|
||||||
</ButtonSuccess>
|
|
||||||
</section>
|
|
||||||
<Alternative />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section>
|
|
||||||
{payStatus.paid && contractTerms.fulfillment_message && (
|
|
||||||
<Part
|
|
||||||
title={<i18n.Translate>Merchant message</i18n.Translate>}
|
|
||||||
text={contractTerms.fulfillment_message}
|
|
||||||
kind="neutral"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
{!payStatus.paid && <Alternative />}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletAction>
|
<WalletAction>
|
||||||
<LogoHeader />
|
<LogoHeader />
|
||||||
@ -328,70 +321,31 @@ export function PaymentRequestView({
|
|||||||
<SubTitle>
|
<SubTitle>
|
||||||
<i18n.Translate>Digital cash payment</i18n.Translate>
|
<i18n.Translate>Digital cash payment</i18n.Translate>
|
||||||
</SubTitle>
|
</SubTitle>
|
||||||
{payStatus.status === PreparePayResultType.AlreadyConfirmed &&
|
|
||||||
(payStatus.paid ? (
|
<ShowImportantMessage state={state} />
|
||||||
payStatus.contractTerms.fulfillment_url ? (
|
|
||||||
<SuccessBox>
|
|
||||||
<i18n.Translate>
|
|
||||||
Already paid, you are going to be redirected to{" "}
|
|
||||||
<a href={payStatus.contractTerms.fulfillment_url}>
|
|
||||||
{payStatus.contractTerms.fulfillment_url}
|
|
||||||
</a>
|
|
||||||
</i18n.Translate>
|
|
||||||
</SuccessBox>
|
|
||||||
) : (
|
|
||||||
<SuccessBox>
|
|
||||||
<i18n.Translate>Already paid</i18n.Translate>
|
|
||||||
</SuccessBox>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<WarningBox>
|
|
||||||
<i18n.Translate>Already claimed</i18n.Translate>
|
|
||||||
</WarningBox>
|
|
||||||
))}
|
|
||||||
{payResult && payResult.type === ConfirmPayResultType.Done && (
|
|
||||||
<SuccessBox>
|
|
||||||
<h3>
|
|
||||||
<i18n.Translate>Payment complete</i18n.Translate>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
{!payResult.contractTerms.fulfillment_message ? (
|
|
||||||
payResult.contractTerms.fulfillment_url ? (
|
|
||||||
<i18n.Translate>
|
|
||||||
You are going to be redirected to $
|
|
||||||
{payResult.contractTerms.fulfillment_url}
|
|
||||||
</i18n.Translate>
|
|
||||||
) : (
|
|
||||||
<i18n.Translate>You can close this page.</i18n.Translate>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
payResult.contractTerms.fulfillment_message
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</SuccessBox>
|
|
||||||
)}
|
|
||||||
<section>
|
<section>
|
||||||
{payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
{state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
|
||||||
Amounts.isNonZero(totalFees) && (
|
Amounts.isNonZero(state.totalFees) && (
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Total to pay</i18n.Translate>}
|
title={<i18n.Translate>Total to pay</i18n.Translate>}
|
||||||
text={<Amount value={payStatus.amountEffective} />}
|
text={<Amount value={state.payStatus.amountEffective} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Purchase amount</i18n.Translate>}
|
title={<i18n.Translate>Purchase amount</i18n.Translate>}
|
||||||
text={<Amount value={payStatus.amountRaw} />}
|
text={<Amount value={state.payStatus.amountRaw} />}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
{Amounts.isNonZero(totalFees) && (
|
{Amounts.isNonZero(state.totalFees) && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
text={<Amount value={totalFees} />}
|
text={<Amount value={state.totalFees} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -417,9 +371,12 @@ export function PaymentRequestView({
|
|||||||
<ProductList products={contractTerms.products} />
|
<ProductList products={contractTerms.products} />
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<ButtonsSection />
|
<ButtonsSection
|
||||||
|
state={state}
|
||||||
|
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||||
|
/>
|
||||||
<section>
|
<section>
|
||||||
<Link upperCased>
|
<Link upperCased onClick={goBack}>
|
||||||
<i18n.Translate>Cancel</i18n.Translate>
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
@ -495,3 +452,189 @@ function ProductList({ products }: { products: Product[] }): VNode {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ShowImportantMessage({ state }: { state: Ready | Confirmed }): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const { payStatus } = state;
|
||||||
|
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
||||||
|
if (payStatus.paid) {
|
||||||
|
if (payStatus.contractTerms.fulfillment_url) {
|
||||||
|
return (
|
||||||
|
<SuccessBox>
|
||||||
|
<i18n.Translate>
|
||||||
|
Already paid, you are going to be redirected to{" "}
|
||||||
|
<a href={payStatus.contractTerms.fulfillment_url}>
|
||||||
|
{payStatus.contractTerms.fulfillment_url}
|
||||||
|
</a>
|
||||||
|
</i18n.Translate>
|
||||||
|
</SuccessBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SuccessBox>
|
||||||
|
<i18n.Translate>Already paid</i18n.Translate>
|
||||||
|
</SuccessBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<WarningBox>
|
||||||
|
<i18n.Translate>Already claimed</i18n.Translate>
|
||||||
|
</WarningBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status == "confirmed") {
|
||||||
|
const { payResult, payHandler } = state;
|
||||||
|
if (payHandler.error) {
|
||||||
|
return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
|
||||||
|
}
|
||||||
|
if (payResult.type === ConfirmPayResultType.Done) {
|
||||||
|
return (
|
||||||
|
<SuccessBox>
|
||||||
|
<h3>
|
||||||
|
<i18n.Translate>Payment complete</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
{!payResult.contractTerms.fulfillment_message ? (
|
||||||
|
payResult.contractTerms.fulfillment_url ? (
|
||||||
|
<i18n.Translate>
|
||||||
|
You are going to be redirected to $
|
||||||
|
{payResult.contractTerms.fulfillment_url}
|
||||||
|
</i18n.Translate>
|
||||||
|
) : (
|
||||||
|
<i18n.Translate>You can close this page.</i18n.Translate>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
payResult.contractTerms.fulfillment_message
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</SuccessBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PayWithMobile({ state }: { state: Ready }): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const [showQR, setShowQR] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const privateUri =
|
||||||
|
state.payStatus.status !== PreparePayResultType.AlreadyConfirmed
|
||||||
|
? `${state.uri}&n=${state.payStatus.noncePriv}`
|
||||||
|
: state.uri;
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
|
||||||
|
{!showQR ? (
|
||||||
|
<i18n.Translate>Pay with a mobile phone</i18n.Translate>
|
||||||
|
) : (
|
||||||
|
<i18n.Translate>Hide QR</i18n.Translate>
|
||||||
|
)}
|
||||||
|
</LinkSuccess>
|
||||||
|
{showQR && (
|
||||||
|
<div>
|
||||||
|
<QR text={privateUri} />
|
||||||
|
<i18n.Translate>
|
||||||
|
Scan the QR code or
|
||||||
|
<a href={privateUri}>
|
||||||
|
<i18n.Translate>click here</i18n.Translate>
|
||||||
|
</a>
|
||||||
|
</i18n.Translate>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonsSection({
|
||||||
|
state,
|
||||||
|
goToWalletManualWithdraw,
|
||||||
|
}: {
|
||||||
|
state: Ready | Confirmed;
|
||||||
|
goToWalletManualWithdraw: (currency: string) => void;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
if (state.status === "ready") {
|
||||||
|
const { payStatus } = state;
|
||||||
|
if (payStatus.status === PreparePayResultType.PaymentPossible) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
<ButtonSuccess upperCased onClick={state.payHandler.onClick}>
|
||||||
|
<i18n.Translate>
|
||||||
|
Pay {<Amount value={payStatus.amountEffective} />}
|
||||||
|
</i18n.Translate>
|
||||||
|
</ButtonSuccess>
|
||||||
|
</section>
|
||||||
|
<PayWithMobile state={state} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
{state.balance ? (
|
||||||
|
<WarningBox>
|
||||||
|
<i18n.Translate>
|
||||||
|
Your balance of {<Amount value={state.balance} />} is not
|
||||||
|
enough to pay for this purchase
|
||||||
|
</i18n.Translate>
|
||||||
|
</WarningBox>
|
||||||
|
) : (
|
||||||
|
<WarningBox>
|
||||||
|
<i18n.Translate>
|
||||||
|
Your balance is not enough to pay for this purchase.
|
||||||
|
</i18n.Translate>
|
||||||
|
</WarningBox>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<ButtonSuccess
|
||||||
|
upperCased
|
||||||
|
onClick={() => goToWalletManualWithdraw(state.amount.currency)}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Withdraw digital cash</i18n.Translate>
|
||||||
|
</ButtonSuccess>
|
||||||
|
</section>
|
||||||
|
<PayWithMobile state={state} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
{payStatus.paid &&
|
||||||
|
state.payStatus.contractTerms.fulfillment_message && (
|
||||||
|
<Part
|
||||||
|
title={<i18n.Translate>Merchant message</i18n.Translate>}
|
||||||
|
text={state.payStatus.contractTerms.fulfillment_message}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
{!payStatus.paid && <PayWithMobile state={state} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "confirmed") {
|
||||||
|
if (state.payResult.type === ConfirmPayResultType.Pending) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>Processing</i18n.Translate>...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
@ -149,7 +149,7 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
||||||
|
|
||||||
expect(state.doWithdrawal.disabled).false
|
expect(state.doWithdrawal.onClick).not.undefined
|
||||||
expect(state.mustAcceptFirst).false
|
expect(state.mustAcceptFirst).false
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -213,7 +213,7 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
||||||
|
|
||||||
expect(state.doWithdrawal.disabled).true
|
expect(state.doWithdrawal.onClick).undefined
|
||||||
expect(state.mustAcceptFirst).true
|
expect(state.mustAcceptFirst).true
|
||||||
|
|
||||||
// accept TOS
|
// accept TOS
|
||||||
@ -238,7 +238,7 @@ describe("Withdraw CTA states", () => {
|
|||||||
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"))
|
||||||
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"))
|
||||||
|
|
||||||
expect(state.doWithdrawal.disabled).false
|
expect(state.doWithdrawal.onClick).not.undefined
|
||||||
expect(state.mustAcceptFirst).true
|
expect(state.mustAcceptFirst).true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ export function useComponentState(
|
|||||||
const uriHookDep =
|
const uriHookDep =
|
||||||
!uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response
|
!uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response
|
||||||
? undefined
|
? undefined
|
||||||
: uriInfoHook;
|
: uriInfoHook.response;
|
||||||
|
|
||||||
const { amount, thisExchange, thisCurrencyExchanges } = useMemo(() => {
|
const { amount, thisExchange, thisCurrencyExchanges } = useMemo(() => {
|
||||||
if (!uriHookDep)
|
if (!uriHookDep)
|
||||||
@ -129,7 +129,7 @@ export function useComponentState(
|
|||||||
thisCurrencyExchanges: [],
|
thisCurrencyExchanges: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const { uriInfo, knownExchanges } = uriHookDep.response;
|
const { uriInfo, knownExchanges } = uriHookDep;
|
||||||
|
|
||||||
const amount = uriInfo ? Amounts.parseOrThrow(uriInfo.amount) : undefined;
|
const amount = uriInfo ? Amounts.parseOrThrow(uriInfo.amount) : undefined;
|
||||||
const thisCurrencyExchanges =
|
const thisCurrencyExchanges =
|
||||||
@ -324,9 +324,11 @@ export function useComponentState(
|
|||||||
withdrawalFee,
|
withdrawalFee,
|
||||||
chosenAmount: amount,
|
chosenAmount: amount,
|
||||||
doWithdrawal: {
|
doWithdrawal: {
|
||||||
onClick: doWithdrawAndCheckError,
|
onClick:
|
||||||
|
doingWithdraw || (mustAcceptFirst && !reviewed)
|
||||||
|
? undefined
|
||||||
|
: doWithdrawAndCheckError,
|
||||||
error: withdrawError,
|
error: withdrawError,
|
||||||
disabled: doingWithdraw || (mustAcceptFirst && !reviewed),
|
|
||||||
},
|
},
|
||||||
tosProps: !termsState
|
tosProps: !termsState
|
||||||
? undefined
|
? undefined
|
||||||
@ -427,7 +429,7 @@ export function View({ state }: { state: Success }): VNode {
|
|||||||
(state.mustAcceptFirst && state.tosProps.reviewed)) && (
|
(state.mustAcceptFirst && state.tosProps.reviewed)) && (
|
||||||
<ButtonSuccess
|
<ButtonSuccess
|
||||||
upperCased
|
upperCased
|
||||||
disabled={state.doWithdrawal.disabled}
|
disabled={!state.doWithdrawal.onClick}
|
||||||
onClick={state.doWithdrawal.onClick}
|
onClick={state.doWithdrawal.onClick}
|
||||||
>
|
>
|
||||||
<i18n.Translate>Confirm withdrawal</i18n.Translate>
|
<i18n.Translate>Confirm withdrawal</i18n.Translate>
|
||||||
@ -436,7 +438,7 @@ export function View({ state }: { state: Success }): VNode {
|
|||||||
{state.tosProps.terms.status === "notfound" && (
|
{state.tosProps.terms.status === "notfound" && (
|
||||||
<ButtonWarning
|
<ButtonWarning
|
||||||
upperCased
|
upperCased
|
||||||
disabled={state.doWithdrawal.disabled}
|
disabled={!state.doWithdrawal.onClick}
|
||||||
onClick={state.doWithdrawal.onClick}
|
onClick={state.doWithdrawal.onClick}
|
||||||
>
|
>
|
||||||
<i18n.Translate>Withdraw anyway</i18n.Translate>
|
<i18n.Translate>Withdraw anyway</i18n.Translate>
|
||||||
|
@ -39,7 +39,12 @@ export interface HookOperationalError {
|
|||||||
details: TalerErrorDetail;
|
details: TalerErrorDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WithRetry {
|
||||||
|
retry: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export type HookResponse<T> = HookOk<T> | HookError | undefined;
|
export type HookResponse<T> = HookOk<T> | HookError | undefined;
|
||||||
|
export type HookResponseWithRetry<T> = ((HookOk<T> | HookError) & WithRetry) | undefined;
|
||||||
|
|
||||||
export function useAsyncAsHook<T>(
|
export function useAsyncAsHook<T>(
|
||||||
fn: () => Promise<T | false>,
|
fn: () => Promise<T | false>,
|
||||||
@ -84,3 +89,45 @@ export function useAsyncAsHook<T>(
|
|||||||
}, [args]);
|
}, [args]);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAsyncAsHook2<T>(
|
||||||
|
fn: () => Promise<T | false>,
|
||||||
|
deps?: any[],
|
||||||
|
): HookResponseWithRetry<T> {
|
||||||
|
|
||||||
|
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
|
||||||
|
|
||||||
|
const args = useMemo(() => ({
|
||||||
|
fn
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}), deps || [])
|
||||||
|
|
||||||
|
async function doAsync(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await args.fn();
|
||||||
|
if (response === false) return;
|
||||||
|
setHookResponse({ hasError: false, response });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TalerError) {
|
||||||
|
setHookResponse({
|
||||||
|
hasError: true,
|
||||||
|
operational: true,
|
||||||
|
details: e.errorDetail,
|
||||||
|
});
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
setHookResponse({
|
||||||
|
hasError: true,
|
||||||
|
operational: false,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
doAsync();
|
||||||
|
}, [args]);
|
||||||
|
|
||||||
|
if (!result) return undefined;
|
||||||
|
return { ...result, retry: doAsync };
|
||||||
|
}
|
||||||
|
@ -64,7 +64,6 @@ export function renderNodeOrBrowser(Component: any, args: any): void {
|
|||||||
|
|
||||||
interface Mounted<T> {
|
interface Mounted<T> {
|
||||||
unmount: () => void;
|
unmount: () => void;
|
||||||
getLastResult: () => T | null;
|
|
||||||
getLastResultOrThrow: () => T;
|
getLastResultOrThrow: () => T;
|
||||||
assertNoPendingUpdate: () => void;
|
assertNoPendingUpdate: () => void;
|
||||||
waitNextUpdate: (s?: string) => Promise<void>;
|
waitNextUpdate: (s?: string) => Promise<void>;
|
||||||
@ -76,15 +75,23 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
|
|||||||
// const result: { current: T | null } = {
|
// const result: { current: T | null } = {
|
||||||
// current: null
|
// current: null
|
||||||
// }
|
// }
|
||||||
let lastResult: T | null = null;
|
let lastResult: T | Error | null = null;
|
||||||
|
|
||||||
const listener: Array<() => void> = []
|
const listener: Array<() => void> = []
|
||||||
|
|
||||||
// component that's going to hold the hook
|
// component that's going to hold the hook
|
||||||
function Component(): VNode {
|
function Component(): VNode {
|
||||||
const hookResult = callback()
|
|
||||||
// save the hook result
|
try {
|
||||||
lastResult = hookResult
|
lastResult = callback()
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
lastResult = e
|
||||||
|
} else {
|
||||||
|
lastResult = new Error(`mounting the hook throw an exception: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// notify to everyone waiting for an update and clean the queue
|
// notify to everyone waiting for an update and clean the queue
|
||||||
listener.splice(0, listener.length).forEach(cb => cb())
|
listener.splice(0, listener.length).forEach(cb => cb())
|
||||||
return create(Fragment, {})
|
return create(Fragment, {})
|
||||||
@ -123,7 +130,7 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastResult(): T | null {
|
function getLastResult(): T | Error | null {
|
||||||
const copy = lastResult
|
const copy = lastResult
|
||||||
lastResult = null
|
lastResult = null
|
||||||
return copy;
|
return copy;
|
||||||
@ -131,6 +138,7 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
|
|||||||
|
|
||||||
function getLastResultOrThrow(): T {
|
function getLastResultOrThrow(): T {
|
||||||
const r = getLastResult()
|
const r = getLastResult()
|
||||||
|
if (r instanceof Error) throw r;
|
||||||
if (!r) throw Error('there was no last result')
|
if (!r) throw Error('there was no last result')
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@ -143,14 +151,18 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
|
|||||||
|
|
||||||
listener.push(() => {
|
listener.push(() => {
|
||||||
clearTimeout(tid)
|
clearTimeout(tid)
|
||||||
rej(Error(`Expecting no pending result but the hook get updated. Check the dependencies of the hooks.`))
|
rej(Error(`Expecting no pending result but the hook got updated.
|
||||||
|
If the update was not intended you need to check the hook dependencies
|
||||||
|
(or dependencies of the internal state) but otherwise make
|
||||||
|
sure to consume the result before ending the test.`))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const r = getLastResult()
|
const r = getLastResult()
|
||||||
if (r) throw Error('There are still pending results. This may happen because the hook did a new update but the test didn\'t get the result using getLastResult');
|
if (r) throw Error(`There are still pending results.
|
||||||
|
This may happen because the hook did a new update but the test didn't consume the result using getLastResult`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
unmount, getLastResult, getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate
|
unmount, getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,8 +62,7 @@ export interface TextFieldHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonHandler {
|
export interface ButtonHandler {
|
||||||
onClick: () => Promise<void>;
|
onClick?: () => Promise<void>;
|
||||||
disabled?: boolean;
|
|
||||||
error?: TalerError;
|
error?: TalerError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user