taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit f64ee751315a52fa43d7977b7b49da31a5ccd966
parent 516cb7d48f6fc5b077dfb43e2a381b505fc805e7
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue,  5 Aug 2025 09:20:28 +0200

fix #10227: contract v1 choices for payments

Diffstat:
Mpackages/taler-wallet-webextension/set-up-dev-wallet.sh | 1+
Mpackages/taler-wallet-webextension/src/components/PaymentButtons.tsx | 270++++++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/taler-wallet-webextension/src/cta/InvoicePay/index.ts | 40++++------------------------------------
Mpackages/taler-wallet-webextension/src/cta/InvoicePay/state.ts | 84++++++++++++++++++++++---------------------------------------------------------
Mpackages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx | 21++++++++++-----------
Mpackages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx | 45++++++++++++++++++++++-----------------------
Mpackages/taler-wallet-webextension/src/cta/Payment/index.ts | 58++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-webextension/src/cta/Payment/state.ts | 160++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-wallet-webextension/src/cta/Payment/stories.tsx | 834++++++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/taler-wallet-webextension/src/cta/Payment/test.ts | 18+++++++++---------
Mpackages/taler-wallet-webextension/src/cta/Payment/views.tsx | 144++++++++++++++++++++++++++++++++++++-------------------------------------------
11 files changed, 777 insertions(+), 898 deletions(-)

diff --git a/packages/taler-wallet-webextension/set-up-dev-wallet.sh b/packages/taler-wallet-webextension/set-up-dev-wallet.sh @@ -42,6 +42,7 @@ cp -r service_worker.js $TEMP_DIR (cd $TEMP_DIR && zip -q -r "../$zipfile" dist static manifest.json service_worker.js) +echo wallet webextension can be installed from extension-dev/v3 and extension-dev/v2 echo now run the development server echo 'INSTALL_DIR=$PWD/extension-dev/v3/dist ./dev.mjs #FOR v3 version' echo 'INSTALL_DIR=$PWD/extension-dev/v2/dist ./dev.mjs #FOR v2 version' diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx @@ -15,175 +15,149 @@ */ import { - AmountJson, Amounts, InsufficientBalanceHint, - assertUnreachable, - PreparePayResult, - PreparePayResultType, TranslatedString, - parsePayUri, + assertUnreachable, + parsePayUri } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; +import { PaymentStates } from "../cta/Payment/views.js"; import { Button } from "../mui/Button.js"; -import { ButtonHandler } from "../mui/handlers.js"; import { Amount } from "./Amount.js"; import { Part } from "./Part.js"; import { QR } from "./QR.js"; import { LinkSuccess, WarningBox } from "./styled/index.js"; -interface Props { - payStatus: PreparePayResult; - payHandler: ButtonHandler | undefined; - uri: string; - amount: AmountJson; - goToWalletManualWithdraw: (currency: string) => Promise<void>; -} - -export function PaymentButtons({ - payStatus, - uri, - payHandler, - amount, - goToWalletManualWithdraw, -}: Props): VNode { +export function PaymentButtons({ paymentState: state }: { paymentState: PaymentStates }): VNode { const { i18n } = useTranslationContext(); - if (payStatus.status === PreparePayResultType.PaymentPossible) { - return ( - <Fragment> - <section> - <Button - variant="contained" - color="success" - onClick={payHandler?.onClick} - > - <i18n.Translate> - Pay &nbsp; - {<Amount value={amount} />} - </i18n.Translate> - </Button> - </section> - <PayWithMobile uri={uri} /> - </Fragment> - ); - } - if (payStatus.status === PreparePayResultType.InsufficientBalance) { + switch (state.status) { + case "ready": { + return ( + <Fragment> + <section> + <Button + variant="contained" + color="success" + onClick={state.payHandler?.onClick} + > + <i18n.Translate> + Pay &nbsp; + {<Amount value={state.amount} />} + </i18n.Translate> + </Button> + </section> + <PayWithMobile uri={state.shareUri} /> + </Fragment> + ); + } - let BalanceMessage = ""; - switch (payStatus.balanceDetails.causeHint) { - case InsufficientBalanceHint.MerchantAcceptInsufficient: { - BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( - payStatus.balanceDetails.balanceReceiverAcceptable, - )} ${ - amount.currency - }. To know more you can check which exchange and auditors the merchant trust.`; - break; - } - case InsufficientBalanceHint.MerchantDepositInsufficient: { - BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( - payStatus.balanceDetails.balanceReceiverDepositable, - )} ${ - amount.currency - }. To know more you can check which wire methods the merchant accepts.`; - break; - } - case InsufficientBalanceHint.AgeRestricted: { - BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( - payStatus.balanceDetails.balanceAgeAcceptable, - )} ${amount.currency} to pay for this contract which is restricted.`; - break; - } - case InsufficientBalanceHint.WalletBalanceMaterialInsufficient: { - BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( - payStatus.balanceDetails.balanceMaterial, - )} ${ - amount.currency - } to spend right know. There are some coins that need to be refreshed.`; - break; - } - case InsufficientBalanceHint.WalletBalanceAvailableInsufficient: { - BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( - payStatus.balanceDetails.balanceAvailable, - )} ${amount.currency} available.`; - break; - } - case InsufficientBalanceHint.ExchangeMissingGlobalFees: { - BalanceMessage = i18n.str`Provider is missing the global fee configuration, this likely means it is misconfigured.`; - break; - } - case InsufficientBalanceHint.FeesNotCovered: { - BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue( - Amounts.stringify( - Amounts.sub( - amount, - payStatus.balanceDetails.maxEffectiveSpendAmount, - ).amount, - ), - )} ${ - amount.currency - } more balance in your wallet or ask your merchant to cover more of the fees.`; - break; - } - case undefined: { - // FIXME: explain why the balance is not enough by checking the hint in all exchanges - BalanceMessage = i18n.str`Balance is not enough because.`; - break; - } - default: - assertUnreachable(payStatus.balanceDetails.causeHint); + case "confirmed": { + return ( + <Fragment> + <section> + {state.message && ( + <Part + title={i18n.str`Merchant message`} + text={state.message as TranslatedString} + kind="neutral" + /> + )} + </section> + </Fragment> + ); } - return ( - <Fragment> - <section> - <WarningBox>{BalanceMessage}</WarningBox> - </section> - <section> - <Button - variant="contained" - color="success" - onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))} - > - <i18n.Translate>Get digital cash</i18n.Translate> - </Button> - </section> - <PayWithMobile uri={uri} /> - </Fragment> - ); - } - if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { - return ( - <Fragment> - <section> - {payStatus.paid && payStatus.contractTerms.fulfillment_message && ( - <Part - title={i18n.str`Merchant message`} - text={ - payStatus.contractTerms.fulfillment_message as TranslatedString - } - kind="neutral" - /> - )} - </section> - </Fragment> - ); - } - if (payStatus.status === PreparePayResultType.ChoiceSelection) { - return ( - <Fragment> - <section> - Unsupported payment result {payStatus.status} - </section> - </Fragment> - ); - } + case "no-enough-balance": { + let BalanceMessage = ""; + const { balanceDetails, amount } = state + if (!balanceDetails) { + BalanceMessage = i18n.str`Balance is not enough.`; + } else switch (balanceDetails.causeHint) { + case InsufficientBalanceHint.MerchantAcceptInsufficient: { + BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( + balanceDetails.balanceReceiverAcceptable, + )} ${amount.currency + }. To know more you can check which exchange and auditors the merchant trust.`; + break; + } + case InsufficientBalanceHint.MerchantDepositInsufficient: { + BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue( + balanceDetails.balanceReceiverDepositable, + )} ${amount.currency + }. To know more you can check which wire methods the merchant accepts.`; + break; + } + case InsufficientBalanceHint.AgeRestricted: { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + balanceDetails.balanceAgeAcceptable, + )} ${amount.currency} to pay for this contract which is restricted.`; + break; + } + case InsufficientBalanceHint.WalletBalanceMaterialInsufficient: { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + balanceDetails.balanceMaterial, + )} ${amount.currency + } to spend right know. There are some coins that need to be refreshed.`; + break; + } + case InsufficientBalanceHint.WalletBalanceAvailableInsufficient: { + BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue( + balanceDetails.balanceAvailable, + )} ${amount.currency} available.`; + break; + } + case InsufficientBalanceHint.ExchangeMissingGlobalFees: { + BalanceMessage = i18n.str`Provider is missing the global fee configuration, this likely means it is misconfigured.`; + break; + } + case InsufficientBalanceHint.FeesNotCovered: { + BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue( + Amounts.stringify( + Amounts.sub( + amount, + balanceDetails.maxEffectiveSpendAmount, + ).amount, + ), + )} ${amount.currency + } more balance in your wallet or ask your merchant to cover more of the fees.`; + break; + } + case undefined: { + BalanceMessage = i18n.str`Balance is not enough.`; + break; + } + default: + assertUnreachable(balanceDetails.causeHint); + } - assertUnreachable(payStatus); + return ( + <Fragment> + <section> + <WarningBox>{BalanceMessage}</WarningBox> + </section> + <section> + <Button + variant="contained" + color="success" + onClick={() => state.goToWalletManualWithdraw(Amounts.stringify(amount))} + > + <i18n.Translate>Get digital cash</i18n.Translate> + </Button> + </section> + <PayWithMobile uri={state.shareUri} /> + </Fragment> + ); + } + default: { + assertUnreachable(state); + } + } } function PayWithMobile({ uri }: { uri: string }): VNode { diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts @@ -14,11 +14,6 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - AbsoluteTime, - AmountJson, - PreparePayResult -} from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -26,6 +21,7 @@ import { ButtonHandler } from "../../mui/handlers.js"; import { StateViewMap, compose } from "../../utils/index.js"; import { useComponentState } from "./state.js"; import { ReadyView } from "./views.js"; +import { PaymentStates } from "../Payment/views.js"; export interface Props { talerPayPullUri: string; @@ -37,9 +33,7 @@ export interface Props { export type State = | State.Loading | State.LoadingUriError - | State.NoEnoughBalance - | State.NoBalanceForCurrency - | State.Ready; + | PaymentStates; export namespace State { export interface Loading { @@ -53,39 +47,13 @@ export namespace State { error: ErrorAlert; } - export interface BaseInfo { - error: undefined; - uri: string; - cancel: ButtonHandler; - effective: AmountJson; - raw: AmountJson; - goToWalletManualWithdraw: (currency: string) => Promise<void>; - summary: string | undefined; - expiration: AbsoluteTime | undefined; - 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, error: ErrorAlertView, - "no-balance-for-currency": ReadyView, + + "confirmed": ReadyView, "no-enough-balance": ReadyView, ready: ReadyView, }; diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts @@ -76,12 +76,6 @@ export function useComponentState({ ), }; } - // if (hook.hasError) { - // return { - // status: "loading-uri", - // error: hook, - // }; - // } const { contractTerms, transactionId, amountEffective, amountRaw } = hook.response.p2p; @@ -89,70 +83,37 @@ export function useComponentState({ const amountStr: string = contractTerms.amount; const amount = Amounts.parseOrThrow(amountStr); const effective = Amounts.parseOrThrow(amountEffective); - const raw = Amounts.parseOrThrow(amountRaw); - const summary: string | undefined = contractTerms.summary; - const expiration: TalerProtocolTimestamp | undefined = - contractTerms.purse_expiration; + // const raw = Amounts.parseOrThrow(amountRaw); + // const summary: string | undefined = contractTerms.summary; + + const expiration: AbsoluteTime = + contractTerms.purse_expiration ? AbsoluteTime.fromProtocolTimestamp(contractTerms.purse_expiration) : AbsoluteTime.never(); const foundBalance = hook.response.balance.balances.find( (b) => Amounts.parseOrThrow(b.available).currency === amount.currency, ); - const paymentPossible: PreparePayResult = { - status: PreparePayResultType.PaymentPossible, - contractTerms: {} as any, - contractTermsHash: "asd", - amountRaw: hook.response.p2p.amount, - amountEffective: hook.response.p2p.amount, - scopes: [], - transactionId: "txn:pay:123" as TransactionIdStr, - talerUri: "taler://pay/example.com/", - } as PreparePayResult; - - const insufficientBalance: PreparePayResult = { - status: PreparePayResultType.InsufficientBalance, - talerUri: "taler://pay", - proposalId: "fakeID", - contractTerms: {} as any, - amountRaw: hook.response.p2p.amount, - noncePriv: "", - } as any; //FIXME: check this interface with new values - const baseResult = { - uri: talerPayPullUri, - cancel: { - onClick: pushAlertOnError(onClose), - }, - effective, - raw, + shareUri: talerPayPullUri, + transactionId, + contractTerms, + summary: contractTerms.summary, + error: undefined, + expiration, + cancel: pushAlertOnError(onClose), goToWalletManualWithdraw, - summary, - expiration: expiration - ? AbsoluteTime.fromProtocolTimestamp(expiration) - : undefined, + minimum_age: undefined, }; 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, + effective: undefined, + balanceDetails: undefined, + amount, + merchant: undefined, + choices: undefined, }; } @@ -168,12 +129,13 @@ export function useComponentState({ return { status: "ready", - error: undefined, ...baseResult, - payStatus: paymentPossible, - balance: foundAmount, - accept: { + payHandler: { onClick: pushAlertOnError(accept), }, + effective, + amount, + merchant: undefined, + choices: undefined, }; } diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx @@ -20,9 +20,7 @@ */ import { - AbsoluteTime, - PreparePayResult, - PreparePayResultType, + AbsoluteTime } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { ReadyView } from "./views.js"; @@ -37,20 +35,21 @@ export const Ready = tests.createExample(ReadyView, { value: 1, fraction: 0, }, - raw: { + amount: { currency: "ARS", value: 1, fraction: 0, }, summary: "some subject", - uri: "taler://pay/merchant.ar/123", - payStatus: { - status: PreparePayResultType.PaymentPossible, - amountEffective: "ARS:1", - } as PreparePayResult, + // uri: "taler://pay/merchant.ar/123", + // payStatus: { + // status: PreparePayResultType.PaymentPossible, + // amountEffective: "ARS:1", + // } as PreparePayResult, expiration: AbsoluteTime.fromMilliseconds( new Date().getTime() + 1000 * 60 * 60, ), - accept: {}, - cancel: {}, + + // accept: {}, + // cancel: {}, }); diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { Part } from "../../components/Part.js"; @@ -23,19 +24,19 @@ import { getAmountWithFee, InvoicePaymentDetails, } from "../../wallet/Transaction.js"; -import { State } from "./index.js"; -import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; +import { PaymentStates } from "../Payment/views.js"; + +const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), +); export function ReadyView( - state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance, + state: PaymentStates, ): VNode { const { i18n } = useTranslationContext(); - const { summary, effective, raw, expiration, uri, status, payStatus } = state; + const { summary, effective, amount, expiration } = state; - const inFiveMinutes = AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromSpec({ minutes: 5 }), - ); const willExpireSoon = expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; @@ -43,14 +44,16 @@ export function ReadyView( <Fragment> <section style={{ textAlign: "left" }}> <Part title={i18n.str`Subject`} text={<div>{summary}</div>} /> - <Part - title={i18n.str`Details`} - text={ - <InvoicePaymentDetails - amount={getAmountWithFee(effective, raw, "debit")} - /> - } - /> + {!effective ? undefined : + <Part + title={i18n.str`Details`} + text={ + <InvoicePaymentDetails + amount={getAmountWithFee(effective, amount, "debit")} + /> + } + /> + } {willExpireSoon && ( <Part title={i18n.str`Expires at`} @@ -59,13 +62,9 @@ export function ReadyView( /> )} </section> - <PaymentButtons - amount={effective} - payStatus={payStatus} - uri={uri} - payHandler={status === "ready" ? state.accept : undefined} - goToWalletManualWithdraw={state.goToWalletManualWithdraw} - /> + + <PaymentButtons paymentState={state} /> + </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts @@ -15,11 +15,12 @@ */ import { + AbsoluteTime, AmountJson, - PreparePayResult, - PreparePayResultAlreadyConfirmed, - PreparePayResultInsufficientBalance, - PreparePayResultPaymentPossible, + MerchantContractChoice, + MerchantInfo, + PaymentInsufficientBalanceDetails, + TransactionIdStr } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; @@ -27,7 +28,7 @@ import { ErrorAlert } from "../../context/alert.js"; import { ButtonHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; import { useComponentState } from "./state.js"; -import { BaseView } from "./views.js"; +import { BaseView, PaymentStates } from "./views.js"; export interface Props { talerPayUri: string; @@ -39,16 +40,15 @@ export interface Props { export type State = | State.Loading | State.LoadingUriError - | State.Ready - | State.NoEnoughBalance - | State.NoBalanceForCurrency - | State.Confirmed; + | PaymentStates; export namespace State { + export interface Loading { status: "loading"; error: undefined; } + export interface LoadingUriError { status: "error"; error: ErrorAlert; @@ -56,41 +56,51 @@ export namespace State { interface BaseInfo { amount: AmountJson; - uri: string; + summary: string; + minimum_age: number | undefined, + expiration: AbsoluteTime; + merchant: MerchantInfo | undefined; + transactionId: TransactionIdStr; + shareUri: string; error: undefined; goToWalletManualWithdraw: (amount?: string) => Promise<void>; cancel: () => Promise<void>; } - export interface NoBalanceForCurrency extends BaseInfo { - status: "no-balance-for-currency"; - payStatus: PreparePayResult; - balance: undefined; - } + export interface NoEnoughBalance extends BaseInfo { status: "no-enough-balance"; - payStatus: PreparePayResultInsufficientBalance; - balance: AmountJson; + balanceDetails: PaymentInsufficientBalanceDetails | undefined; + effective: undefined; + choices: undefined | { + list: MerchantContractChoice[] + index: number; + select: (d: number) => void; + }, } + export interface Ready extends BaseInfo { status: "ready"; - payStatus: PreparePayResultPaymentPossible; payHandler: ButtonHandler; - balance: AmountJson; - onSelectChoice: (idx: number) => void; - selectedChoice: number; + effective: AmountJson; + choices: undefined | { + list: MerchantContractChoice[] + index: number; + select: (d: number) => void; + }, } export interface Confirmed extends BaseInfo { status: "confirmed"; - payStatus: PreparePayResultAlreadyConfirmed; - balance: AmountJson; + effective: undefined; + message?: string; + paid: boolean; } + } const viewMapping: StateViewMap<State> = { loading: Loading, error: ErrorAlertView, - "no-balance-for-currency": BaseView, "no-enough-balance": BaseView, confirmed: BaseView, ready: BaseView, diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts @@ -15,10 +15,16 @@ */ import { + AbsoluteTime, Amounts, + assertUnreachable, + ChoiceSelectionDetailType, ConfirmPayResultType, + GetChoicesForPaymentResult, + MerchantContractVersion, NotificationType, PreparePayResultType, + TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -28,6 +34,7 @@ import { useBackendContext } from "../../context/backend.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { ButtonHandler } from "../../mui/handlers.js"; import { Props, State } from "./index.js"; +import { trace } from "console"; export function useComponentState({ talerPayUri, @@ -48,8 +55,16 @@ export function useComponentState({ talerPayUri: talerPayUri, }, ); - const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); - return { payStatus, balance, uri: talerPayUri }; + + let choicesForPayment: GetChoicesForPaymentResult | undefined; + if (payStatus.status === PreparePayResultType.ChoiceSelection) { + + choicesForPayment = await api.wallet.call(WalletApiOperation.GetChoicesForPayment, { + transactionId: payStatus.transactionId + }); + + } + return { payStatus, uri: talerPayUri, choicesForPayment }; }, []); useEffect( @@ -91,48 +106,104 @@ export function useComponentState({ ), }; } - // if (hook.hasError) { - // return { - // status: "loading-uri", - // error: hook, - // }; - // } - const { payStatus } = hook.response; - if (payStatus.status === PreparePayResultType.ChoiceSelection) { - throw Error(`unsupported payment result ${payStatus.status}`); + const { payStatus, choicesForPayment } = hook.response; + + async function doPayment(): Promise<void> { + const res = await api.wallet.call(WalletApiOperation.ConfirmPay, { + transactionId: payStatus.transactionId, + choiceIndex: selectedChoice + }); + // handle confirm pay + if (res.type !== ConfirmPayResultType.Done) { + onSuccess(res.transactionId); + return; + } + const fu = res.contractTerms.fulfillment_url; + if (fu) { + if (typeof window !== "undefined") { + document.location.href = fu; + } + } + onSuccess(res.transactionId); } - const amount = Amounts.parseOrThrow(payStatus.amountRaw); + const payHandler: ButtonHandler = { + onClick: pushAlertOnError(doPayment), + }; - const foundBalance = hook.response.balance.balances.find( - (b) => Amounts.parseOrThrow(b.available).currency === amount.currency, - ); + const expiration: AbsoluteTime = + AbsoluteTime.fromProtocolTimestamp(payStatus.contractTerms.pay_deadline); const baseResult = { - uri: hook.response.uri, - amount, + shareUri: hook.response.uri, + transactionId: payStatus.transactionId, + receiver: payStatus.contractTerms.merchant, + summary: payStatus.contractTerms.summary, + merchant: payStatus.contractTerms.merchant, error: undefined, + expiration, + minimum_age: payStatus.contractTerms.minimum_age, cancel, goToWalletManualWithdraw, }; - if (!foundBalance) { - return { - status: "no-balance-for-currency", - balance: undefined, - payStatus, - ...baseResult, - }; + if (payStatus.status === PreparePayResultType.ChoiceSelection) { + // if status is choiceSelection we expect choicesForPayment to be present + const { choices: choicesDetails } = choicesForPayment!; + const selectedChoiceData = choicesDetails[selectedChoice]; + const amount = Amounts.parseOrThrow(selectedChoiceData.amountRaw); + + const choices = { + list: payStatus.contractTerms.version === 1 ? payStatus.contractTerms.choices : [], + index: selectedChoice, + select: onSelectChoice, + } + switch (selectedChoiceData.status) { + case ChoiceSelectionDetailType.PaymentPossible: { + const effective = Amounts.parseOrThrow(selectedChoiceData.amountEffective) + return { + status: "ready", + payHandler, + choices, + effective, + amount, + ...baseResult, + }; + } + case ChoiceSelectionDetailType.InsufficientBalance: { + return { + status: "no-enough-balance", + amount, + choices, + effective: undefined, + balanceDetails: selectedChoiceData.balanceDetails!, + ...baseResult, + }; + } + default: { + assertUnreachable(selectedChoiceData) + } + } + } - const foundAmount = Amounts.parseOrThrow(foundBalance.available); + const amount = Amounts.parseOrThrow(payStatus.amountRaw); + const effective = + "amountEffective" in payStatus + ? payStatus.amountEffective + ? Amounts.parseOrThrow(payStatus.amountEffective) + : Amounts.zeroOfCurrency(amount.currency) + : amount; + if (payStatus.status === PreparePayResultType.InsufficientBalance) { return { status: "no-enough-balance", - balance: foundAmount, - payStatus, + amount, + choices: undefined, + effective: undefined, + balanceDetails: payStatus.balanceDetails, ...baseResult, }; } @@ -140,41 +211,20 @@ export function useComponentState({ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { return { status: "confirmed", - balance: foundAmount, - payStatus, + amount, + effective: undefined, + paid: payStatus.paid, + message: payStatus.paid && payStatus.contractTerms.fulfillment_message ? payStatus.contractTerms.fulfillment_message : undefined, ...baseResult, }; } - async function doPayment(): Promise<void> { - const res = await api.wallet.call(WalletApiOperation.ConfirmPay, { - transactionId: payStatus.transactionId, - }); - // handle confirm pay - if (res.type !== ConfirmPayResultType.Done) { - onSuccess(res.transactionId); - return; - } - const fu = res.contractTerms.fulfillment_url; - if (fu) { - if (typeof window !== "undefined") { - document.location.href = fu; - } - } - onSuccess(res.transactionId); - } - - const payHandler: ButtonHandler = { - onClick: pushAlertOnError(doPayment), - }; - return { status: "ready", payHandler, - payStatus, + amount, + effective, ...baseResult, - balance: foundAmount, - onSelectChoice, - selectedChoice, + choices: undefined, }; } diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx @@ -44,169 +44,145 @@ export const NoEnoughBalanceAvailable = tests.createExample(BaseView, { status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - transactionId: " " as TransactionIdStr, - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:9" as AmountString, - balanceMaterial: "USD:9" as AmountString, - balanceAgeAcceptable: "USD:9" as AmountString, - balanceReceiverAcceptable: "USD:9" as AmountString, - balanceReceiverDepositable: "USD:9" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: - "taler://payment/96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0", - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - summary: "some beers", - amount: "USD:10", - } as Partial<ContractTerms> as any, - scopes: [], - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // transactionId: " " as TransactionIdStr, + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:9" as AmountString, + // balanceMaterial: "USD:9" as AmountString, + // balanceAgeAcceptable: "USD:9" as AmountString, + // balanceReceiverAcceptable: "USD:9" as AmountString, + // balanceReceiverDepositable: "USD:9" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: + // "taler://payment/96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0", + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // summary: "some beers", + // amount: "USD:10", + // } as Partial<ContractTerms> as any, + // scopes: [], + // amountRaw: "USD:10" as AmountString, + // }, }); export const NoEnoughBalanceMaterial = tests.createExample(BaseView, { status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:10" as AmountString, - balanceMaterial: "USD:9" as AmountString, - balanceAgeAcceptable: "USD:9" as AmountString, - balanceReceiverAcceptable: "USD:9" as AmountString, - balanceReceiverDepositable: "USD:0" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: "taler://pay/..", - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - summary: "some beers", - amount: "USD:10" as AmountString, - } as Partial<ContractTerms> as any, - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:10" as AmountString, + // balanceMaterial: "USD:9" as AmountString, + // balanceAgeAcceptable: "USD:9" as AmountString, + // balanceReceiverAcceptable: "USD:9" as AmountString, + // balanceReceiverDepositable: "USD:0" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: "taler://pay/..", + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // summary: "some beers", + // amount: "USD:10" as AmountString, + // } as Partial<ContractTerms> as any, + // amountRaw: "USD:10" as AmountString, + // }, }); export const NoEnoughBalanceAgeAcceptable = tests.createExample(BaseView, { status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:10" as AmountString, - balanceMaterial: "USD:10" as AmountString, - balanceAgeAcceptable: "USD:9" as AmountString, - balanceReceiverAcceptable: "USD:9" as AmountString, - balanceReceiverDepositable: "USD:9" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: "taler://pay/..", - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - minimum_age: 18, - summary: "some beers", - amount: "USD:10", - } as Partial<ContractTerms> as any, - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:10" as AmountString, + // balanceMaterial: "USD:10" as AmountString, + // balanceAgeAcceptable: "USD:9" as AmountString, + // balanceReceiverAcceptable: "USD:9" as AmountString, + // balanceReceiverDepositable: "USD:9" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: "taler://pay/..", + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // minimum_age: 18, + // summary: "some beers", + // amount: "USD:10", + // } as Partial<ContractTerms> as any, + // amountRaw: "USD:10" as AmountString, + // }, }); export const NoEnoughBalanceMerchantAcceptable = tests.createExample(BaseView, { status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:10" as AmountString, - balanceMaterial: "USD:10" as AmountString, - balanceAgeAcceptable: "USD:10" as AmountString, - balanceReceiverAcceptable: "USD:9" as AmountString, - balanceReceiverDepositable: "USD:9" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: "taler://pay/..", - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - summary: "some beers", - amount: "USD:10" as AmountString, - } as Partial<ContractTerms> as any, - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:10" as AmountString, + // balanceMaterial: "USD:10" as AmountString, + // balanceAgeAcceptable: "USD:10" as AmountString, + // balanceReceiverAcceptable: "USD:9" as AmountString, + // balanceReceiverDepositable: "USD:9" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: "taler://pay/..", + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // summary: "some beers", + // amount: "USD:10" as AmountString, + // } as Partial<ContractTerms> as any, + // amountRaw: "USD:10" as AmountString, + // }, }); export const NoEnoughBalanceMerchantDepositable = tests.createExample( @@ -215,42 +191,36 @@ export const NoEnoughBalanceMerchantDepositable = tests.createExample( status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:10" as AmountString, - balanceMaterial: "USD:10" as AmountString, - balanceAgeAcceptable: "USD:10" as AmountString, - balanceReceiverAcceptable: "USD:10" as AmountString, - balanceReceiverDepositable: "USD:9" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: "taler://pay/..", - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - summary: "some beers", - amount: "USD:10" as AmountString, - } as Partial<ContractTerms> as any, - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:10" as AmountString, + // balanceMaterial: "USD:10" as AmountString, + // balanceAgeAcceptable: "USD:10" as AmountString, + // balanceReceiverAcceptable: "USD:10" as AmountString, + // balanceReceiverDepositable: "USD:9" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: "taler://pay/..", + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // summary: "some beers", + // amount: "USD:10" as AmountString, + // } as Partial<ContractTerms> as any, + // amountRaw: "USD:10" as AmountString, + // }, }, ); @@ -258,322 +228,282 @@ export const NoEnoughBalanceFeeGap = tests.createExample(BaseView, { status: "no-enough-balance", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 12, - }, - - uri: "", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.InsufficientBalance, - balanceDetails: { - amountRequested: "USD:10" as AmountString, - balanceAvailable: "USD:10" as AmountString, - balanceMaterial: "USD:10" as AmountString, - balanceAgeAcceptable: "USD:10" as AmountString, - balanceReceiverAcceptable: "USD:10" as AmountString, - balanceReceiverDepositable: "USD:10" as AmountString, - maxEffectiveSpendAmount: "USD:9.5" as AmountString, - balanceExchangeDepositable: "USD:9.5" as AmountString, - perExchange: {}, - }, - talerUri: "taler://pay/..", - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - minimum_age: 18, - summary: "some beers", - amount: "USD:10" as AmountString, - } as Partial<ContractTerms> as any, - amountRaw: "USD:10" as AmountString, - }, + shareUri: "", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // status: PreparePayResultType.InsufficientBalance, + // balanceDetails: { + // amountRequested: "USD:10" as AmountString, + // balanceAvailable: "USD:10" as AmountString, + // balanceMaterial: "USD:10" as AmountString, + // balanceAgeAcceptable: "USD:10" as AmountString, + // balanceReceiverAcceptable: "USD:10" as AmountString, + // balanceReceiverDepositable: "USD:10" as AmountString, + // maxEffectiveSpendAmount: "USD:9.5" as AmountString, + // balanceExchangeDepositable: "USD:9.5" as AmountString, + // perExchange: {}, + // }, + // talerUri: "taler://pay/..", + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // minimum_age: 18, + // summary: "some beers", + // amount: "USD:10" as AmountString, + // } as Partial<ContractTerms> as any, + // amountRaw: "USD:10" as AmountString, + // }, }); export const PaymentPossible = tests.createExample(BaseView, { status: "ready", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, payHandler: { onClick: nullFunction, }, - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.PaymentPossible, - talerUri: "taler://pay/..", - amountEffective: "USD:10" as AmountString, - amountRaw: "USD:10" as AmountString, - contractTerms: { - nonce: "123213123", - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - pay_deadline: { - t_s: new Date().getTime() / 1000 + 60 * 60 * 3, - }, - amount: "USD:10" as AmountString, - summary: "some beers", - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - }, + shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // status: PreparePayResultType.PaymentPossible, + // talerUri: "taler://pay/..", + // amountEffective: "USD:10" as AmountString, + // amountRaw: "USD:10" as AmountString, + // contractTerms: { + // nonce: "123213123", + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // pay_deadline: { + // t_s: new Date().getTime() / 1000 + 60 * 60 * 3, + // }, + // amount: "USD:10" as AmountString, + // summary: "some beers", + // } as Partial<ContractTerms> as any, + // contractTermsHash: "123456", + // }, }); -export const PaymentWithChoices = tests.createExample(BaseView, { - status: "ready", - error: undefined, - amount: Amounts.parseOrThrow("USD:9"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, - payHandler: { - onClick: nullFunction, - }, - - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.PaymentPossible, - talerUri: "taler://pay/..", - amountEffective: "USD:10" as AmountString, - amountRaw: "USD:10" as AmountString, - contractTerms: { - version: 1, - nonce: "123213123", - choices: [{ - amount: "USD:0.5" as AmountString, - description: "Access to the article", - inputs: [], - max_fee: "USD:1" as AmountString, - outputs: [], - },{ - amount: "USD:10" as AmountString, - description: "One month of access", - description_i18n: "Buy one month of access to articles", - inputs: [], - max_fee: "USD:1" as AmountString, - outputs: [{ - token_family_slug: "zxc", - type: MerchantContractOutputType.Token, - key_index: 1, - // count: 1 - }], - }], - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - pay_deadline: { - t_s: new Date().getTime() / 1000 + 60 * 60 * 3, - }, - amount: "USD:10" as AmountString, - summary: "some beers", - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - }, -}); +// export const PaymentWithChoices = tests.createExample(BaseView, { +// status: "select-choice", +// error: undefined, +// amount: Amounts.parseOrThrow("USD:9"), +// payHandler: { +// onClick: nullFunction, +// }, +// selectedChoice: 0, +// shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", +// payStatus: { +// transactionId: +// "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, +// status: PreparePayResultType.ChoiceSelection, +// talerUri: "taler://pay/..", +// contractTerms: { +// version: 1, +// nonce: "123213123", +// choices: [{ +// amount: "USD:0.5" as AmountString, +// description: "Access to the article", +// inputs: [], +// max_fee: "USD:1" as AmountString, +// outputs: [], +// },{ +// amount: "USD:10" as AmountString, +// description: "One month of access", +// description_i18n: "Buy one month of access to articles", +// inputs: [], +// max_fee: "USD:1" as AmountString, +// outputs: [{ +// token_family_slug: "zxc", +// type: MerchantContractOutputType.Token, +// key_index: 1, +// // count: 1 +// }], +// }], +// merchant: { +// name: "the merchant", +// logo: merchantIcon, +// website: "https://www.themerchant.taler", +// email: "contact@merchant.taler", +// }, +// pay_deadline: { +// t_s: new Date().getTime() / 1000 + 60 * 60 * 3, +// }, +// amount: "USD:10" as AmountString, +// summary: "some beers", +// } as Partial<ContractTerms> as any, +// contractTermsHash: "123456", +// }, +// }); export const PaymentPossibleWithFee = tests.createExample(BaseView, { status: "ready", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, payHandler: { onClick: nullFunction, }, - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - status: PreparePayResultType.PaymentPossible, - talerUri: "taler://pay/..", - amountEffective: "USD:10.20" as AmountString, - amountRaw: "USD:10" as AmountString, - scopes: [], - - contractTerms: { - nonce: "123213123", - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - amount: "USD:10" as AmountString, - summary: "some beers", - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - }, + shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // status: PreparePayResultType.PaymentPossible, + // talerUri: "taler://pay/..", + // amountEffective: "USD:10.20" as AmountString, + // amountRaw: "USD:10" as AmountString, + // scopes: [], + + // contractTerms: { + // nonce: "123213123", + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // amount: "USD:10" as AmountString, + // summary: "some beers", + // } as Partial<ContractTerms> as any, + // contractTermsHash: "123456", + // }, }); export const TicketWithAProductList = tests.createExample(BaseView, { status: "ready", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, payHandler: { onClick: nullFunction, }, - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - status: PreparePayResultType.PaymentPossible, - talerUri: "taler://pay/..", - amountEffective: "USD:10.20" as AmountString, - amountRaw: "USD:10" as AmountString, - scopes: [], - - contractTerms: { - nonce: "123213123", - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - amount: "USD:10", - summary: "some beers", - products: [ - { - description: "ten beers", - price: "USD:1", - quantity: 10, - image: beer, - }, - { - description: "beer without image", - price: "USD:1", - quantity: 10, - }, - { - description: "one brown beer", - price: "USD:2", - quantity: 1, - image: beer, - }, - ], - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - }, + shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // status: PreparePayResultType.PaymentPossible, + // talerUri: "taler://pay/..", + // amountEffective: "USD:10.20" as AmountString, + // amountRaw: "USD:10" as AmountString, + // scopes: [], + + // contractTerms: { + // nonce: "123213123", + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // amount: "USD:10", + // summary: "some beers", + // products: [ + // { + // description: "ten beers", + // price: "USD:1", + // quantity: 10, + // image: beer, + // }, + // { + // description: "beer without image", + // price: "USD:1", + // quantity: 10, + // }, + // { + // description: "one brown beer", + // price: "USD:2", + // quantity: 1, + // image: beer, + // }, + // ], + // } as Partial<ContractTerms> as any, + // contractTermsHash: "123456", + // }, }); export const TicketWithShipping = tests.createExample(BaseView, { status: "ready", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, payHandler: { onClick: nullFunction, }, - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - status: PreparePayResultType.PaymentPossible, - talerUri: "taler://pay/..", - amountEffective: "USD:10.20" as AmountString, - amountRaw: "USD:10" as AmountString, - scopes: [], - - contractTerms: { - nonce: "123213123", - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - amount: "USD:10", - summary: "banana pi set", - products: [ - { - description: "banana pi", - price: "USD:2", - quantity: 1, - }, - ], - delivery_date: { - t_s: new Date().getTime() / 1000 + 30 * 24 * 60 * 60, - }, - delivery_location: { - town: "Liverpool", - street: "Down st 1234", - }, - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - }, + shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // status: PreparePayResultType.PaymentPossible, + // talerUri: "taler://pay/..", + // amountEffective: "USD:10.20" as AmountString, + // amountRaw: "USD:10" as AmountString, + // scopes: [], + + // contractTerms: { + // nonce: "123213123", + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // amount: "USD:10", + // summary: "banana pi set", + // products: [ + // { + // description: "banana pi", + // price: "USD:2", + // quantity: 1, + // }, + // ], + // delivery_date: { + // t_s: new Date().getTime() / 1000 + 30 * 24 * 60 * 60, + // }, + // delivery_location: { + // town: "Liverpool", + // street: "Down st 1234", + // }, + // } as Partial<ContractTerms> as any, + // contractTermsHash: "123456", + // }, }); export const AlreadyConfirmedByOther = tests.createExample(BaseView, { status: "confirmed", error: undefined, amount: Amounts.parseOrThrow("USD:10"), - balance: { - currency: "USD", - fraction: 40000000, - value: 11, - }, - - uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", - payStatus: { - transactionId: - "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, - scopes: [], - status: PreparePayResultType.AlreadyConfirmed, - talerUri: "taler://pay/..", - amountEffective: "USD:10" as AmountString, - amountRaw: "USD:10" as AmountString, - contractTerms: { - merchant: { - name: "the merchant", - logo: merchantIcon, - website: "https://www.themerchant.taler", - email: "contact@merchant.taler", - }, - summary: "some beers", - amount: "USD:10", - } as Partial<ContractTerms> as any, - contractTermsHash: "123456", - paid: false, - }, + shareUri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0", + // payStatus: { + // transactionId: + // "txn:payment:96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0" as TransactionIdStr, + // scopes: [], + // status: PreparePayResultType.AlreadyConfirmed, + // talerUri: "taler://pay/..", + // amountEffective: "USD:10" as AmountString, + // amountRaw: "USD:10" as AmountString, + // contractTerms: { + // merchant: { + // name: "the merchant", + // logo: merchantIcon, + // website: "https://www.themerchant.taler", + // email: "contact@merchant.taler", + // }, + // summary: "some beers", + // amount: "USD:10", + // } as Partial<ContractTerms> as any, + // contractTermsHash: "123456", + // paid: false, + // }, }); diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts @@ -103,11 +103,11 @@ describe("Payment CTA states", () => { expect(error).undefined; }, (state) => { - if (state.status !== "no-balance-for-currency") { + if (state.status !== "no-enough-balance") { expect(state).eq({}); return; } - expect(state.balance).undefined; + // expect(state.balance).undefined; expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); }, ], @@ -167,7 +167,7 @@ describe("Payment CTA states", () => { }, (state) => { if (state.status !== "no-enough-balance") expect.fail(); - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); }, ], @@ -230,7 +230,7 @@ describe("Payment CTA states", () => { expect(state).eq({}); return; } - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10")); expect(state.payHandler.onClick).not.undefined; }, @@ -291,7 +291,7 @@ describe("Payment CTA states", () => { }, (state) => { if (state.status !== "ready") expect.fail(); - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); expect(state.payHandler.onClick).not.undefined; }, @@ -361,7 +361,7 @@ describe("Payment CTA states", () => { expect(state).eq({}); return; } - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); if (state.payHandler.onClick === undefined) expect.fail(); state.payHandler.onClick(); @@ -432,7 +432,7 @@ describe("Payment CTA states", () => { }, (state) => { if (state.status !== "ready") expect.fail(); - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); if (state.payHandler.onClick === undefined) expect.fail(); @@ -545,7 +545,7 @@ describe("Payment CTA states", () => { }, (state) => { if (state.status !== "ready") expect.fail(); - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); expect(state.payHandler.onClick).not.undefined; @@ -561,7 +561,7 @@ describe("Payment CTA states", () => { }, (state) => { if (state.status !== "ready") expect.fail(); - expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); + // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15")); expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9")); // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1")); expect(state.payHandler.onClick).not.undefined; diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -17,13 +17,12 @@ import { AbsoluteTime, Amounts, - MerchantContractTerms as ContractTerms, Duration, - PreparePayResultType, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; +import { EnabledBySettings } from "../../components/EnabledBySettings.js"; import { Part } from "../../components/Part.js"; import { PaymentButtons } from "../../components/PaymentButtons.js"; import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js"; @@ -36,41 +35,25 @@ import { } from "../../components/styled/index.js"; import { MerchantDetails } from "../../wallet/Transaction.js"; import { State } from "./index.js"; -import { EnabledBySettings } from "../../components/EnabledBySettings.js"; -type SupportedStates = +export type PaymentStates = | State.Ready | State.Confirmed - | State.NoBalanceForCurrency | State.NoEnoughBalance; -export function BaseView(state: SupportedStates): VNode { +const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), +); + +export function BaseView(state: PaymentStates): VNode { const { i18n } = useTranslationContext(); - const contractTerms: ContractTerms = state.payStatus.contractTerms; - - const effective = - "amountEffective" in state.payStatus - ? state.payStatus.amountEffective - ? Amounts.parseOrThrow(state.payStatus.amountEffective) - : Amounts.zeroOfCurrency(state.amount.currency) - : state.amount; - - const expiration = !contractTerms.pay_deadline - ? undefined - : AbsoluteTime.fromProtocolTimestamp(contractTerms.pay_deadline); - const inFiveMinutes = AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromSpec({ minutes: 5 }), - ); - const willExpireSoon = - !expiration || expiration.t_ms === "never" - ? undefined - : AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; + const willExpireSoon = AbsoluteTime.cmp(state.expiration, inFiveMinutes) === -1; + + // const choices = state.contractTerms.version && state.contractTerms.version === 1 ? state.contractTerms.choices : []; - const choices = contractTerms.version && contractTerms.version === 1 ? contractTerms.choices : []; - // choices[0].inputs[0].type === return ( <Fragment> <ShowImportantMessage state={state} /> @@ -78,7 +61,7 @@ export function BaseView(state: SupportedStates): VNode { <section style={{ textAlign: "left" }}> <Part title={ - contractTerms.minimum_age ? ( + state.minimum_age ? ( <Fragment> <i18n.Translate>Purchase</i18n.Translate> &nbsp; @@ -86,85 +69,88 @@ export function BaseView(state: SupportedStates): VNode { size={20} title={i18n.str`This purchase is age restricted.`} > - {contractTerms.minimum_age}+ + {state.minimum_age}+ </AgeSign> </Fragment> ) : ( <i18n.Translate>Purchase</i18n.Translate> ) } - text={contractTerms.summary as TranslatedString} - kind="neutral" - /> - <Part - title={i18n.str`Merchant`} - text={<MerchantDetails merchant={contractTerms.merchant} />} + text={state.summary as TranslatedString} kind="neutral" /> + {!state.merchant ? undefined : + <Part + title={i18n.str`Merchant`} + text={<MerchantDetails merchant={state.merchant} />} + kind="neutral" + /> + } {willExpireSoon && ( <Part title={i18n.str`Expires at`} - text={<Time timestamp={expiration} format="HH:mm" />} + text={<Time timestamp={state.expiration} format="HH:mm" />} kind="neutral" /> )} + {state.status === "confirmed" || state.choices === undefined ? undefined : + <TableWithRoundRows> + {state.choices.list.map((entry, idx) => { + const selected = state.choices!.index === idx; + const av = Amounts.parseOrThrow(entry.amount); + + return ( + <tr + key={idx} + onClick={() => state.choices!.select(idx)} + style={{ cursor: !selected ? "pointer" : "default" }} + > + <td style={{ borderWidth: selected ? 4 : 1 }}>{av.currency}</td> + <td + style={{ + fontSize: "2em", + textAlign: "left", + borderWidth: selected ? 4 : 1, + }} + > + {Amounts.stringifyValue(av, 2)} + </td> + <td style={{ + borderWidth: selected ? 4 : 1, + width: "100%", + textAlign: "right", + }}>{entry.description}</td> + + </tr> + ); + })} + </TableWithRoundRows> + } </section> <EnabledBySettings name="advancedMode"> <section style={{ textAlign: "left" }}> <ShowFullContractTermPopup - transactionId={state.payStatus.transactionId} + transactionId={state.transactionId} /> </section> </EnabledBySettings> - <TableWithRoundRows> - {choices.map((entry, idx) => { - const selected = state.status === "ready" && state.selectedChoice === idx; - const av = Amounts.parseOrThrow(entry.amount); - - return ( - <tr - key={idx} - onClick={state.status === "ready" ? () => state.onSelectChoice(idx) : undefined} - style={{ cursor: !selected ? "pointer" : "default" }} - > - <td>{av.currency}</td> - <td - style={{ - fontSize: "2em", - textAlign: "right", - width: "100%", - }} - > - {Amounts.stringifyValue(av, 2)} - </td> - </tr> - ); - })} - </TableWithRoundRows> - - <PaymentButtons - amount={effective} - payStatus={state.payStatus} - uri={state.uri} - payHandler={state.status === "ready" ? state.payHandler : undefined} - goToWalletManualWithdraw={state.goToWalletManualWithdraw} - /> + + <PaymentButtons paymentState={state} /> </Fragment> ); } -function ShowImportantMessage({ state }: { state: SupportedStates }): VNode { +function ShowImportantMessage({ state }: { state: PaymentStates }): VNode { const { i18n } = useTranslationContext(); - const { payStatus } = state; - if (payStatus.status === PreparePayResultType.AlreadyConfirmed) { - if (payStatus.paid) { - if (payStatus.contractTerms.fulfillment_url) { + if (state.status === "confirmed") { + if (state.paid) { + if (state.message) { return ( <SuccessBox> <i18n.Translate> Already paid, you are going to be redirected to{" "} - <a href={payStatus.contractTerms.fulfillment_url}> - {payStatus.contractTerms.fulfillment_url} + <a href={state.message}> + {state.message} </a> </i18n.Translate> </SuccessBox>