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/> 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 { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler } from "../../mui/handlers.js"; import { ButtonHandler } from "../../mui/handlers.js";
@ -26,11 +26,14 @@ import { LoadingUriView, ReadyView } from "./views.js";
export interface Props { export interface Props {
talerPayPullUri: string; talerPayPullUri: string;
onClose: () => Promise<void>; onClose: () => Promise<void>;
goToWalletManualWithdraw: (amount?: string) => Promise<void>;
} }
export type State = export type State =
| State.Loading | State.Loading
| State.LoadingUriError | State.LoadingUriError
| State.NoEnoughBalance
| State.NoBalanceForCurrency
| State.Ready; | State.Ready;
export namespace State { export namespace State {
@ -47,22 +50,38 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
error: undefined; error: undefined;
uri: string;
cancel: ButtonHandler; cancel: ButtonHandler;
}
export interface Ready extends BaseInfo {
status: "ready";
amount: AmountJson, amount: AmountJson,
goToWalletManualWithdraw: (currency: string) => Promise<void>;
summary: string | undefined, summary: string | undefined,
expiration: AbsoluteTime | undefined, expiration: AbsoluteTime | undefined,
error: undefined;
accept: ButtonHandler;
operationError?: TalerErrorDetail; 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> = { const viewMapping: StateViewMap<State> = {
loading: Loading, loading: Loading,
"loading-uri": LoadingUriView, "loading-uri": LoadingUriView,
"no-balance-for-currency": ReadyView,
"no-enough-balance": ReadyView,
"ready": ReadyView, "ready": ReadyView,
}; };

View File

@ -14,22 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { 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 { 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 { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import * as wxApi from "../../wxApi.js"; import * as wxApi from "../../wxApi.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState( export function useComponentState(
{ talerPayPullUri, onClose }: Props, { talerPayPullUri, onClose, goToWalletManualWithdraw }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const hook = useAsyncAsHook(async () => { const hook = useAsyncAsHook(async () => {
return await api.checkPeerPullPayment({ const p2p = await api.checkPeerPullPayment({
talerUri: talerPayPullUri talerUri: talerPayPullUri
}) })
}, []) const balance = await api.getBalance();
return { p2p, balance }
})
useEffect(() => {
api.onUpdateNotification([NotificationType.CoinWithdrawn], () => {
hook?.retry();
});
});
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)
if (!hook) { 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 summary: string | undefined = contractTerms?.summary
const expiration: TalerProtocolTimestamp | undefined = contractTerms?.purse_expiration 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> { async function accept(): Promise<void> {
try { try {
const resp = await api.acceptPeerPullPayment({ const resp = await api.acceptPeerPullPayment({
@ -69,16 +150,12 @@ export function useComponentState(
return { return {
status: "ready", status: "ready",
amount: Amounts.parseOrThrow(amount),
error: undefined, error: undefined,
...baseResult,
payStatus: paymentPossible,
balance: foundAmount,
accept: { accept: {
onClick: 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) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
import { createExample } from "../../test-utils.js"; import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
@ -32,6 +33,10 @@ export const Ready = createExample(ReadyView, {
value: 1, value: 1,
fraction: 0, fraction: 0,
}, },
payStatus: {
status: PreparePayResultType.PaymentPossible,
amountEffective: "ARS:1",
} as PreparePayResult,
accept: {}, accept: {},
cancel: {}, cancel: {},
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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