This commit is contained in:
Sebastian 2022-09-10 23:21:44 -03:00
parent dda90b51f6
commit e4f3acfeb2
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
9 changed files with 238 additions and 112 deletions

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AbsoluteTime, AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
import { AbsoluteTime, AmountJson, PreparePayResult, TalerErrorDetail } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
@ -26,11 +26,14 @@ import { LoadingUriView, ReadyView } from "./views.js";
export interface Props {
talerPayPullUri: string;
onClose: () => Promise<void>;
goToWalletManualWithdraw: (amount?: string) => Promise<void>;
}
export type State =
| State.Loading
| State.LoadingUriError
| State.NoEnoughBalance
| State.NoBalanceForCurrency
| State.Ready;
export namespace State {
@ -47,22 +50,38 @@ export namespace State {
export interface BaseInfo {
error: undefined;
uri: string;
cancel: ButtonHandler;
}
export interface Ready extends BaseInfo {
status: "ready";
amount: AmountJson,
goToWalletManualWithdraw: (currency: string) => Promise<void>;
summary: string | undefined,
expiration: AbsoluteTime | undefined,
error: undefined;
accept: ButtonHandler;
operationError?: TalerErrorDetail;
payStatus: PreparePayResult;
}
export interface NoBalanceForCurrency extends BaseInfo {
status: "no-balance-for-currency"
balance: undefined;
}
export interface NoEnoughBalance extends BaseInfo {
status: "no-enough-balance"
balance: AmountJson;
}
export interface Ready extends BaseInfo {
status: "ready";
error: undefined;
balance: AmountJson;
accept: ButtonHandler;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
"loading-uri": LoadingUriView,
"no-balance-for-currency": ReadyView,
"no-enough-balance": ReadyView,
"ready": ReadyView,
};

View File

@ -14,22 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { AbsoluteTime, Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { AbsoluteTime, Amounts, NotificationType, PreparePayResult, PreparePayResultType, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState(
{ talerPayPullUri, onClose }: Props,
{ talerPayPullUri, onClose, goToWalletManualWithdraw }: Props,
api: typeof wxApi,
): State {
const hook = useAsyncAsHook(async () => {
return await api.checkPeerPullPayment({
const p2p = await api.checkPeerPullPayment({
talerUri: talerPayPullUri
})
}, [])
const balance = await api.getBalance();
return { p2p, balance }
})
useEffect(() => {
api.onUpdateNotification([NotificationType.CoinWithdrawn], () => {
hook?.retry();
});
});
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (!hook) {
@ -45,12 +54,84 @@ export function useComponentState(
};
}
const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response
// const { payStatus } = hook.response.p2p;
const amount: string = contractTerms?.amount
const { amount: purseAmount, contractTerms, peerPullPaymentIncomingId } = hook.response.p2p
const amountStr: string = contractTerms?.amount
const amount = Amounts.parseOrThrow(amountStr)
const summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration
const foundBalance = hook.response.balance.balances.find(
(b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
);
const paymentPossible: PreparePayResult = {
status: PreparePayResultType.PaymentPossible,
proposalId: "fakeID",
contractTerms: {
} as any,
contractTermsHash: "asd",
amountRaw: hook.response.p2p.amount,
amountEffective: hook.response.p2p.amount,
noncePriv: "",
} as PreparePayResult
const insufficientBalance: PreparePayResult = {
status: PreparePayResultType.InsufficientBalance,
proposalId: "fakeID",
contractTerms: {
} as any,
amountRaw: hook.response.p2p.amount,
noncePriv: "",
}
const baseResult = {
uri: talerPayPullUri,
cancel: {
onClick: onClose
},
amount,
goToWalletManualWithdraw,
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
operationError,
}
if (!foundBalance) {
return {
status: "no-balance-for-currency",
error: undefined,
balance: undefined,
...baseResult,
payStatus: insufficientBalance,
}
}
const foundAmount = Amounts.parseOrThrow(foundBalance.available);
//FIXME: should use pay result type since it check for coins exceptions
if (Amounts.cmp(foundAmount, amount) < 0) { //payStatus.status === PreparePayResultType.InsufficientBalance) {
return {
status: 'no-enough-balance',
error: undefined,
balance: foundAmount,
...baseResult,
payStatus: insufficientBalance,
}
}
// if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
// return {
// status: "confirmed",
// balance: foundAmount,
// ...baseResult,
// };
// }
async function accept(): Promise<void> {
try {
const resp = await api.acceptPeerPullPayment({
@ -69,16 +150,12 @@ export function useComponentState(
return {
status: "ready",
amount: Amounts.parseOrThrow(amount),
error: undefined,
...baseResult,
payStatus: paymentPossible,
balance: foundAmount,
accept: {
onClick: accept
},
summary,
expiration: expiration ? AbsoluteTime.fromTimestamp(expiration) : undefined,
cancel: {
onClick: onClose
},
operationError
}
}

View File

@ -19,6 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js";
@ -32,6 +33,10 @@ export const Ready = createExample(ReadyView, {
value: 1,
fraction: 0,
},
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: "ARS:1",
} as PreparePayResult,
accept: {},
cancel: {},
});

View File

@ -14,16 +14,23 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { h, VNode } from "preact";
import { Amounts, PreparePayResultType } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js";
import { LoadingError } from "../../components/LoadingError.js";
import { LogoHeader } from "../../components/LogoHeader.js";
import { Part } from "../../components/Part.js";
import { Link, SubTitle, WalletAction } from "../../components/styled/index.js";
import {
Link,
SubTitle,
WalletAction,
WarningBox,
} from "../../components/styled/index.js";
import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { ButtonsSection, PayWithMobile } from "../Payment/views.js";
import { State } from "./index.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@ -37,16 +44,21 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
export function ReadyView({
export function ReadyView(
state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance,
): VNode {
const { i18n } = useTranslationContext();
const {
operationError,
cancel,
accept,
expiration,
summary,
amount,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
expiration,
uri,
status,
balance,
payStatus,
cancel,
} = state;
return (
<WalletAction>
<LogoHeader />
@ -78,13 +90,14 @@ export function ReadyView({
kind="neutral"
/>
</section>
<section>
<Button variant="contained" color="success" onClick={accept.onClick}>
<i18n.Translate>
Pay &nbsp; {<Amount value={amount} />}
</i18n.Translate>
</Button>
</section>
<ButtonsSection
amount={amount}
balance={balance}
payStatus={payStatus}
uri={uri}
payHandler={status === "ready" ? state.accept : undefined}
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
/>
<section>
<Link upperCased onClick={cancel.onClick}>
<i18n.Translate>Cancel</i18n.Translate>

View File

@ -15,6 +15,7 @@
*/
import { AmountJson, ConfirmPayResult, PreparePayResult, PreparePayResultAlreadyConfirmed, PreparePayResultInsufficientBalance, PreparePayResultPaymentPossible } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js";
@ -85,7 +86,7 @@ export namespace State {
status: "completed";
payStatus: PreparePayResult;
payResult: ConfirmPayResult;
payHandler: ButtonHandler;
paymentError?: TalerError;
balance: AmountJson;
}
}

View File

@ -101,9 +101,7 @@ export function useComponentState(
status: "completed",
balance: foundAmount,
payStatus,
payHandler: {
error: payErrMsg,
},
paymentError: payErrMsg,
payResult,
...baseResult,
};

View File

@ -16,9 +16,12 @@
import {
AbsoluteTime,
AmountJson,
Amounts,
ConfirmPayResultType,
ContractTerms,
PreparePayResult,
PreparePayResultPaymentPossible,
PreparePayResultType,
Product,
} from "@gnu-taler/taler-util";
@ -42,6 +45,7 @@ import {
import { Time } from "../../components/Time.js";
import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js";
import { ButtonHandler } from "../../mui/handlers.js";
import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
@ -164,7 +168,11 @@ export function BaseView(state: SupportedStates): VNode {
)}
</section>
<ButtonsSection
state={state}
amount={state.amount}
balance={state.balance}
payStatus={state.payStatus}
uri={state.uri}
payHandler={state.status === "ready" ? state.payHandler : undefined}
goToWalletManualWithdraw={state.goToWalletManualWithdraw}
/>
<section>
@ -276,9 +284,9 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
}
if (state.status == "completed") {
const { payResult, payHandler } = state;
if (payHandler.error) {
return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
const { payResult, paymentError } = state;
if (paymentError) {
return <ErrorTalerOperation error={paymentError.errorDetail} />;
}
if (payResult.type === ConfirmPayResultType.Done) {
return (
@ -307,15 +315,11 @@ function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
return <Fragment />;
}
function PayWithMobile({ state }: { state: SupportedStates }): VNode {
export function PayWithMobile({ uri }: { uri: string }): 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)}>
@ -327,10 +331,10 @@ function PayWithMobile({ state }: { state: SupportedStates }): VNode {
</LinkSuccess>
{showQR && (
<div>
<QR text={privateUri} />
<QR text={uri} />
<i18n.Translate>
Scan the QR code or &nbsp;
<a href={privateUri}>
<a href={uri}>
<i18n.Translate>click here</i18n.Translate>
</a>
</i18n.Translate>
@ -340,50 +344,60 @@ function PayWithMobile({ state }: { state: SupportedStates }): VNode {
);
}
function ButtonsSection({
state,
goToWalletManualWithdraw,
}: {
state: SupportedStates;
interface ButtonSectionProps {
payStatus: PreparePayResult;
payHandler: ButtonHandler | undefined;
balance: AmountJson | undefined;
uri: string;
amount: AmountJson;
goToWalletManualWithdraw: (currency: string) => Promise<void>;
}): VNode {
}
export function ButtonsSection({
payStatus,
uri,
payHandler,
balance,
amount,
goToWalletManualWithdraw,
}: ButtonSectionProps): VNode {
const { i18n } = useTranslationContext();
if (state.status === "ready") {
if (payStatus.status === PreparePayResultType.PaymentPossible) {
const privateUri = `${uri}&n=${payStatus.noncePriv}`;
return (
<Fragment>
<section>
<Button
variant="contained"
color="success"
onClick={state.payHandler.onClick}
onClick={payHandler?.onClick}
>
<i18n.Translate>
Pay &nbsp;
{<Amount value={state.payStatus.amountEffective} />}
{<Amount value={amount} />}
</i18n.Translate>
</Button>
</section>
<PayWithMobile state={state} />
<PayWithMobile uri={privateUri} />
</Fragment>
);
}
if (
state.status === "no-enough-balance" ||
state.status === "no-balance-for-currency"
) {
// if (state.payStatus.status === PreparePayResultType.InsufficientBalance) {
if (payStatus.status === PreparePayResultType.InsufficientBalance) {
let BalanceMessage = "";
if (!state.balance) {
if (!balance) {
BalanceMessage = i18n.str`You have no balance for this currency. Withdraw digital cash first.`;
} else {
const balanceShouldBeEnough =
Amounts.cmp(state.balance, state.amount) !== -1;
const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
if (balanceShouldBeEnough) {
BalanceMessage = i18n.str`Could not find enough coins to pay this order. Even if you have enough ${state.balance.currency} some restriction may apply.`;
BalanceMessage = i18n.str`Could not find enough coins to pay. Even if you have enough ${balance.currency} some restriction may apply.`;
} else {
BalanceMessage = i18n.str`Your current balance is not enough for this order.`;
BalanceMessage = i18n.str`Your current balance is not enough.`;
}
}
const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
return (
<Fragment>
<section>
@ -393,51 +407,45 @@ function ButtonsSection({
<Button
variant="contained"
color="success"
onClick={() =>
goToWalletManualWithdraw(Amounts.stringify(state.amount))
}
onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
>
<i18n.Translate>Get digital cash</i18n.Translate>
</Button>
</section>
<PayWithMobile state={state} />
<PayWithMobile uri={uriPrivate} />
</Fragment>
);
// }
}
if (state.status === "confirmed") {
if (state.payStatus.status === PreparePayResultType.AlreadyConfirmed) {
if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
return (
<Fragment>
<section>
{state.payStatus.paid &&
state.payStatus.contractTerms.fulfillment_message && (
{payStatus.paid && payStatus.contractTerms.fulfillment_message && (
<Part
title={<i18n.Translate>Merchant message</i18n.Translate>}
text={state.payStatus.contractTerms.fulfillment_message}
text={payStatus.contractTerms.fulfillment_message}
kind="neutral"
/>
)}
</section>
{!state.payStatus.paid && <PayWithMobile state={state} />}
{!payStatus.paid && <PayWithMobile uri={uri} />}
</Fragment>
);
}
}
if (state.status === "completed") {
if (state.payResult.type === ConfirmPayResultType.Pending) {
return (
<section>
<div>
<p>
<i18n.Translate>Processing</i18n.Translate>...
</p>
</div>
</section>
);
}
}
// if (state.status === "completed") {
// if (state.payResult.type === ConfirmPayResultType.Pending) {
// return (
// <section>
// <div>
// <p>
// <i18n.Translate>Processing</i18n.Translate>...
// </p>
// </div>
// </section>
// );
// }
// }
return <Fragment />;
}

View File

@ -20,11 +20,13 @@ import { h, VNode } from "preact";
import { expect } from "chai";
describe("useTalerActionURL hook", () => {
it("should be set url to undefined when dismiss", async () => {
const ctx = ({ children }: { children: any }): VNode => {
return h(IoCProviderForTesting, {
value: {
findTalerUriInActiveTab: async () => "asd",
findTalerUriInClipboard: async () => "qwe",
},
children,
});
@ -42,7 +44,10 @@ describe("useTalerActionURL hook", () => {
{
const [url, setDismissed] = getLastResultOrThrow();
expect(url).equals("asd");
expect(url).deep.equals({
location: "clipboard",
uri: "qwe"
});
setDismissed(true);
}
@ -53,7 +58,6 @@ describe("useTalerActionURL hook", () => {
if (url !== undefined) throw Error("invalid");
expect(url).undefined;
}
await assertNoPendingUpdate();
});
});

View File

@ -52,7 +52,8 @@ export function useTalerActionURL(): [
}
}
check();
});
}, [setTalerActionUrl]);
const url = dismissed ? undefined : talerActionUrl;
return [url, setDismissed];
}