aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/cta
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/cta')
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts105
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx24
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts203
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts41
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx28
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>
&nbsp;
- <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 &nbsp; {<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} />