diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/cta')
10 files changed, 428 insertions, 248 deletions
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx index 547d5ac9a..0d8035136 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -24,12 +24,21 @@ import { InvoicePaymentDetails, } from "../../wallet/Transaction.js"; import { State } from "./index.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; export function ReadyView( state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance, ): VNode { const { i18n } = useTranslationContext(); const { summary, effective, raw, expiration, uri, status, payStatus } = state; + + const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ); + const willExpireSoon = + expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; + return ( <Fragment> <section style={{ textAlign: "left" }}> @@ -42,11 +51,13 @@ export function ReadyView( /> } /> - <Part - title={i18n.str`Valid until`} - text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />} - kind="neutral" - /> + {willExpireSoon && ( + <Part + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} + kind="neutral" + /> + )} </section> <PaymentButtons amount={effective} diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx index 68d161ab2..b1eee85ec 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx @@ -18,6 +18,7 @@ import { AbsoluteTime, Amounts, MerchantContractTerms as ContractTerms, + Duration, PreparePayResultType, TranslatedString, } from "@gnu-taler/taler-util"; @@ -54,6 +55,17 @@ export function BaseView(state: SupportedStates): VNode { : 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; return ( <Fragment> <ShowImportantMessage state={state} /> @@ -65,7 +77,12 @@ export function BaseView(state: SupportedStates): VNode { <Fragment> <i18n.Translate>Purchase</i18n.Translate> - <AgeSign size={20} title={i18n.str`This purchase is age restricted.`}>{contractTerms.minimum_age}+</AgeSign> + <AgeSign + size={20} + title={i18n.str`This purchase is age restricted.`} + > + {contractTerms.minimum_age}+ + </AgeSign> </Fragment> ) : ( <i18n.Translate>Purchase</i18n.Translate> @@ -79,17 +96,10 @@ export function BaseView(state: SupportedStates): VNode { text={<MerchantDetails merchant={contractTerms.merchant} />} kind="neutral" /> - {contractTerms.pay_deadline && ( + {willExpireSoon && ( <Part - title={i18n.str`Valid until`} - text={ - <Time - timestamp={AbsoluteTime.fromProtocolTimestamp( - contractTerms.pay_deadline, - )} - format="dd MMMM yyyy, HH:mm" - /> - } + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} kind="neutral" /> )} diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts index 1a92c4073..ba854a93c 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts @@ -47,12 +47,18 @@ export function useComponentState({ const hook = useAsyncAsHook(async () => { if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE"); const templateP = await api.wallet.call( - WalletApiOperation.CheckPayForTemplate, { talerPayTemplateUri: talerTemplateUri }, + WalletApiOperation.CheckPayForTemplate, + { talerPayTemplateUri: talerTemplateUri }, ); - const requireMoreInfo = !templateP.templateDetails.template_contract.amount || !templateP.templateDetails.template_contract.summary; + const requireMoreInfo = + !templateP.templateDetails.template_contract.amount || + !templateP.templateDetails.template_contract.summary; let payStatus: PreparePayResult | undefined = undefined; if (!requireMoreInfo) { - payStatus = await api.wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: talerTemplateUri }); + payStatus = await api.wallet.call( + WalletApiOperation.PreparePayForTemplate, + { talerPayTemplateUri: talerTemplateUri }, + ); } const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); return { payStatus, balance, uri: talerTemplateUri, templateP }; @@ -102,20 +108,28 @@ export function useComponentState({ const cfg = hook.response.templateP.templateDetails.template_contract; const def = hook.response.templateP.templateDetails.editable_defaults; - const fixedAmount = cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined; - const fixedSummary = cfg.summary !== undefined ? cfg.summary : undefined; - - const defaultAmount = def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined; - const defaultSummary = def?.summary !== undefined ? def.summary : undefined; - - const zero = fixedAmount ? Amounts.zeroOfAmount(fixedAmount) : - cfg.currency !== undefined ? Amounts.zeroOfCurrency(cfg.currency) : - defaultAmount !== undefined ? Amounts.zeroOfAmount(defaultAmount) : - def?.currency !== undefined ? Amounts.zeroOfCurrency(def.currency) : - Amounts.zeroOfCurrency(hook.response.templateP.supportedCurrencies[0]); - - const [amount, setAmount] = useState(defaultAmount ?? zero); - const [summary, setSummary] = useState(defaultSummary ?? ""); + const fixedAmount = + cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined; + const fixedSummary = cfg.summary; + + const defaultAmount = + def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined; + const defaultSummary = def?.summary; + + const zero = fixedAmount + ? Amounts.zeroOfAmount(fixedAmount) + : cfg.currency !== undefined + ? Amounts.zeroOfCurrency(cfg.currency) + : defaultAmount !== undefined + ? Amounts.zeroOfAmount(defaultAmount) + : def?.currency !== undefined + ? Amounts.zeroOfCurrency(def.currency) + : Amounts.zeroOfCurrency( + hook.response.templateP.supportedCurrencies[0], + ); + + const [amount, setAmount] = useState(defaultAmount ?? fixedAmount ?? zero); + const [summary, setSummary] = useState(defaultSummary ?? fixedSummary ?? ""); async function createOrder() { try { @@ -140,41 +154,50 @@ export function useComponentState({ } const errors = undefinedIfEmpty({ - amount: fixedAmount !== undefined ? undefined : amount && Amounts.isZero(amount) ? i18n.str`required` : undefined, - summary: fixedSummary !== undefined ? undefined : summary !== undefined && !summary ? i18n.str`required` : undefined, + amount: + fixedAmount !== undefined + ? undefined + : amount && Amounts.isZero(amount) + ? i18n.str`required` + : undefined, + summary: + fixedSummary !== undefined + ? undefined + : summary !== undefined && !summary + ? i18n.str`required` + : undefined, }); return { status: "fill-template", error: undefined, minAge: cfg.minimum_age ?? 0, - amount: - fixedAmount === undefined - ? ({ - onInput: (a) => { - setAmount(a); - }, - value: amount, - error: errors?.amount, - } as AmountFieldHandler) - : undefined, - summary: - fixedSummary === undefined - ? ({ - onInput: (t) => { - setSummary(t); - }, - value: summary, - error: errors?.summary, - } as TextFieldHandler) - : undefined, + amount: { + onInput: + fixedAmount !== undefined + ? undefined + : (a) => { + setAmount(a); + }, + value: amount, + error: errors?.amount, + } as AmountFieldHandler, + summary: { + onInput: + fixedSummary !== undefined + ? undefined + : (t) => { + setSummary(t); + }, + value: summary, + error: errors?.summary, + } as TextFieldHandler, onCreate: { onClick: errors ? undefined : safely("create order for pay template", createOrder), }, }; - } - + }; } function undefinedIfEmpty<T extends object>(obj: T): T | undefined { diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx index ce53c3cf9..4a1cfe3ac 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx @@ -33,24 +33,11 @@ export function ReadyView({ return ( <Fragment> <section style={{ textAlign: "left" }}> - {/* <Part - title={ - <div - style={{ - display: "flex", - alignItems: "center", - }} - > - <i18n.Translate>Merchant</i18n.Translate> - </div> - } - text={<ExchangeDetails exchange={exchangeUrl} />} - kind="neutral" - big - /> */} {!amount ? undefined : ( <p> - <AmountField label={i18n.str`Amount`} handler={amount} /> + <AmountField label={i18n.str`Amount`} + handler={amount} + /> </p> )} {!summary ? undefined : ( @@ -60,6 +47,7 @@ export function ReadyView({ variant="filled" required fullWidth + disabled={summary.onInput === undefined} error={summary.error} value={summary.value} onChange={summary.onInput} @@ -67,12 +55,12 @@ export function ReadyView({ </p> )} </section> - {minAge && ( + {minAge ? ( <section> <AgeSign size={25}>{minAge}+</AgeSign> <i18n.Translate>This purchase is age restricted.</i18n.Translate> </section> - )} + ) : undefined} <section> <Button onClick={onCreate.onClick} variant="contained" color="success"> <i18n.Translate>Review order</i18n.Translate> diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx index caa1b485a..e82c4fbd2 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx @@ -26,6 +26,7 @@ import { } from "../../wallet/Transaction.js"; import { State } from "./index.js"; import { TermsOfService } from "../../components/TermsOfService/index.js"; +import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; export function ReadyView({ accept, @@ -36,6 +37,12 @@ export function ReadyView({ raw, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + const inFiveMinutes = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ); + const willExpireSoon = + expiration && AbsoluteTime.cmp(expiration, inFiveMinutes) === -1; return ( <Fragment> <section style={{ textAlign: "left" }}> @@ -49,15 +56,16 @@ export function ReadyView({ /> } /> - - <Part - title={i18n.str`Valid until`} - text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />} - kind="neutral" - /> + {willExpireSoon && ( + <Part + title={i18n.str`Expires at`} + text={<Time timestamp={expiration} format="HH:mm" />} + kind="neutral" + /> + )} </section> <section> - <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} > + <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl}> <Button variant="contained" color="success" onClick={accept.onClick}> <i18n.Translate> Receive {<Amount value={effective} />} diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts index d33abffee..418fef505 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts @@ -18,7 +18,7 @@ import { AmountJson, AmountString, CurrencySpecification, - ExchangeListItem + ExchangeListItem, } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js"; @@ -85,7 +85,7 @@ export namespace State { operationState: "confirmed" | "aborted" | "selected"; thisWallet: boolean; redirectToTx: () => void; - confirmTransferUrl?: string, + confirmTransferUrl?: string; error: undefined; } @@ -95,20 +95,26 @@ export namespace State { currentExchange: ExchangeListItem; - chosenAmount: AmountJson; - withdrawalFee: AmountJson; + amount: AmountFieldHandler; + editableAmount: boolean; + + bankFee: AmountJson; toBeReceived: AmountJson; + toBeSent: AmountJson; doWithdrawal: ButtonHandler; doSelectExchange: ButtonHandler; + editableExchange: boolean; chooseCurrencies: string[]; selectedCurrency: string; changeCurrency: (s: string) => void; - conversionInfo: { - spec: CurrencySpecification, - amount: AmountJson, - } | undefined; + conversionInfo: + | { + spec: CurrencySpecification; + amount: AmountJson; + } + | undefined; ageRestriction?: SelectFieldHandler; diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index f592072ff..0541bbf3f 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -54,7 +54,7 @@ export function useComponentStateFromParams({ ? parseWithdrawExchangeUri(maybeTalerUri) : undefined; const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl; - + let ex: ExchangeFullDetails | undefined; if (exchangeByTalerUri) { await api.wallet.call(WalletApiOperation.AddExchange, { @@ -185,9 +185,16 @@ export function useComponentStateFromParams({ cancel, onSuccess, undefined, - chosenAmount, - exchangeList, - exchangeByTalerUri, + { + amount: chosenAmount, + currency: chosenAmount.currency, + maxAmount: Amounts.zeroOfCurrency(chosenAmount.currency), + bankFee: Amounts.zeroOfCurrency(chosenAmount.currency), + editableAmount: true, + editableExchange: true, + exchange: exchangeByTalerUri, + exchangeList: exchangeList, + }, setUpdatedExchangeByUser, ); } @@ -212,33 +219,21 @@ export function useComponentStateFromURI({ const uriInfo = await api.wallet.call( WalletApiOperation.PrepareBankIntegratedWithdrawal, + { talerWithdrawUri }, + ); + const { status } = uriInfo.info; + const txInfo = await api.wallet.call( + WalletApiOperation.GetTransactionById, { - talerWithdrawUri, - selectedExchange: updatedExchangeByUser, + transactionId: uriInfo.transactionId, }, ); - const { - amount, - defaultExchangeBaseUrl, - possibleExchanges, - confirmTransferUrl, - status, - } = uriInfo.info; - const txInfo = - uriInfo.transactionId === undefined - ? undefined - : await api.wallet.call(WalletApiOperation.GetTransactionById, { - transactionId: uriInfo.transactionId, - }); return { talerWithdrawUri, status, transactionId: uriInfo.transactionId, + bankWithdrawalInfo: uriInfo.info, txInfo: txInfo, - confirmTransferUrl, - amount: Amounts.parseOrThrow(amount), - thisExchange: defaultExchangeBaseUrl, - exchanges: possibleExchanges, }; }); @@ -278,9 +273,22 @@ export function useComponentStateFromURI({ const uri = uriInfoHook.response.talerWithdrawUri; const txId = uriInfoHook.response.transactionId; - const chosenAmount = uriInfoHook.response.amount; - const defaultExchange = uriInfoHook.response.thisExchange; - const exchangeList = uriInfoHook.response.exchanges; + const bwi = uriInfoHook.response.bankWithdrawalInfo; + + const amount = + bwi.amount === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.amount); + + const maxAmount = + bwi.maxAmount === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.maxAmount); + + const bankFee = + bwi.wireFee === undefined + ? Amounts.zeroOfCurrency(bwi.currency) + : Amounts.parseOrThrow(bwi.wireFee); async function doManagedWithdraw( exchange: string, @@ -290,9 +298,6 @@ export function useComponentStateFromURI({ transactionId: string; confirmTransferUrl: string | undefined; }> { - if (!txId) { - throw Error("can't confirm transaction"); - } const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, { exchangeBaseUrl: exchange, amount, @@ -305,12 +310,15 @@ export function useComponentStateFromURI({ }; } - if (uriInfoHook.response.txInfo && uriInfoHook.response.status !== "pending") { + if ( + uriInfoHook.response.txInfo && + uriInfoHook.response.status !== "pending" + ) { const info = uriInfoHook.response.txInfo; return { status: "already-completed", operationState: uriInfoHook.response.status, - confirmTransferUrl: uriInfoHook.response.confirmTransferUrl, + confirmTransferUrl: bwi.confirmTransferUrl, thisWallet: info.txState.major === TransactionMajorState.Pending, redirectToTx: () => onSuccess(info.transactionId), error: undefined, @@ -323,14 +331,32 @@ export function useComponentStateFromURI({ cancel, onSuccess, uri, - chosenAmount, - exchangeList, - defaultExchange, + { + amount, + bankFee, + maxAmount, + currency: bwi.currency, + editableAmount: bwi.editableAmount, + editableExchange: bwi.editableExchange, + exchange: bwi.defaultExchangeBaseUrl, + exchangeList: bwi.possibleExchanges, + }, setUpdatedExchangeByUser, ); }, []); } +type WithdrawalInfo = { + currency: string; + amount: AmountJson; + bankFee: AmountJson; + maxAmount: AmountJson; + editableAmount: boolean; + exchange: string | undefined; + editableExchange: boolean; + exchangeList: ExchangeListItem[]; +}; + type ManualOrManagedWithdrawFunction = ( exchange: string, ageRestricted: number | undefined, @@ -342,16 +368,14 @@ function exchangeSelectionState( cancel: () => Promise<void>, onSuccess: (txid: string) => Promise<void>, talerWithdrawUri: string | undefined, - chosenAmount: AmountJson, - exchangeList: ExchangeListItem[], - exchangeSuggestedByTheBank: string | undefined, + wInfo: WithdrawalInfo, onExchangeUpdated: (ex: string) => void, ): RecursiveState<State> { const api = useBackendContext(); const selectedExchange = useSelectedExchange({ - currency: chosenAmount.currency, - defaultExchange: exchangeSuggestedByTheBank, - list: exchangeList, + currency: wInfo.currency, + defaultExchange: wInfo.exchange, + list: wInfo.exchangeList, }); const current = @@ -364,6 +388,10 @@ function exchangeSelectionState( } }, [current]); + const safeAmount = wInfo.amount + ? wInfo.amount + : Amounts.zeroOfCurrency(wInfo.currency); + if (selectedExchange.status !== "ready") { return selectedExchange; } @@ -374,11 +402,12 @@ function exchangeSelectionState( | State.Loading => { const { i18n } = useTranslationContext(); const { pushAlertOnError } = useAlertContext(); + + const [choosenAmount, setChoosenAmount] = useState(safeAmount); const [ageRestricted, setAgeRestricted] = useState(0); - const currentExchange = selectedExchange.selected; const [selectedCurrency, setSelectedCurrency] = useState<string>( - chosenAmount.currency, + wInfo.currency, ); /** * With the exchange and amount, ask the wallet the information @@ -388,8 +417,8 @@ function exchangeSelectionState( const info = await api.wallet.call( WalletApiOperation.GetWithdrawalDetailsForAmount, { - exchangeBaseUrl: currentExchange.exchangeBaseUrl, - amount: Amounts.stringify(chosenAmount), + exchangeBaseUrl: selectedExchange.selected.exchangeBaseUrl, + amount: Amounts.stringify(choosenAmount), restrictAge: ageRestricted, }, ); @@ -401,20 +430,40 @@ function exchangeSelectionState( return { amount: withdrawAmount, + currentExchange: selectedExchange.selected, ageRestrictionOptions: info.ageRestrictionOptions, accounts: info.withdrawalAccountsList, }; - }, []); + }, [choosenAmount, selectedExchange.selected, ageRestricted]); const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false); + if (!amountHook) { + return { status: "loading", error: undefined }; + } + if (amountHook.hasError) { + return { + status: "error", + error: alertFromError( + i18n, + i18n.str`Could not load the withdrawal details`, + amountHook, + ), + }; + } + if (!amountHook.response) { + return { status: "loading", error: undefined }; + } + + const currentExchange = amountHook.response.currentExchange; + async function doWithdrawAndCheckError(): Promise<void> { try { setDoingWithdraw(true); const res = await doWithdraw( currentExchange.exchangeBaseUrl, !ageRestricted ? undefined : ageRestricted, - Amounts.stringify(chosenAmount), + Amounts.stringify(choosenAmount), ); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; @@ -429,32 +478,14 @@ function exchangeSelectionState( setDoingWithdraw(false); } - if (!amountHook) { - return { status: "loading", error: undefined }; - } - if (amountHook.hasError) { - return { - status: "error", - error: alertFromError( - i18n, - i18n.str`Could not load the withdrawal details`, - amountHook, - ), - }; - } - if (!amountHook.response) { - return { status: "loading", error: undefined }; - } - - const withdrawalFee = Amounts.sub( - amountHook.response.amount.raw, - amountHook.response.amount.effective, - ).amount; + const toBeSent = amountHook.response.amount.raw; const toBeReceived = amountHook.response.amount.effective; + const bankFee = wInfo.bankFee; + const ageRestrictionOptions = amountHook.response.ageRestrictionOptions?.reduce( - (p, c) => ({ ...p, [c]: `under ${c}` }), + (p, c) => ({ ...p, [c]: i18n.str`under ${c}` }), {} as Record<string, string>, ); @@ -495,28 +526,50 @@ function exchangeSelectionState( amount: Amounts.parseOrThrow(convAccount.transferAmount!), }; + const amountError = Amounts.isZero(choosenAmount) + ? i18n.str`should be greater than zero` + : Amounts.cmp(choosenAmount, wInfo.maxAmount) === -1 + ? i18n.str`choose a lower value` + : undefined; + return { status: "success", error: undefined, - doSelectExchange: selectedExchange.doSelect, + doSelectExchange: { + onClick: wInfo.editableExchange + ? selectedExchange.doSelect.onClick + : undefined, + }, + editableAmount: wInfo.editableAmount, + editableExchange: wInfo.editableExchange, currentExchange, toBeReceived, + toBeSent, chooseCurrencies, + bankFee, selectedCurrency, changeCurrency: (s) => { setSelectedCurrency(s); }, conversionInfo, - withdrawalFee, - chosenAmount, + amount: { + value: choosenAmount, + onInput: wInfo.editableAmount + ? pushAlertOnError(async (v) => { + setChoosenAmount(v); + }) + : undefined, + error: amountError, + }, talerWithdrawUri, ageRestriction, doWithdrawal: { - onClick: doingWithdraw - ? undefined - : pushAlertOnError(doWithdrawAndCheckError), + onClick: + doingWithdraw || amountError + ? undefined + : pushAlertOnError(doWithdrawAndCheckError), }, cancel, }; - }, []); + }, [selectedExchange.selected]); } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx index 29f39054f..d9b7c380e 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx @@ -43,17 +43,25 @@ const ageRestrictionSelectField = { export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + }, }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, + doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 10000000, value: 1, @@ -70,34 +78,41 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, { export const AlreadyAborted = tests.createExample(FinalStateOperation, { error: undefined, status: "already-completed", - operationState: "aborted" + operationState: "aborted", }); export const AlreadySelected = tests.createExample(FinalStateOperation, { error: undefined, status: "already-completed", - operationState: "selected" + operationState: "selected", }); export const AlreadyConfirmed = tests.createExample(FinalStateOperation, { error: undefined, status: "already-completed", - operationState: "confirmed" + operationState: "confirmed", }); - export const WithSomeFee = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + }, }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, + doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 10000000, value: 1, @@ -114,17 +129,25 @@ export const WithSomeFee = tests.createExample(SuccessView, { export const WithoutFee = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 0, + }, + }, + bankFee: { + currency: "EUR", fraction: 0, + value: 1, }, + doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 0, value: 0, @@ -141,17 +164,25 @@ export const WithoutFee = tests.createExample(SuccessView, { export const EditExchangeUntouched = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + }, }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, + doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 0, value: 0, @@ -168,17 +199,25 @@ export const EditExchangeUntouched = tests.createExample(SuccessView, { export const EditExchangeModified = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + }, }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, + doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 0, value: 0, @@ -196,18 +235,26 @@ export const WithAgeRestriction = tests.createExample(SuccessView, { error: undefined, status: "success", ageRestriction: ageRestrictionSelectField, - chosenAmount: { - currency: "USD", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "USD", + value: 2, + fraction: 10000000, + }, + }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, }, + doSelectExchange: {}, doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.demo.taler.net", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "USD", fraction: 0, value: 0, @@ -223,11 +270,19 @@ export const WithAgeRestriction = tests.createExample(SuccessView, { export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + }, }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, + chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "NETZBON", doWithdrawal: { onClick: nullFunction }, @@ -235,7 +290,7 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, { exchangeBaseUrl: "https://exchange.netzbon.ch", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "NETZBON", fraction: 10000000, value: 1, @@ -251,30 +306,38 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, { export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + }, + }, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, }, + chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "EUR", - changeCurrency: () => { }, + changeCurrency: () => {}, conversionInfo: { spec: { - name: "EUR" + name: "EUR", } as CurrencySpecification, amount: { currency: "EUR", fraction: 10000000, value: 1, - } + }, }, doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.netzbon.ch", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "NETZBON", fraction: 10000000, value: 1, @@ -290,30 +353,37 @@ export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, { export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, { error: undefined, status: "success", - chosenAmount: { - currency: "NETZBON", - value: 2, - fraction: 10000000, + amount: { + value: { + currency: "NETZBON", + value: 2, + fraction: 10000000, + }, }, chooseCurrencies: ["NETZBON", "EUR"], selectedCurrency: "EUR", - changeCurrency: () => { }, + changeCurrency: () => {}, + bankFee: { + currency: "EUR", + fraction: 0, + value: 1, + }, conversionInfo: { spec: { - name: "EUR" + name: "EUR", } as CurrencySpecification, amount: { currency: "EUR", fraction: 10000000, value: 2, - } + }, }, doWithdrawal: { onClick: nullFunction }, currentExchange: { exchangeBaseUrl: "https://exchange.netzbon.ch", tos: {}, } as Partial<ExchangeListItem> as any, - withdrawalFee: { + toBeSent: { currency: "NETZBON", fraction: 10000000, value: 1, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index 860cf1099..5a75cb4be 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -26,6 +26,7 @@ import { ExchangeListItem, ExchangeTosStatus, ScopeType, + TransactionIdStr, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { expect } from "chai"; @@ -111,13 +112,18 @@ describe("Withdraw CTA states", () => { WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - transactionId: "123", + transactionId: "123" as TransactionIdStr, info: { status: "pending", operationId: "123", + currency: "ARS", amount: "EUR:2" as AmountString, possibleExchanges: [], - } + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", + }, }, ); @@ -152,14 +158,19 @@ describe("Withdraw CTA states", () => { WalletApiOperation.PrepareBankIntegratedWithdrawal, undefined, { - transactionId: "123", + transactionId: "123" as TransactionIdStr, info: { status: "pending", operationId: "123", + currency: "ARS", amount: "ARS:2" as AmountString, possibleExchanges: exchanges, defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, - } + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", + }, }, ); handler.addWalletCallResponse( @@ -173,7 +184,7 @@ describe("Withdraw CTA states", () => { scopeInfo: { currency: "ARS", type: ScopeType.Exchange, - url: "http://asd" + url: "http://asd", }, withdrawalAccountsList: [], ageRestrictionOptions: [], @@ -197,8 +208,8 @@ describe("Withdraw CTA states", () => { if (state.status !== "success") return; expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); - expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); - expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; }, @@ -229,9 +240,14 @@ describe("Withdraw CTA states", () => { { status: "pending", operationId: "123", + currency: "ARS", amount: "ARS:2" as AmountString, possibleExchanges: exchangeWithNewTos, defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl, + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", }, ); handler.addWalletCallResponse( @@ -244,7 +260,7 @@ describe("Withdraw CTA states", () => { scopeInfo: { currency: "ARS", type: ScopeType.Exchange, - url: "http://asd" + url: "http://asd", }, tosAccepted: false, withdrawalAccountsList: [], @@ -259,9 +275,14 @@ describe("Withdraw CTA states", () => { { status: "pending", operationId: "123", + currency: "ARS", amount: "ARS:2" as AmountString, possibleExchanges: exchanges, defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl, + editableAmount: false, + editableExchange: false, + maxAmount: "ARS:1", + wireFee: "ARS:0", }, ); @@ -281,8 +302,8 @@ describe("Withdraw CTA states", () => { if (state.status !== "success") return; expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")); - expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")); - expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2")); + expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2")); expect(state.doWithdrawal.onClick).not.undefined; }, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx index cdddd9bbc..b6a356de8 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx @@ -19,6 +19,7 @@ import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Amount } from "../../components/Amount.js"; import { AmountField } from "../../components/AmountField.js"; +import { EnabledBySettings } from "../../components/EnabledBySettings.js"; import { Part } from "../../components/Part.js"; import { QR } from "../../components/QR.js"; import { SelectList } from "../../components/SelectList.js"; @@ -38,7 +39,7 @@ import { getAmountWithFee, } from "../../wallet/Transaction.js"; import { State } from "./index.js"; -import { EnabledBySettings } from "../../components/EnabledBySettings.js"; +import { Amounts } from "@gnu-taler/taler-util"; export function FinalStateOperation(state: State.AlreadyCompleted): VNode { const { i18n } = useTranslationContext(); @@ -143,8 +144,6 @@ export function FinalStateOperation(state: State.AlreadyCompleted): VNode { export function SuccessView(state: State.Success): VNode { const { i18n } = useTranslationContext(); - // const currentTosVersionIsAccepted = - // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted; return ( <Fragment> <section style={{ textAlign: "left" }}> @@ -174,6 +173,11 @@ export function SuccessView(state: State.Success): VNode { kind="neutral" big /> + {state.editableAmount ? ( + <Fragment> + <AmountField handler={state.amount} label={i18n.str`Amount`} /> + </Fragment> + ) : undefined} {state.chooseCurrencies.length > 0 ? ( <Fragment> <p> @@ -207,9 +211,10 @@ export function SuccessView(state: State.Success): VNode { conversion={state.conversionInfo?.amount} amount={getAmountWithFee( state.toBeReceived, - state.chosenAmount, + state.toBeSent, "credit", )} + bankFee={state.bankFee} /> } /> @@ -227,7 +232,6 @@ export function SuccessView(state: State.Success): VNode { </section> <section> - {/* <div> */} <TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}> <Button variant="contained" @@ -240,20 +244,6 @@ export function SuccessView(state: State.Success): VNode { </i18n.Translate> </Button> </TermsOfService> - {/* </div> - <div style={{ marginTop: 20 }}> - <Button - variant="text" - color="success" - - disabled={!state.doAbort.onClick} - onClick={state.doAbort.onClick} - > - <i18n.Translate> - Cancel - </i18n.Translate> - </Button> - </div> */} </section> {state.talerWithdrawUri ? ( <WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} /> |