diff options
author | Sebastian <sebasjm@gmail.com> | 2023-11-22 15:20:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-11-22 15:20:29 -0300 |
commit | 305c513c2bcc2b25fe57cf0ed9723781944f9f3f (patch) | |
tree | 2022b093f6bed0bfd74c257168033b206850e2b0 | |
parent | 33c0267b37eecf44dc9f04e124eb44d27cba700c (diff) | |
download | wallet-core-305c513c2bcc2b25fe57cf0ed9723781944f9f3f.tar.xz |
more cashout ui
17 files changed, 245 insertions, 82 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts b/packages/demobank-ui/src/components/Cashouts/index.ts index 6cbb1247d..ca58de98f 100644 --- a/packages/demobank-ui/src/components/Cashouts/index.ts +++ b/packages/demobank-ui/src/components/Cashouts/index.ts @@ -14,17 +14,18 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpError, utils } from "@gnu-taler/web-util/browser"; +import { ErrorLoading, HttpError, utils } from "@gnu-taler/web-util/browser"; import { Loading } from "@gnu-taler/web-util/browser"; // import { compose, StateViewMap } from "../../utils/index.js"; // import { wxApi } from "../../wxApi.js"; import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util"; import { useComponentState } from "./state.js"; import { FailedView, LoadingUriView, ReadyView } from "./views.js"; +import { h } from "preact"; export interface Props { account: string; - onSelected: (id: string) => void; + onSelected: (id: number) => void; } export type State = State.Loading | State.Failed | State.LoadingUriError | State.Ready; @@ -51,8 +52,8 @@ export namespace State { export interface Ready extends BaseInfo { status: "ready"; error: undefined; - cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: string })[]; - onSelected: (id: string) => void; + cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[]; + onSelected: (id: number) => void; } } @@ -66,7 +67,9 @@ export interface Transaction { const viewMapping: utils.StateViewMap<State> = { loading: Loading, - "loading-error": LoadingUriView, + "loading-error": ({error}) => { + return h(ErrorLoading, {error, showDetail:true}); + }, "failed": FailedView, ready: ReadyView, }; diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index 76a3a90df..651a7a034 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -14,13 +14,13 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Fragment, h, VNode } from "preact"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { State } from "./index.js"; +import { Amounts, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; +import { Attention, ErrorLoading, Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Amounts, assertUnreachable } from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; -import { Attention } from "@gnu-taler/web-util/browser"; +import { State } from "./index.js"; +import { useConversionInfo } from "../../hooks/circuit.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -52,6 +52,13 @@ export function FailedView({ error }: State.Failed) { export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { const { i18n } = useTranslationContext(); + const resp = useConversionInfo(); + if (!resp) { + return <Loading /> + } + if (resp instanceof TalerError) { + return <ErrorLoading error={resp} /> + } if (!cashouts.length) return <div /> const txByDate = cashouts.reduce((prev, cur) => { const d = cur.creation_time.t_s === "never" @@ -122,8 +129,8 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { </dl> */} </td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{confirmationTime}</td> - <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td> - <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} spec={resp.body.regional_currency_specification} /></td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} spec={resp.body.fiat_currency_specification} /></td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.status}</td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md"> diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index 72dd43415..1613cb06a 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -19,6 +19,7 @@ import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { doAutoFocus, RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; +import { useBankCoreApiContext } from "../../context/config.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -32,6 +33,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode { const { i18n } = useTranslationContext(); + const {config} = useBankCoreApiContext(); if (!transactions.length) return <div /> const txByDate = transactions.reduce((prev, cur) => { const d = cur.when.t_ms === "never" @@ -78,7 +80,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <dd class="mt-1 truncate text-gray-700"> {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> - <RenderAmount value={item.amount} /> + <RenderAmount value={item.amount} spec={config.currency_specification}/> </span> ) : ( <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> @@ -99,7 +101,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </td> <td data-negative={item.negative ? "true" : "false"} class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 "> - {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor /> + {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} withColor spec={config.currency_specification}/> ) : ( <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> )} diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index c0164d60a..01c62c409 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -83,7 +83,7 @@ export function useEstimator(): CashoutEstimators { } const credit = Amounts.parseOrThrow(resp.body.amount_credit); const debit = Amounts.parseOrThrow(resp.body.amount_debit); - const beforeFee = Amounts.add(credit, fee).amount; + const beforeFee = Amounts.sub(credit, fee).amount; return { debit, @@ -92,8 +92,8 @@ export function useEstimator(): CashoutEstimators { }; }, estimateByDebit: async (regionalAmount, fee) => { - const resp = await api.getConversionInfoAPI().getCashoutRate({ - debit: regionalAmount + const resp = await api.getConversionInfoAPI().getCashoutRate({ + debit: regionalAmount }); if (resp.type === "fail") { // can't happen @@ -170,7 +170,7 @@ export function useBusinessAccounts() { return undefined; } -type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: string } +type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number } function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId { return c !== undefined } @@ -182,11 +182,15 @@ export function useCashouts(account: string) { async function fetcher([username, token]: [string, AccessToken]) { const list = await api.getAccountCashouts({ username, token }) if (list.type !== "ok") { + console.error(list) return list; } const all: Array<CashoutWithId | undefined> = await Promise.all(list.body.cashouts.map(c => { return api.getCashoutById({ username, token }, c.cashout_id).then(r => { - if (r.type === "fail") return undefined + if (r.type === "fail") { + console.error("failed", r) + return undefined + } return { ...r.body, id: c.cashout_id } }) })) @@ -196,7 +200,7 @@ export function useCashouts(account: string) { } const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }> | TalerCoreBankErrorsByMethod<"getAccountCashouts">, TalerHttpError>( - !config.allow_conversion ? false : [account, token, "getAccountCashouts"], fetcher, { + !config.allow_conversion ? undefined : [account, token, "getAccountCashouts"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -213,17 +217,17 @@ export function useCashouts(account: string) { return undefined; } -export function useCashoutDetails(cashoutId: string) { +export function useCashoutDetails(cashoutId: number | undefined) { const { state: credentials } = useBackendState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials const { api } = useBankCoreApiContext(); - async function fetcher([username, token, id]: [string, AccessToken, string]) { + async function fetcher([username, token, id]: [string, AccessToken, number]) { return api.getCashoutById({ username, token }, id) } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getCashoutById">, TalerHttpError>( - [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, { + cashoutId === undefined ? undefined : [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 5fef04b66..34c39e9d3 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -23,6 +23,7 @@ import { useBackendState } from "../hooks/backend.js"; import { getAllBooleanPreferences, getLabelForPreferences, usePreferences } from "../hooks/preferences.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { useSettingsContext } from "../context/settings.js"; +import { useBankCoreApiContext } from "../context/config.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -45,8 +46,8 @@ export function BankFrame({ useEffect(() => { if (error) { const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString + console.log(error) if (error instanceof Error) { - console.log(error) notifyException(i18n.str`Internal error, please report.`, error) } else { notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString) @@ -118,9 +119,9 @@ export function BankFrame({ <Footer testingUrl={ - (typeof localStorage !== "undefined") && localStorage.getItem("bank-base-url") ? - localStorage.getItem("bank-base-url") ?? undefined : - undefined} + (typeof localStorage !== "undefined") && localStorage.getItem("bank-base-url") ? + localStorage.getItem("bank-base-url") ?? undefined : + undefined} GIT_HASH={GIT_HASH} VERSION={VERSION} /> @@ -172,6 +173,7 @@ function WelcomeAccount({ account: accountName }: { account: string }): VNode { function AccountBalance({ account }: { account: string }): VNode { const result = useAccountDetails(account); + const { config } = useBankCoreApiContext(); if (!result) { return <Loading /> } @@ -183,5 +185,6 @@ function AccountBalance({ account }: { account: string }): VNode { return <RenderAmount value={Amounts.parseOrThrow(result.body.balance.amount)} negative={result.body.balance.credit_debit_indicator === "debit"} + spec={config.currency_specification} /> } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index a6282c947..e035c7fed 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -18,6 +18,7 @@ import { AmountJson, AmountString, Amounts, + CurrencySpecification, Logger, PaytoString, TranslatedString, @@ -470,13 +471,12 @@ export function InputAmount( ); } -export function RenderAmount({ value, negative, withColor }: { value: AmountJson, negative?: boolean, withColor?: boolean }): VNode { - const { config } = useBankCoreApiContext() +export function RenderAmount({ value, spec, negative, withColor }: { spec: CurrencySpecification; value: AmountJson, negative?: boolean, withColor?: boolean }): VNode { const neg = !!negative //convert to true or false const str = Amounts.stringifyValue(value) const sep_pos = str.indexOf(FRAC_SEPARATOR) - if (sep_pos !== -1 && str.length - sep_pos - 1 > config.currency_specification.num_fractional_normal_digits) { - const limit = sep_pos + config.currency_specification.num_fractional_normal_digits + 1 + if (sep_pos !== -1 && str.length - sep_pos - 1 > spec.num_fractional_normal_digits) { + const limit = sep_pos + spec.num_fractional_normal_digits + 1 const normal = str.substring(0, limit) const small = str.substring(limit) return <span data-negative={withColor ? neg : undefined} class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index be8ff8b58..bfb118c6c 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -70,7 +70,7 @@ export function WithdrawalConfirmationQuestion({ }, []); const [notification, notify, handleError] = useLocalNotification() - const { api } = useBankCoreApiContext() + const { config, api } = useBankCoreApiContext() const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>(); const answer = parseInt(captchaAnswer ?? "", 10); const [busy, setBusy] = useState<Record<string, undefined>>() @@ -289,7 +289,7 @@ export function WithdrawalConfirmationQuestion({ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={details.amount} /> + <RenderAmount value={details.amount} spec={config.currency_specification} /> </dd> </div> </dl> diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx index 293b821e2..f2972ed65 100644 --- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx +++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx @@ -9,7 +9,7 @@ import { CreateCashout } from "../business/CreateCashout.js"; interface Props { account: string, onClose: () => void, - onSelected: (cid: string) => void + onSelected: (cid: number) => void } export function CashoutListForAccount({ account, onSelected, onClose }: Props): VNode { @@ -29,7 +29,7 @@ export function CashoutListForAccount({ account, onSelected, onClose }: Props): </h1> } - <CreateCashout onCancel={() => { }} onComplete={() => { }} account={account} /> + <CreateCashout focus onCancel={onClose} onComplete={() => { }} account={account} /> <Cashouts account={account} diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index 7a4fbddf5..ab5ceb8d5 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -52,7 +52,10 @@ export function ShowAccountDetails({ async function doUpdate() { if (!update || !submitAccount || !creds) return; await handleError(async () => { - const resp = await api.updateAccount(creds, { + const resp = await api.updateAccount({ + token: creds.token, + username: account, + }, { cashout_address: submitAccount.cashout_payto_uri, challenge_contact_data: undefinedIfEmpty({ email: submitAccount.contact_data?.email, diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index 5d8e3797a..b38d40012 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -61,12 +61,12 @@ export function AccountForm({ : validateIBAN(parsed.iban, i18n)) as PaytoString, contact_data: undefinedIfEmpty({ email: !newForm.contact_data?.email - ? i18n.str`required` + ? undefined : !EMAIL_REGEX.test(newForm.contact_data.email) ? i18n.str`it should be an email` : undefined, phone: !newForm.contact_data?.phone - ? i18n.str`required` + ? undefined : !newForm.contact_data.phone.startsWith("+") ? i18n.str`should start with +` : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) @@ -152,7 +152,7 @@ export function AccountForm({ name="name" data-error={!!errors?.name && form.name !== undefined} id="name" - disabled={purpose !== "create"} + disabled={purpose === "show"} value={form.name ?? ""} onChange={(e) => { form.name = e.currentTarget.value; @@ -189,7 +189,7 @@ export function AccountForm({ name="email" id="email" data-error={!!errors?.contact_data?.email && form.contact_data?.email !== undefined} - disabled={purpose !== "create"} + disabled={purpose === "show"} value={form.contact_data?.email ?? ""} onChange={(e) => { if (form.contact_data) { @@ -273,7 +273,11 @@ export function AccountForm({ <i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate> </p> </div> - + <div class="sm:col-span-5"> + <pre> + {JSON.stringify(errors, undefined, 2)} + </pre> + </div> </div> </div> {children} diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index 8c018120d..7d6cfaf7d 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -105,7 +105,7 @@ export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAcc i18n.str`unknown` ) : ( <span class="amount"> - <RenderAmount value={balance} negative={balanceIsDebit} /> + <RenderAmount value={balance} negative={balanceIsDebit} spec={config.currency_specification} /> </span> )} </td> diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx index 795a2c6d0..e9fa1dc47 100644 --- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx +++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx @@ -7,6 +7,7 @@ import { useLastMonitorInfo } from "../../hooks/circuit.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; import { WireTransfer } from "../WireTransfer.js"; import { AccountList } from "./AccountList.js"; +import { useBankCoreApiContext } from "../../context/config.js"; /** * Query account information and show QR code if there is pending withdrawal @@ -42,7 +43,6 @@ function Metrics(): VNode { const [metricType, setMetricType] = useState<TalerCorebankApi.MonitorTimeframeParam>(TalerCorebankApi.MonitorTimeframeParam.day); const resp = useLastMonitorInfo(new Date(), metricType); - console.log(resp) if (!resp) return <Fragment />; if (resp instanceof TalerError) { return <ErrorLoading error={resp} /> @@ -162,6 +162,7 @@ function Metrics(): VNode { function MetricValue({ current, previous }: { current: AmountString | undefined, previous: AmountString | undefined }): VNode { const { i18n } = useTranslationContext() + const {config} = useBankCoreApiContext(); const cmp = current && previous ? Amounts.cmp(current, previous) : 0; const currAmount = !current ? undefined : Number.parseFloat(Amounts.stringifyValue(current)) const prevAmount = !previous ? undefined : Number.parseFloat(Amounts.stringifyValue(previous)) @@ -173,11 +174,11 @@ function MetricValue({ current, previous }: { current: AmountString | undefined, const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%` return <dd class="mt-1 flex justify-between md:block lg:flex"> <div class="flex justify-start items-baseline text-2xl font-semibold text-indigo-600"> - {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} />} + {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} spec={config.currency_specification} />} </div> <div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600"> <small class="ml-2 text-sm font-medium text-gray-500"> - <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount value={Amounts.parseOrThrow(previous)} />} + <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount value={Amounts.parseOrThrow(previous)} spec={config.currency_specification}/>} </small> </div> diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index c5f4ebc4e..2f77f3960 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -26,6 +26,7 @@ import { Loading, LocalNotificationBanner, ShowInputErrorLabel, + notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; @@ -46,12 +47,13 @@ import { import { LoginForm } from "../LoginForm.js"; import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { getRandomPassword, getRandomUsername } from "../rnd.js"; interface Props { account: string; focus?: boolean, onComplete: (id: string) => void; - onCancel: () => void; + onCancel?: () => void; } type FormType = { @@ -77,7 +79,10 @@ export function CreateCashout({ estimateByCredit: calculateFromCredit, estimateByDebit: calculateFromDebit, } = useEstimator(); - const { config } = useBankCoreApiContext() + const { state: credentials } = useBackendState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials + + const { api, config } = useBankCoreApiContext() const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount: "2" }); const [notification, notify, handleError] = useLocalNotification() const info = useConversionInfo(); @@ -119,7 +124,7 @@ export function CreateCashout({ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold) } - const {fiat_currency, regional_currency} = info.body + const { fiat_currency, regional_currency, fiat_currency_specification, regional_currency_specification } = info.body const regionalZero = Amounts.zeroOfCurrency(regional_currency); const fiatZero = Amounts.zeroOfCurrency(fiat_currency); const limit = account.balanceIsDebit @@ -128,7 +133,6 @@ export function CreateCashout({ const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: fiatZero }; const [calc, setCalc] = useState(zeroCalc); - console.log(calc) const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee); const sellRate = conversionInfo.cashout_ratio /** @@ -159,6 +163,7 @@ export function CreateCashout({ setForm(newForm); } const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({ + subject: !form.subject ? i18n.str`required` : undefined, amount: !form.amount ? i18n.str`required` : !inputAmount @@ -174,6 +179,70 @@ export function CreateCashout({ }); const trimmedAmountStr = form.amount?.trim(); + async function createCashout() { + const request_uid = encodeCrock(getRandomBytes(32)) + await handleError(async () => { + if (!creds || !form.subject || !form.channel) return; + + const resp = await api.createCashout(creds, { + request_uid, + amount_credit: Amounts.stringify(calc.credit), + amount_debit: Amounts.stringify(calc.debit), + subject: form.subject, + tan_channel: form.channel, + }) + if (resp.type === "ok") { + notifyInfo(i18n.str`Cashout created`) + } else { + switch (resp.case) { + case "account-not-found": return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "request-already-used": return notify({ + type: "error", + title: i18n.str`Duplicated request detected, check if the operation succeded or try again.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "incorrect-exchange-rate": return notify({ + type: "error", + title: i18n.str`The exchange rate was incorrectly applied`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-contact-info": return notify({ + type: "error", + title: i18n.str`Missing contact info before to create the cashout`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-enough-balance": return notify({ + type: "error", + title: i18n.str`The account does not have sufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "cashout-not-supported": return notify({ + type: "error", + title: i18n.str`Cashouts are not supported`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "tan-failed": return notify({ + type: "error", + title: i18n.str`Sending the confirmation code failed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + } + assertUnreachable(resp) + } + }) + } + return ( <div> <LocalNotificationBanner notification={notification} /> @@ -192,18 +261,18 @@ export function CreateCashout({ <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span><i18n.Translate>Current balance</i18n.Translate></span> + <span><i18n.Translate>Balance</i18n.Translate></span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={account.balance} /> + <RenderAmount value={account.balance} spec={regional_currency_specification} /> </dd> </div> <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> - <span><i18n.Translate>Cashout fee</i18n.Translate></span> + <span><i18n.Translate>Fee</i18n.Translate></span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={sellFee} /> + <RenderAmount value={sellFee} spec={fiat_currency_specification} /> </dd> </div> </dl> @@ -226,7 +295,7 @@ export function CreateCashout({ class="block text-sm font-medium leading-6 text-gray-900" for="subject" > - {i18n.str`Subject`} + {i18n.str`Transfer subject`} </label> <div class="mt-2"> <input @@ -253,14 +322,24 @@ export function CreateCashout({ {/* amount */} <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="amount" - > - {form.isDebit - ? i18n.str`Amount to send` - : i18n.str`Amount to receive`} - </label> + <div class="flex justify-between"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="amount" + > + {form.isDebit + ? i18n.str`Amount to send` + : i18n.str`Amount to receive`} + </label> + <button type="button" data-enabled={form.isDebit} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" + onClick={() => { + form.isDebit = !form.isDebit + updateForm(structuredClone(form)) + }}> + <span aria-hidden="true" data-enabled={form.isDebit} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> + </button> + + </div> <div class="mt-2"> <InputAmount name="amount" @@ -287,7 +366,7 @@ export function CreateCashout({ <div class="justify-between items-center flex "> <dt class="text-sm text-gray-600"><i18n.Translate>Total cost</i18n.Translate></dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={calc.debit} negative withColor /> + <RenderAmount value={calc.debit} negative withColor spec={regional_currency_specification} /> </dd> </div> @@ -302,7 +381,7 @@ export function CreateCashout({ </a> */} </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={balanceAfter} /> + <RenderAmount value={balanceAfter} spec={regional_currency_specification} /> </dd> </div> {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : ( @@ -316,14 +395,14 @@ export function CreateCashout({ </a> */} </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={calc.beforeFee} /> + <RenderAmount value={calc.beforeFee} spec={fiat_currency_specification} /> </dd> </div> )} <div class="flex justify-between items-center border-t-2 afu pt-4"> <dt class="text-lg text-gray-900 font-medium"><i18n.Translate>Total cashout transfer</i18n.Translate></dt> <dd class="text-lg text-gray-900 font-medium"> - <RenderAmount value={calc.credit} withColor /> + <RenderAmount value={calc.credit} withColor spec={fiat_currency_specification} /> </dd> </div> </dl> @@ -331,6 +410,55 @@ export function CreateCashout({ )} {/* channel */} + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="channel" + > + {i18n.str`Confirmation the operation using`} + </label> + + <div class="mt-2 max-w-xl text-sm text-gray-500"> + <div class="px-4 mt-4 grid grid-cols-1 gap-y-6"> + + <label onClick={()=>{ + form.channel = TanChannel.EMAIL + updateForm(structuredClone(form)) + }} data-selected={form.channel === TanChannel.EMAIL} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Newsletter" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Email</i18n.Translate> + </span> + </span> + </span> + <svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + <label onClick={()=>{ + form.channel = TanChannel.SMS + updateForm(structuredClone(form)) + }} data-selected={form.channel === TanChannel.SMS} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Existing Customers" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>SMS</i18n.Translate> + </span> + </span> + </span> + <svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + + </div> + </div> + + </div> </div> </div> @@ -348,10 +476,10 @@ export function CreateCashout({ disabled={!!errors} onClick={(e) => { e.preventDefault() - // doChangePassword() + createCashout() }} > - <i18n.Translate>Change</i18n.Translate> + <i18n.Translate>Cashout</i18n.Translate> </button> </div> </form> diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index ddfc18a0c..52ff713e2 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -53,7 +53,9 @@ export function ShowCashoutDetails({ const { state } = useBackendState(); const creds = state.status !== "loggedIn" ? undefined : state const { api } = useBankCoreApiContext() - const result = useCashoutDetails(id); + const cid = Number.parseInt(id, 10) + + const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid); const [code, setCode] = useState<string | undefined>(undefined); const [notification, notify, handleError] = useLocalNotification() @@ -72,6 +74,10 @@ export function ShowCashoutDetails({ default: assertUnreachable(result) } } + if (Number.isNaN(cid)) { + //TODO: better error message + return <div>cashout id should be a number</div> + } const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, }); @@ -165,7 +171,7 @@ export function ShowCashoutDetails({ e.preventDefault(); if (!creds) return; await handleError(async () => { - const resp = await api.abortCashoutById(creds, id); + const resp = await api.abortCashoutById(creds, cid); if (resp.type === "ok") { onCancel(); } else { @@ -207,7 +213,7 @@ export function ShowCashoutDetails({ e.preventDefault(); if (!creds || !code) return; await handleError(async () => { - const resp = await api.confirmCashoutById(creds, id, { + const resp = await api.confirmCashoutById(creds, cid, { tan: code, }); if (resp.type === "ok") { diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index d7bf6be29..273fb97c6 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -442,7 +442,7 @@ export class TalerCoreBankHttpClient { body, }); switch (resp.status) { - case HttpStatusCode.Accepted: return opSuccess(resp, codecForCashoutPending()) + case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutPending()) case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp) case HttpStatusCode.Conflict: { const body = await resp.json() @@ -465,7 +465,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort * */ - async abortCashoutById(auth: UserAndToken, cid: string) { + async abortCashoutById(auth: UserAndToken, cid: number) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -487,7 +487,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm * */ - async confirmCashoutById(auth: UserAndToken, cid: string, body: TalerCorebankApi.CashoutConfirmRequest) { + async confirmCashoutById(auth: UserAndToken, cid: number, body: TalerCorebankApi.CashoutConfirmRequest) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -522,7 +522,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID * */ - async getCashoutById(auth: UserAndToken, cid: string) { + async getCashoutById(auth: UserAndToken, cid: number) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index 0ecc08b33..4c8a146a6 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -404,7 +404,7 @@ export const codecForBankAccountGetWithdrawalResponse = export const codecForCashoutPending = (): Codec<TalerCorebankApi.CashoutPending> => buildCodecForObject<TalerCorebankApi.CashoutPending>() - .property("cashout_id", codecForString()) + .property("cashout_id", codecForNumber()) .build("TalerCorebankApi.CashoutPending"); export const codecForCashoutConversionResponse = @@ -428,7 +428,7 @@ export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> => export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> => buildCodecForObject<TalerCorebankApi.CashoutInfo>() - .property("cashout_id", codecForString()) + .property("cashout_id", codecForNumber()) .property( "status", codecForEither( @@ -448,7 +448,7 @@ export const codecForGlobalCashouts = export const codecForGlobalCashoutInfo = (): Codec<TalerCorebankApi.GlobalCashoutInfo> => buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>() - .property("cashout_id", codecForString()) + .property("cashout_id", codecForNumber()) .property("username", codecForString()) .property( "status", @@ -465,7 +465,7 @@ export const codecForCashoutStatusResponse = buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>() .property("amount_credit", codecForAmountString()) .property("amount_debit", codecForAmountString()) - .property("confirmation_time", codecForTimestamp) + .property("confirmation_time", codecOptional(codecForTimestamp)) .property("creation_time", codecForTimestamp) // .property("credit_payto_uri", codecForPaytoString()) .property( @@ -1462,7 +1462,7 @@ export namespace TalerCorebankApi { export interface CashoutPending { // ID identifying the operation being created // and now waiting for the TAN confirmation. - cashout_id: string; + cashout_id: number; } export interface CashoutConfirmRequest { @@ -1476,7 +1476,7 @@ export namespace TalerCorebankApi { } export interface CashoutInfo { - cashout_id: string; + cashout_id: number; status: "pending" | "aborted" | "confirmed"; } export interface GlobalCashouts { @@ -1484,7 +1484,7 @@ export namespace TalerCorebankApi { cashouts: GlobalCashoutInfo[]; } export interface GlobalCashoutInfo { - cashout_id: string; + cashout_id: number; username: string; status: "pending" | "aborted" | "confirmed"; } diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index e5eaa3c14..108f5d005 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -159,6 +159,7 @@ describe("Withdraw CTA states", () => { amountEffective: "ARS:2" as AmountString, paytoUris: ["payto://"], tosAccepted: true, + withdrawalAccountList: [], ageRestrictionOptions: [], numCoins: 42, }, @@ -223,6 +224,7 @@ describe("Withdraw CTA states", () => { amountEffective: "ARS:2" as AmountString, paytoUris: ["payto://"], tosAccepted: false, + withdrawalAccountList: [], ageRestrictionOptions: [], numCoins: 42, }, |