diff options
author | Sebastian <sebasjm@gmail.com> | 2024-03-11 14:56:25 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-03-11 14:57:48 -0300 |
commit | 37f46f4d6b821d163c3e4db5c374b1120212ac74 (patch) | |
tree | 641c5bccd6d1b77fa440e67b80543eec9378ef4a /packages/bank-ui/src/pages/regional | |
parent | 4cbe754aca72b6edee922e3a84f251030293f088 (diff) | |
download | wallet-core-37f46f4d6b821d163c3e4db5c374b1120212ac74.tar.xz |
obs and cancel request, plus lint
Diffstat (limited to 'packages/bank-ui/src/pages/regional')
3 files changed, 914 insertions, 688 deletions
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx index 8845ec9a0..818a131e0 100644 --- a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx +++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx @@ -15,13 +15,14 @@ */ import { + AbsoluteTime, AmountJson, Amounts, HttpStatusCode, TalerBankConversionApi, TalerError, TranslatedString, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, @@ -30,18 +31,30 @@ import { ShowInputErrorLabel, useLocalNotification, useTranslationContext, - utils + utils, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { useBankCoreApiContext } from "../../context/config.js"; import { useSessionState } from "../../hooks/session.js"; -import { TransferCalculation, useCashinEstimator, useCashoutEstimator, useConversionInfo } from "../../hooks/regional.js"; +import { + TransferCalculation, + useCashinEstimator, + useCashoutEstimator, + useConversionInfo, +} from "../../hooks/regional.js"; import { RouteDefinition } from "../../route.js"; import { undefinedIfEmpty } from "../../utils.js"; import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js"; import { ProfileNavigation } from "../ProfileNavigation.js"; -import { FormErrors, FormStatus, FormValues, RecursivePartial, UIField, useFormState } from "../../hooks/form.js"; +import { + FormErrors, + FormStatus, + FormValues, + RecursivePartial, + UIField, + useFormState, +} from "../../hooks/form.js"; interface Props { routeMyAccountDetails: RouteDefinition; @@ -53,11 +66,12 @@ interface Props { onUpdateSuccess: () => void; } -type FormType = { amount: AmountJson, conv: TalerBankConversionApi.ConversionRate } - +type FormType = { + amount: AmountJson; + conv: TalerBankConversionApi.ConversionRate; +}; function useComponentState({ - onUpdateSuccess, routeCancel, routeConversionConfig, routeMyAccountCashout, @@ -67,9 +81,11 @@ function useComponentState({ }: Props): utils.RecursiveState<VNode> { const { i18n } = useTranslationContext(); - const result = useConversionInfo() - const info = result && !(result instanceof TalerError) && result.type === "ok" ? - result.body : undefined; + const result = useConversionInfo(); + const info = + result && !(result instanceof TalerError) && result.type === "ok" + ? result.body + : undefined; const { state: credentials } = useSessionState(); const creds = @@ -78,17 +94,17 @@ function useComponentState({ : credentials; if (!info) { - return <i18n.Translate>loading...</i18n.Translate> + return <i18n.Translate>loading...</i18n.Translate>; } if (!creds) { - return <i18n.Translate>only admin can setup conversion</i18n.Translate> + return <i18n.Translate>only admin can setup conversion</i18n.Translate>; } - return () => { + return function afterComponentLoads() { const { i18n } = useTranslationContext(); - const { bank, conversion, config } = useBankCoreApiContext(); + const { conversion } = useBankCoreApiContext(); const [notification, notify, handleError] = useLocalNotification(); @@ -96,66 +112,91 @@ function useComponentState({ amount: "100", conv: { cashin_min_amount: info.conversion_rate.cashin_min_amount.split(":")[1], - cashin_tiny_amount: info.conversion_rate.cashin_tiny_amount.split(":")[1], + cashin_tiny_amount: + info.conversion_rate.cashin_tiny_amount.split(":")[1], cashin_fee: info.conversion_rate.cashin_fee.split(":")[1], cashin_ratio: info.conversion_rate.cashin_ratio, cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode, - cashout_min_amount: info.conversion_rate.cashout_min_amount.split(":")[1], - cashout_tiny_amount: info.conversion_rate.cashout_tiny_amount.split(":")[1], + cashout_min_amount: + info.conversion_rate.cashout_min_amount.split(":")[1], + cashout_tiny_amount: + info.conversion_rate.cashout_tiny_amount.split(":")[1], cashout_fee: info.conversion_rate.cashout_fee.split(":")[1], cashout_ratio: info.conversion_rate.cashout_ratio, cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode, - } - } + }, + }; const [form, status] = useFormState<FormType>( initalState, - createFormValidator(i18n, info.regional_currency, info.fiat_currency) - ) + createFormValidator(i18n, info.regional_currency, info.fiat_currency), + ); - const { - estimateByDebit: calculateCashoutFromDebit, - } = useCashoutEstimator(); + const { estimateByDebit: calculateCashoutFromDebit } = + useCashoutEstimator(); - const { - estimateByDebit: calculateCashinFromDebit, - } = useCashinEstimator(); + const { estimateByDebit: calculateCashinFromDebit } = useCashinEstimator(); - const [calculationResult, setCalc] = useState<{ cashin: TransferCalculation, cashout: TransferCalculation }>() + const [calculationResult, setCalc] = useState<{ + cashin: TransferCalculation; + cashout: TransferCalculation; + }>(); useEffect(() => { async function doAsync() { await handleError(async () => { if (!info) return; if (!form.amount?.value || form.amount.error) return; - const in_amount = Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`) - const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee) + const in_amount = Amounts.parseOrThrow( + `${info.fiat_currency}:${form.amount.value}`, + ); + const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee); const cashin = await calculateCashinFromDebit(in_amount, in_fee); if (cashin === "amount-is-too-small") { - setCalc(undefined) + setCalc(undefined); return; } // const out_amount = Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`) - const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee) - const cashout = await calculateCashoutFromDebit(cashin.credit, out_fee); + const out_fee = Amounts.parseOrThrow( + info.conversion_rate.cashout_fee, + ); + const cashout = await calculateCashoutFromDebit( + cashin.credit, + out_fee, + ); setCalc({ cashin, cashout }); }); } doAsync(); - }, [form.amount?.value, form.conv?.cashin_fee?.value, form.conv?.cashout_fee?.value]); - - const [section, setSection] = useState<"detail" | "cashout" | "cashin">("detail") - const cashinCalc = calculationResult?.cashin === "amount-is-too-small" ? undefined : calculationResult?.cashin - const cashoutCalc = calculationResult?.cashout === "amount-is-too-small" ? undefined : calculationResult?.cashout + }, [ + form.amount?.value, + form.conv?.cashin_fee?.value, + form.conv?.cashout_fee?.value, + ]); + + const [section, setSection] = useState<"detail" | "cashout" | "cashin">( + "detail", + ); + const cashinCalc = + calculationResult?.cashin === "amount-is-too-small" + ? undefined + : calculationResult?.cashin; + const cashoutCalc = + calculationResult?.cashout === "amount-is-too-small" + ? undefined + : calculationResult?.cashout; async function doUpdate() { - if (!creds) return + if (!creds) return; await handleError(async () => { if (status.status === "fail") return; - const resp = await conversion.updateConversionRate(creds.token, status.result.conv) + const resp = await conversion.updateConversionRate( + creds.token, + status.result.conv, + ); if (resp.type === "ok") { - setSection("detail") + setSection("detail"); } else { switch (resp.case) { case HttpStatusCode.Unauthorized: { @@ -164,6 +205,7 @@ function useComponentState({ title: i18n.str`Wrong credentials`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); } case HttpStatusCode.NotImplemented: { @@ -172,6 +214,7 @@ function useComponentState({ title: i18n.str`Conversion is disabled`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); } default: @@ -181,16 +224,16 @@ function useComponentState({ }); } - const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio) - const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio) + const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio); + const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio); const both_high = in_ratio > 1 && out_ratio > 1; const both_low = in_ratio < 1 && out_ratio < 1; - return ( <div> - <ProfileNavigation current="conversion" + <ProfileNavigation + current="conversion" routeMyAccountCashout={routeMyAccountCashout} routeMyAccountDelete={routeMyAccountDelete} routeMyAccountDetails={routeMyAccountDetails} @@ -200,7 +243,6 @@ function useComponentState({ <LocalNotificationBanner notification={notification} /> <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> - <div class="px-4 sm:px-0"> <h2 class="text-base font-semibold leading-7 text-gray-900"> <i18n.Translate>Conversion</i18n.Translate> @@ -218,7 +260,7 @@ function useComponentState({ aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { - setSection("detail") + setSection("detail"); }} /> <span class="flex flex-1"> @@ -242,7 +284,7 @@ function useComponentState({ aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => { - setSection("cashout") + setSection("cashout"); }} /> <span class="flex flex-1"> @@ -265,7 +307,7 @@ function useComponentState({ aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => { - setSection("cashin") + setSection("cashin"); }} /> <span class="flex flex-1"> @@ -277,7 +319,6 @@ function useComponentState({ </span> </label> </div> - </div> <form @@ -288,8 +329,9 @@ function useComponentState({ e.preventDefault(); }} > - {section == "cashin" && - <ConversionForm id="cashin" + {section == "cashin" && ( + <ConversionForm + id="cashin" inputCurrency={info.fiat_currency} outputCurrency={info.regional_currency} fee={form?.conv?.cashin_fee} @@ -297,682 +339,830 @@ function useComponentState({ ratio={form?.conv?.cashin_ratio} rounding={form?.conv?.cashin_rounding_mode} tiny={form?.conv?.cashin_tiny_amount} - />} - - {section == "cashout" && <Fragment> - <ConversionForm id="cashout" - inputCurrency={info.regional_currency} - outputCurrency={info.fiat_currency} - fee={form?.conv?.cashout_fee} - minimum={form?.conv?.cashout_min_amount} - ratio={form?.conv?.cashout_ratio} - rounding={form?.conv?.cashout_rounding_mode} - tiny={form?.conv?.cashout_tiny_amount} /> - </Fragment>} - - {section == "detail" && <Fragment> - <div class="px-6 pt-6"> - <div class="justify-between items-center flex "> - <dt class="text-sm text-gray-600"> - <i18n.Translate>Cashin ratio</i18n.Translate> - </dt> - <dd class="text-sm text-gray-900"> - {info.conversion_rate.cashin_ratio} - </dd> - </div> - </div> + )} + + {section == "cashout" && ( + <Fragment> + <ConversionForm + id="cashout" + inputCurrency={info.regional_currency} + outputCurrency={info.fiat_currency} + fee={form?.conv?.cashout_fee} + minimum={form?.conv?.cashout_min_amount} + ratio={form?.conv?.cashout_ratio} + rounding={form?.conv?.cashout_rounding_mode} + tiny={form?.conv?.cashout_tiny_amount} + /> + </Fragment> + )} - <div class="px-6 pt-6"> - <div class="justify-between items-center flex "> - <dt class="text-sm text-gray-600"> - <i18n.Translate>Cashout ratio</i18n.Translate> - </dt> - <dd class="text-sm text-gray-900"> - {info.conversion_rate.cashout_ratio} - </dd> + {section == "detail" && ( + <Fragment> + <div class="px-6 pt-6"> + <div class="justify-between items-center flex "> + <dt class="text-sm text-gray-600"> + <i18n.Translate>Cashin ratio</i18n.Translate> + </dt> + <dd class="text-sm text-gray-900"> + {info.conversion_rate.cashin_ratio} + </dd> + </div> </div> - </div> - {both_low || both_high ? <div class="p-4"> - <Attention title={i18n.str`Bad ratios`} type="warning"> - <i18n.Translate> - One of the ratios should be higher or equal than 1 an the other should be lower or equal than 1. - </i18n.Translate> - </Attention> - </div> : undefined} - - <div class="px-6 pt-6"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - for="amount" - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Initial amount`}</label> - <InputAmount - name="amount" - left - currency={info.fiat_currency} - value={form.amount?.value ?? ""} - onChange={form.amount?.onUpdate} - /> - <ShowInputErrorLabel - message={form.amount?.error} - isDirty={form.amount?.value !== undefined} - /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate>Use it to test how the conversion will affect the amount.</i18n.Translate> - </p> + <div class="px-6 pt-6"> + <div class="justify-between items-center flex "> + <dt class="text-sm text-gray-600"> + <i18n.Translate>Cashout ratio</i18n.Translate> + </dt> + <dd class="text-sm text-gray-900"> + {info.conversion_rate.cashout_ratio} + </dd> </div> </div> - </div> - {!cashoutCalc || !cashinCalc ? undefined : ( + {both_low || both_high ? ( + <div class="p-4"> + <Attention title={i18n.str`Bad ratios`} type="warning"> + <i18n.Translate> + One of the ratios should be higher or equal than 1 an + the other should be lower or equal than 1. + </i18n.Translate> + </Attention> + </div> + ) : undefined} + <div class="px-6 pt-6"> - <div class="sm:col-span-5"> - <dl class="mt-4 space-y-4"> - <div class="justify-between items-center flex "> - <dt class="text-sm text-gray-600"> - <i18n.Translate>Sending to this bank</i18n.Translate> - </dt> - <dd class="text-sm text-gray-900"> - <RenderAmount - value={cashinCalc.debit} - negative - withColor - spec={info.regional_currency_specification} - /> - </dd> - </div> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + for="amount" + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Initial amount`}</label> + <InputAmount + name="amount" + left + currency={info.fiat_currency} + value={form.amount?.value ?? ""} + onChange={form.amount?.onUpdate} + /> + <ShowInputErrorLabel + message={form.amount?.error} + isDirty={form.amount?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Use it to test how the conversion will affect the + amount. + </i18n.Translate> + </p> + </div> + </div> + </div> - {Amounts.isZero(cashinCalc.beforeFee) ? undefined : ( - <div class="flex items-center justify-between afu "> - <dt class="flex items-center text-sm text-gray-600"> - <span> - <i18n.Translate>Converted</i18n.Translate> - </span> + {!cashoutCalc || !cashinCalc ? undefined : ( + <div class="px-6 pt-6"> + <div class="sm:col-span-5"> + <dl class="mt-4 space-y-4"> + <div class="justify-between items-center flex "> + <dt class="text-sm text-gray-600"> + <i18n.Translate> + Sending to this bank + </i18n.Translate> </dt> <dd class="text-sm text-gray-900"> <RenderAmount - value={cashinCalc.beforeFee} - spec={info.fiat_currency_specification} + value={cashinCalc.debit} + negative + withColor + spec={info.regional_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>Cashin after fee</i18n.Translate> - </dt> - <dd class="text-lg text-gray-900 font-medium"> - <RenderAmount - value={cashinCalc.credit} - withColor - spec={info.fiat_currency_specification} - /> - </dd> - </div> - </dl> - </div> - - <div class="sm:col-span-5"> - <dl class="mt-4 space-y-4"> - <div class="justify-between items-center flex "> - <dt class="text-sm text-gray-600"> - <i18n.Translate>Sending from this bank</i18n.Translate> - </dt> - <dd class="text-sm text-gray-900"> - <RenderAmount - value={cashoutCalc.debit} - negative - withColor - spec={info.fiat_currency_specification} - /> - </dd> - </div> - {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : ( - <div class="flex items-center justify-between afu"> - <dt class="flex items-center text-sm text-gray-600"> - <span> - <i18n.Translate>Converted</i18n.Translate> - </span> + {Amounts.isZero(cashinCalc.beforeFee) ? undefined : ( + <div class="flex items-center justify-between afu "> + <dt class="flex items-center text-sm text-gray-600"> + <span> + <i18n.Translate>Converted</i18n.Translate> + </span> + </dt> + <dd class="text-sm text-gray-900"> + <RenderAmount + value={cashinCalc.beforeFee} + spec={info.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>Cashin after fee</i18n.Translate> + </dt> + <dd class="text-lg text-gray-900 font-medium"> + <RenderAmount + value={cashinCalc.credit} + withColor + spec={info.fiat_currency_specification} + /> + </dd> + </div> + </dl> + </div> + + <div class="sm:col-span-5"> + <dl class="mt-4 space-y-4"> + <div class="justify-between items-center flex "> + <dt class="text-sm text-gray-600"> + <i18n.Translate> + Sending from this bank + </i18n.Translate> </dt> <dd class="text-sm text-gray-900"> <RenderAmount - value={cashoutCalc.beforeFee} + value={cashoutCalc.debit} + negative + withColor + spec={info.fiat_currency_specification} + /> + </dd> + </div> + + {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : ( + <div class="flex items-center justify-between afu"> + <dt class="flex items-center text-sm text-gray-600"> + <span> + <i18n.Translate>Converted</i18n.Translate> + </span> + </dt> + <dd class="text-sm text-gray-900"> + <RenderAmount + value={cashoutCalc.beforeFee} + spec={info.regional_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>Cashout after fee</i18n.Translate> + </dt> + <dd class="text-lg text-gray-900 font-medium"> + <RenderAmount + value={cashoutCalc.credit} + withColor spec={info.regional_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>Cashout after fee</i18n.Translate> - </dt> - <dd class="text-lg text-gray-900 font-medium"> - <RenderAmount - value={cashoutCalc.credit} - withColor - spec={info.regional_currency_specification} - /> - </dd> + </dl> + </div> + + {cashoutCalc && + status.status === "ok" && + Amounts.cmp(status.result.amount, cashoutCalc.credit) < + 0 ? ( + <div class="p-4"> + <Attention + title={i18n.str`Bad configuration`} + type="warning" + > + <i18n.Translate> + This configuration allows users to cash out more of + what has been cashed in. + </i18n.Translate> + </Attention> </div> - </dl> + ) : undefined} </div> - - {cashoutCalc && status.status === "ok" && Amounts.cmp(status.result.amount, cashoutCalc.credit) < 0 ? <div class="p-4"> - <Attention title={i18n.str`Bad configuration`} type="warning"> - <i18n.Translate> - This configuration allows users to cash out more of what has been cashed in. - </i18n.Translate> - </Attention> - </div> : undefined} - </div> - )} - </Fragment>} - + )} + </Fragment> + )} <div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4"> - <a name="cancel" + <a + name="cancel" href={routeCancel.url({})} class="text-sm font-semibold leading-6 text-gray-900" > <i18n.Translate>Cancel</i18n.Translate> </a> - {section == "cashin" || section == "cashout" ? <Fragment> - <button - type="submit" - name="update conversion" - class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={async () => { - doUpdate() - }} - > - <i18n.Translate>Update</i18n.Translate> - </button> - </Fragment> : <div />} + {section == "cashin" || section == "cashout" ? ( + <Fragment> + <button + type="submit" + name="update conversion" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + onClick={async () => { + doUpdate(); + }} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </Fragment> + ) : ( + <div /> + )} </div> - - </form> </div> </div> ); - - } + }; } export const ConversionConfig = utils.recursive(useComponentState); /** - * - * @param i18n - * @param regional - * @param fiat + * + * @param i18n + * @param regional + * @param fiat * @returns form validator */ -function createFormValidator(i18n: InternationalizationAPI, regional: string, fiat: string) { +function createFormValidator( + i18n: InternationalizationAPI, + regional: string, + fiat: string, +) { return function check(state: FormValues<FormType>): FormStatus<FormType> { + const cashin_min_amount = Amounts.parse( + `${fiat}:${state.conv.cashin_min_amount}`, + ); + const cashin_tiny_amount = Amounts.parse( + `${regional}:${state.conv.cashin_tiny_amount}`, + ); + const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`); - const cashin_min_amount = Amounts.parse(`${fiat}:${state.conv.cashin_min_amount}`) - const cashin_tiny_amount = Amounts.parse(`${regional}:${state.conv.cashin_tiny_amount}`) - const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`) - - const cashout_min_amount = Amounts.parse(`${regional}:${state.conv.cashout_min_amount}`) - const cashout_tiny_amount = Amounts.parse(`${fiat}:${state.conv.cashout_tiny_amount}`) - const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`) + const cashout_min_amount = Amounts.parse( + `${regional}:${state.conv.cashout_min_amount}`, + ); + const cashout_tiny_amount = Amounts.parse( + `${fiat}:${state.conv.cashout_tiny_amount}`, + ); + const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`); - const am = Amounts.parse(`${fiat}:${state.amount}`) + const am = Amounts.parse(`${fiat}:${state.amount}`); - const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "") - const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "") + const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? ""); + const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? ""); const errors = undefinedIfEmpty<FormErrors<FormType>>({ conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({ - cashin_min_amount: !state.conv.cashin_min_amount ? i18n.str`required` : - !cashin_min_amount ? i18n.str`invalid` : - undefined, - cashin_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` : - !cashin_tiny_amount ? i18n.str`invalid` : - undefined, - cashin_fee: !state.conv.cashin_fee ? i18n.str`required` : - !cashin_fee ? i18n.str`invalid` : - undefined, - - cashout_min_amount: !state.conv.cashout_min_amount ? i18n.str`required` : - !cashout_min_amount ? i18n.str`invalid` : - undefined, - cashout_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` : - !cashout_tiny_amount ? i18n.str`invalid` : - undefined, - cashout_fee: !state.conv.cashin_fee ? i18n.str`required` : - !cashout_fee ? i18n.str`invalid` : - undefined, - - cashin_rounding_mode: !state.conv.cashin_rounding_mode ? i18n.str`required` : undefined, - cashout_rounding_mode: !state.conv.cashout_rounding_mode ? i18n.str`required` : undefined, - - cashin_ratio: !state.conv.cashin_ratio ? i18n.str`required` : Number.isNaN(cashin_ratio) ? i18n.str`invalid` : undefined, - cashout_ratio: !state.conv.cashout_ratio ? i18n.str`required` : Number.isNaN(cashout_ratio) ? i18n.str`invalid` : undefined, + cashin_min_amount: !state.conv.cashin_min_amount + ? i18n.str`required` + : !cashin_min_amount + ? i18n.str`invalid` + : undefined, + cashin_tiny_amount: !state.conv.cashin_tiny_amount + ? i18n.str`required` + : !cashin_tiny_amount + ? i18n.str`invalid` + : undefined, + cashin_fee: !state.conv.cashin_fee + ? i18n.str`required` + : !cashin_fee + ? i18n.str`invalid` + : undefined, + + cashout_min_amount: !state.conv.cashout_min_amount + ? i18n.str`required` + : !cashout_min_amount + ? i18n.str`invalid` + : undefined, + cashout_tiny_amount: !state.conv.cashin_tiny_amount + ? i18n.str`required` + : !cashout_tiny_amount + ? i18n.str`invalid` + : undefined, + cashout_fee: !state.conv.cashin_fee + ? i18n.str`required` + : !cashout_fee + ? i18n.str`invalid` + : undefined, + + cashin_rounding_mode: !state.conv.cashin_rounding_mode + ? i18n.str`required` + : undefined, + cashout_rounding_mode: !state.conv.cashout_rounding_mode + ? i18n.str`required` + : undefined, + + cashin_ratio: !state.conv.cashin_ratio + ? i18n.str`required` + : Number.isNaN(cashin_ratio) + ? i18n.str`invalid` + : undefined, + cashout_ratio: !state.conv.cashout_ratio + ? i18n.str`required` + : Number.isNaN(cashout_ratio) + ? i18n.str`invalid` + : undefined, }), - amount: !state.amount ? i18n.str`required` : - !am ? i18n.str`invalid` : - undefined, - }) + amount: !state.amount + ? i18n.str`required` + : !am + ? i18n.str`invalid` + : undefined, + }); const result: RecursivePartial<FormType> = { amount: am, conv: { - cashin_fee: !errors?.conv?.cashin_fee ? Amounts.stringify(cashin_fee!) : undefined, - cashin_min_amount: !errors?.conv?.cashin_min_amount ? Amounts.stringify(cashin_min_amount!) : undefined, - cashin_ratio: !errors?.conv?.cashin_ratio ? String(cashin_ratio!) : undefined, - cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? (state.conv.cashin_rounding_mode!) : undefined, - cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount ? Amounts.stringify(cashin_tiny_amount!) : undefined, - cashout_fee: !errors?.conv?.cashout_fee ? Amounts.stringify(cashout_fee!) : undefined, - cashout_min_amount: !errors?.conv?.cashout_min_amount ? Amounts.stringify(cashout_min_amount!) : undefined, - cashout_ratio: !errors?.conv?.cashout_ratio ? String(cashout_ratio!) : undefined, - cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? (state.conv.cashout_rounding_mode!) : undefined, - cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount ? Amounts.stringify(cashout_tiny_amount!) : undefined, - } - - } - return errors === undefined ? - { status: "ok", result: result as FormType, errors } : - { status: "fail", result, errors } - } + cashin_fee: !errors?.conv?.cashin_fee + ? Amounts.stringify(cashin_fee!) + : undefined, + cashin_min_amount: !errors?.conv?.cashin_min_amount + ? Amounts.stringify(cashin_min_amount!) + : undefined, + cashin_ratio: !errors?.conv?.cashin_ratio + ? String(cashin_ratio!) + : undefined, + cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode + ? state.conv.cashin_rounding_mode! + : undefined, + cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount + ? Amounts.stringify(cashin_tiny_amount!) + : undefined, + cashout_fee: !errors?.conv?.cashout_fee + ? Amounts.stringify(cashout_fee!) + : undefined, + cashout_min_amount: !errors?.conv?.cashout_min_amount + ? Amounts.stringify(cashout_min_amount!) + : undefined, + cashout_ratio: !errors?.conv?.cashout_ratio + ? String(cashout_ratio!) + : undefined, + cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode + ? state.conv.cashout_rounding_mode! + : undefined, + cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount + ? Amounts.stringify(cashout_tiny_amount!) + : undefined, + }, + }; + return errors === undefined + ? { status: "ok", result: result as FormType, errors } + : { status: "fail", result, errors }; + }; } - -function ConversionForm({ id, inputCurrency, outputCurrency, fee, minimum, ratio, rounding, tiny }: { - inputCurrency: string, - outputCurrency: string, - minimum: UIField | undefined, - tiny: UIField | undefined, - fee: UIField | undefined, - rounding: UIField | undefined, - ratio: UIField | undefined, - id: string, +function ConversionForm({ + id, + inputCurrency, + outputCurrency, + fee, + minimum, + ratio, + rounding, + tiny, +}: { + inputCurrency: string; + outputCurrency: string; + minimum: UIField | undefined; + tiny: UIField | undefined; + fee: UIField | undefined; + rounding: UIField | undefined; + ratio: UIField | undefined; + id: string; }): VNode { const { i18n } = useTranslationContext(); - return <Fragment> - <div class="px-6 pt-6"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - for="cashin_min_amount" - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Minimum amount`}</label> - <InputAmount - name="cashin_min_amount" - left - currency={inputCurrency} - value={minimum?.value ?? ""} - onChange={minimum?.onUpdate} + return ( + <Fragment> + <div class="px-6 pt-6"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + for={`${id}_min_amount`} + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Minimum amount`}</label> + <InputAmount + name={`${id}_min_amount`} + left + currency={inputCurrency} + value={minimum?.value ?? ""} + onChange={minimum?.onUpdate} + /> + <ShowInputErrorLabel + message={minimum?.error} + isDirty={minimum?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Only cashout operation above this threshold will be allowed + </i18n.Translate> + </p> + </div> + </div> + </div> + + <div class="px-6 pt-6"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for={`${id}_ratio`} + > + {i18n.str`Ratio`} + </label> + <div class="mt-2"> + <input + type="number" + class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + name="current" + id={`${id}_ratio`} + data-error={!!ratio?.error && ratio?.value !== undefined} + value={ratio?.value ?? ""} + onChange={(e) => { + ratio?.onUpdate(e.currentTarget.value); + }} + autocomplete="off" /> <ShowInputErrorLabel - message={minimum?.error} - isDirty={minimum?.value !== undefined} + message={ratio?.error} + isDirty={ratio?.value !== undefined} /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate>Only cashout operation above this threshold will be allowed</i18n.Translate> - </p> </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Conversion ratio between currencies</i18n.Translate> + </p> </div> - </div> - - <div class="px-6 pt-6"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="password" - > - {i18n.str`Ratio`} - </label> - <div class="mt-2"> - <input - type="number" - class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - name="current" - id="cashin_ratio" - data-error={!!ratio?.error && ratio?.value !== undefined} - value={ratio?.value ?? ""} - onChange={(e) => { - ratio?.onUpdate(e.currentTarget.value); - }} - autocomplete="off" - /> - <ShowInputErrorLabel - message={ratio?.error} - isDirty={ratio?.value !== undefined} - /> + + <div class="px-6 pt-4"> + <Attention title={i18n.str`Example conversion`}> + <i18n.Translate> + 1 {inputCurrency} will be converted into {ratio?.value}{" "} + {outputCurrency} + </i18n.Translate> + </Attention> </div> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate> - Conversion ratio between currencies - </i18n.Translate> - </p> - </div> - - <div class="px-6 pt-4"> - <Attention title={i18n.str`Example conversion`}> - <i18n.Translate>1 {inputCurrency} will be converted into {ratio?.value} {outputCurrency}</i18n.Translate> - </Attention> - </div> - - <div class="px-6 pt-6"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - for="cashin_tiny_amount" - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Rounding value`}</label> - <InputAmount - name="cashin_tiny_amount" - left - currency={outputCurrency} - value={tiny?.value ?? ""} - onChange={tiny?.onUpdate} - /> - <ShowInputErrorLabel - message={tiny?.error} - isDirty={tiny?.value !== undefined} - /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate>Smallest difference between two amounts after the ratio is applied.</i18n.Translate> - </p> + + <div class="px-6 pt-6"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + for={`${id}_tiny_amount`} + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Rounding value`}</label> + <InputAmount + name={`${id}_tiny_amount`} + left + currency={outputCurrency} + value={tiny?.value ?? ""} + onChange={tiny?.onUpdate} + /> + <ShowInputErrorLabel + message={tiny?.error} + isDirty={tiny?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Smallest difference between two amounts after the ratio is + applied. + </i18n.Translate> + </p> + </div> </div> </div> - </div> - - <div class="px-6 pt-6"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="channel" - > - {i18n.str`Rounding mode`} - </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={(e) => { - e.preventDefault(); - rounding?.onUpdate("zero") - }} - data-selected={rounding?.value === "zero"} - class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 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>Zero</i18n.Translate> + + <div class="px-6 pt-6"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for={`${id}_channel`} + > + {i18n.str`Rounding mode`} + </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={(e) => { + e.preventDefault(); + rounding?.onUpdate("zero"); + }} + data-selected={rounding?.value === "zero"} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Zero</i18n.Translate> + </span> + <i18n.Translate> + Amount will be round below to the largest possible value + smaller than the input. + </i18n.Translate> </span> - <i18n.Translate>Amount will be round below to the largest possible value smaller than the input.</i18n.Translate> </span> - </span> + <svg + data-selected={rounding?.value === "zero"} + 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={(e) => { + e.preventDefault(); + rounding?.onUpdate("up"); + }} + data-selected={rounding?.value === "up"} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Up</i18n.Translate> + </span> + <i18n.Translate> + Amount will be round up to the smallest possible value + larger than the input. + </i18n.Translate> + </span> + </span> + <svg + data-selected={rounding?.value === "up"} + 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={(e) => { + e.preventDefault(); + rounding?.onUpdate("nearest"); + }} + data-selected={rounding?.value === "nearest"} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Nearest</i18n.Translate> + </span> + <i18n.Translate> + Amount will be round to the closest possible value. + </i18n.Translate> + </span> + </span> + <svg + data-selected={rounding?.value === "nearest"} + 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> + + <div class="px-6 pt-4"> + <Attention title={i18n.str`Examples`}> + <section class="grid grid-cols-1 gap-y-3 text-gray-600"> + <details class="group text-sm"> + <summary class="flex cursor-pointer flex-row items-center justify-between "> + <i18n.Translate> + Rounding an amount of 1.24 with rounding value 0.1 + </i18n.Translate> <svg - data-selected={rounding?.value === "zero"} - class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" - viewBox="0 0 20 20" - fill="currentColor" + class="h-6 w-6 rotate-0 transform group-open:rotate-180" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="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" - /> + stroke-linecap="round" + stroke-linejoin="round" + d="M19 9l-7 7-7-7" + ></path> </svg> - </label> - - <label - onClick={(e) => { - e.preventDefault(); - rounding?.onUpdate("up") - }} - data-selected={rounding?.value === "up"} - class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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-0-label" - class="block text-sm font-medium text-gray-900 " - > - <i18n.Translate>Up</i18n.Translate> - </span> - <i18n.Translate>Amount will be round up to the smallest possible value larger than the input.</i18n.Translate> - </span> - </span> + </summary> + <p class="text-gray-900 my-4"> + <i18n.Translate> + Given the rounding value of 0.1 the possible values closest to + 1.24 are: 1.1, 1.2, 1.3, 1.4. + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "zero" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "nearest" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 mt-4"> + <i18n.Translate> + With the "up" mode the value will be rounded to 1.3 + </i18n.Translate> + </p> + </details> + <details class="group "> + <summary class="flex cursor-pointer flex-row items-center justify-between "> + <i18n.Translate> + Rounding an amount of 1.26 with rounding value 0.1 + </i18n.Translate> <svg - data-selected={rounding?.value === "up"} - class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" - viewBox="0 0 20 20" - fill="currentColor" + class="h-6 w-6 rotate-0 transform group-open:rotate-180" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="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" - /> + stroke-linecap="round" + stroke-linejoin="round" + d="M19 9l-7 7-7-7" + ></path> </svg> - </label> - <label - onClick={(e) => { - e.preventDefault(); - rounding?.onUpdate("nearest") - }} - data-selected={rounding?.value === "nearest"} - class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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-0-label" - class="block text-sm font-medium text-gray-900 " - > - <i18n.Translate>Nearest</i18n.Translate> - </span> - <i18n.Translate>Amount will be round to the closest possible value.</i18n.Translate> - </span> - </span> + </summary> + <p class="text-gray-900 my-4"> + <i18n.Translate> + Given the rounding value of 0.1 the possible values closest to + 1.24 are: 1.1, 1.2, 1.3, 1.4. + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "zero" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "nearest" mode the value will be rounded to 1.3 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "up" mode the value will be rounded to 1.3 + </i18n.Translate> + </p> + </details> + <details class="group "> + <summary class="flex cursor-pointer flex-row items-center justify-between "> + <i18n.Translate> + Rounding an amount of 1.24 with rounding value 0.3 + </i18n.Translate> <svg - data-selected={rounding?.value === "nearest"} - class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" - viewBox="0 0 20 20" - fill="currentColor" + class="h-6 w-6 rotate-0 transform group-open:rotate-180" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="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" - /> + stroke-linecap="round" + stroke-linejoin="round" + d="M19 9l-7 7-7-7" + ></path> </svg> - </label> - </div> - </div> - </div> + </summary> + <p class="text-gray-900 my-4"> + <i18n.Translate> + Given the rounding value of 0.3 the possible values closest to + 1.24 are: 0.9, 1.2, 1.5, 1.8. + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "zero" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "nearest" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "up" mode the value will be rounded to 1.5 + </i18n.Translate> + </p> + </details> + <details class="group "> + <summary class="flex cursor-pointer flex-row items-center justify-between "> + <i18n.Translate> + Rounding an amount of 1.26 with rounding value 0.3 + </i18n.Translate> + <svg + class="h-6 w-6 rotate-0 transform group-open:rotate-180" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M19 9l-7 7-7-7" + ></path> + </svg> + </summary> + <p class="text-gray-900 my-4"> + <i18n.Translate> + Given the rounding value of 0.3 the possible values closest to + 1.24 are: 0.9, 1.2, 1.5, 1.8. + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "zero" mode the value will be rounded to 1.2 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "nearest" mode the value will be rounded to 1.3 + </i18n.Translate> + </p> + <p class="text-gray-900 my-4"> + <i18n.Translate> + With the "up" mode the value will be rounded to 1.3 + </i18n.Translate> + </p> + </details> + </section> + </Attention> </div> - </div> - <div class="px-6 pt-4"> - <Attention title={i18n.str`Examples`}> - <section class="grid grid-cols-1 gap-y-3 text-gray-600"> - <details class="group text-sm"> - <summary class="flex cursor-pointer flex-row items-center justify-between "> + <div class="px-6 pt-6"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + for={`${id}_fee`} + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Fee`}</label> + <InputAmount + name={`${id}_fee`} + left + currency={outputCurrency} + value={fee?.value ?? ""} + onChange={fee?.onUpdate} + /> + <ShowInputErrorLabel + message={fee?.error} + isDirty={fee?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> <i18n.Translate> - Rounding an amount of 1.24 with rounding value 0.1 - </i18n.Translate> - <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> - </svg> - </summary> - <p class="text-gray-900 my-4"> - <i18n.Translate> - Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4. - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "zero" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "nearest" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 mt-4"> - <i18n.Translate> - With the "up" mode the value will be rounded to 1.3 - </i18n.Translate> - </p> - </details> - <details class="group "> - <summary class="flex cursor-pointer flex-row items-center justify-between "> - <i18n.Translate> - Rounding an amount of 1.26 with rounding value 0.1 - </i18n.Translate> - <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> - </svg> - </summary> - <p class="text-gray-900 my-4"> - <i18n.Translate> - Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4. - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "zero" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "nearest" mode the value will be rounded to 1.3 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "up" mode the value will be rounded to 1.3 - </i18n.Translate> - </p> - </details> - <details class="group "> - <summary class="flex cursor-pointer flex-row items-center justify-between "> - <i18n.Translate> - Rounding an amount of 1.24 with rounding value 0.3 - </i18n.Translate> - <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> - </svg> - </summary> - <p class="text-gray-900 my-4"> - <i18n.Translate> - Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8. - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "zero" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "nearest" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "up" mode the value will be rounded to 1.5 - </i18n.Translate> - </p> - </details> - <details class="group "> - <summary class="flex cursor-pointer flex-row items-center justify-between "> - <i18n.Translate> - Rounding an amount of 1.26 with rounding value 0.3 - </i18n.Translate> - <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> - </svg> - </summary> - <p class="text-gray-900 my-4"> - <i18n.Translate> - Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8. + Amount to be deducted before amount is credited. </i18n.Translate> </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "zero" mode the value will be rounded to 1.2 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "nearest" mode the value will be rounded to 1.3 - </i18n.Translate> - </p> - <p class="text-gray-900 my-4"> - <i18n.Translate> - With the "up" mode the value will be rounded to 1.3 - </i18n.Translate> - </p> - </details> - </section> - </Attention> - </div> - - - - <div class="px-6 pt-6"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> - <label - for="cashin_fee" - class="block text-sm font-medium leading-6 text-gray-900" - >{i18n.str`Fee`}</label> - <InputAmount - name="cashin_fee" - left - currency={outputCurrency} - value={fee?.value ?? ""} - onChange={fee?.onUpdate} - /> - <ShowInputErrorLabel - message={fee?.error} - isDirty={fee?.value !== undefined} - /> - <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate>Amount to be deducted before amount is credited.</i18n.Translate> - </p> + </div> </div> </div> - </div> - - </Fragment> + </Fragment> + ); } diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx index 2f15d16b4..a76179b4d 100644 --- a/packages/bank-ui/src/pages/regional/CreateCashout.tsx +++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -39,9 +39,13 @@ import { useEffect, useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; import { VersionHint, useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/account.js"; -import { useSessionState } from "../../hooks/session.js"; import { useBankState } from "../../hooks/bank-state.js"; -import { TransferCalculation, useCashoutEstimator, useConversionInfo, useEstimator } from "../../hooks/regional.js"; +import { + TransferCalculation, + useCashoutEstimator, + useConversionInfo, +} from "../../hooks/regional.js"; +import { useSessionState } from "../../hooks/session.js"; import { RouteDefinition } from "../../route.js"; import { TanChannel, undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; @@ -141,11 +145,11 @@ export function CreateCashout({ switch (info.case) { case HttpStatusCode.NotImplemented: { return ( - <Attention - type="danger" - title={i18n.str`Cashout are disabled`} - > - <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate> + <Attention type="danger" title={i18n.str`Cashout are disabled`}> + <i18n.Translate> + Cashout should be enable by configuration and the conversion rate + should be initialized with fee, ratio and rounding mode. + </i18n.Translate> </Attention> ); } @@ -185,7 +189,8 @@ export function CreateCashout({ credit: fiatZero, beforeFee: fiatZero, }; - const [calculationResult, setCalculation] = useState<TransferCalculation>(zeroCalc); + const [calculationResult, setCalculation] = + useState<TransferCalculation>(zeroCalc); const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee); const sellRate = conversionInfo.cashout_ratio; /** @@ -193,30 +198,33 @@ export function CreateCashout({ * depending on the isDebit flag */ const inputAmount = Amounts.parseOrThrow( - `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount + `${form.isDebit ? regional_currency : fiat_currency}:${ + !form.amount ? "0" : form.amount }`, ); useEffect(() => { async function doAsync() { await handleError(async () => { - const higerThanMin = form.isDebit ? - Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1 : true; - const notZero = Amounts.isNonZero(inputAmount) + const higerThanMin = form.isDebit + ? Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1 + : true; + const notZero = Amounts.isNonZero(inputAmount); if (notZero && higerThanMin) { const resp = await (form.isDebit ? calculateFromDebit(inputAmount, sellFee) : calculateFromCredit(inputAmount, sellFee)); setCalculation(resp); } else { - setCalculation(zeroCalc) + setCalculation(zeroCalc); } }); } doAsync(); }, [form.amount, form.isDebit]); - const calc = calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult + const calc = + calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult; const balanceAfter = Amounts.sub(account.balance, calc.debit).amount; @@ -231,8 +239,14 @@ export function CreateCashout({ ? i18n.str`Invalid` : Amounts.cmp(limit, calc.debit) === -1 ? i18n.str`Balance is not enough` - : form.isDebit && Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1 - ? i18n.str`Needs to be higher than ${Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(conversionInfo.cashout_min_amount), regional_currency_specification).normal}` + : form.isDebit && + Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1 + ? i18n.str`Needs to be higher than ${ + Amounts.stringifyValueWithSpec( + Amounts.parseOrThrow(conversionInfo.cashout_min_amount), + regional_currency_specification, + ).normal + }` : calculationResult === "amount-is-too-small" ? i18n.str`Amount needs to be higher` : Amounts.isZero(calc.credit) @@ -280,6 +294,7 @@ export function CreateCashout({ title: i18n.str`Account not found`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: return notify({ @@ -287,6 +302,7 @@ export function CreateCashout({ title: i18n.str`Duplicated request detected, check if the operation succeeded or try again.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case TalerErrorCode.BANK_BAD_CONVERSION: return notify({ @@ -294,6 +310,7 @@ export function CreateCashout({ title: i18n.str`The conversion rate was incorrectly applied`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ @@ -301,6 +318,7 @@ export function CreateCashout({ title: i18n.str`The account does not have sufficient funds`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case HttpStatusCode.NotImplemented: return notify({ @@ -308,6 +326,7 @@ export function CreateCashout({ title: i18n.str`Cashout are disabled`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({ @@ -315,6 +334,7 @@ export function CreateCashout({ title: i18n.str`Missing cashout URI in the profile`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({ @@ -322,6 +342,7 @@ export function CreateCashout({ title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); } assertUnreachable(resp); @@ -406,7 +427,10 @@ export function CreateCashout({ <dd class="text-sm text-gray-900">{cashoutLegalName}</dd> </div> <p class="mt-2 text-sm text-gray-500"> - <i18n.Translate>If this name doesn't match the account holder's name your transaction may fail.</i18n.Translate> + <i18n.Translate> + If this name doesn't match the account holder's name your + transaction may fail. + </i18n.Translate> </p> </Fragment> ) : ( @@ -482,7 +506,7 @@ export function CreateCashout({ updateForm(structuredClone(form)); }} > - {form.isDebit ? + {form.isDebit ? ( <svg class="self-center flex-none h-5 w-5 text-indigo-600" viewBox="0 0 20 20" @@ -495,12 +519,17 @@ export function CreateCashout({ clip-rule="evenodd" /> </svg> - - : - <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> + ) : ( + <svg + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> </svg> - } + )} <i18n.Translate>Send {regional_currency}</i18n.Translate> </button> @@ -514,7 +543,7 @@ export function CreateCashout({ updateForm(structuredClone(form)); }} > - {!form.isDebit ? + {!form.isDebit ? ( <svg class="self-center flex-none h-5 w-5 text-indigo-600" viewBox="0 0 20 20" @@ -527,12 +556,17 @@ export function CreateCashout({ clip-rule="evenodd" /> </svg> - - : - <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> + ) : ( + <svg + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> </svg> - } + )} <i18n.Translate>Receive {fiat_currency}</i18n.Translate> </button> @@ -579,9 +613,9 @@ export function CreateCashout({ cashoutDisabled ? undefined : (value) => { - form.amount = value; - updateForm(structuredClone(form)); - } + form.amount = value; + updateForm(structuredClone(form)); + } } /> <ShowInputErrorLabel @@ -622,7 +656,7 @@ export function CreateCashout({ </dd> </div> {Amounts.isZero(sellFee) || - Amounts.isZero(calc.beforeFee) ? undefined : ( + Amounts.isZero(calc.beforeFee) ? undefined : ( <div class="flex items-center justify-between border-t-2 afu pt-4"> <dt class="flex items-center text-sm text-gray-600"> <span> @@ -655,7 +689,7 @@ export function CreateCashout({ {/* channel, not shown if new cashout api */} {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels - .length === 0 ? ( + .length === 0 ? ( <div class="sm:col-span-5"> <Attention type="warning" @@ -727,7 +761,7 @@ export function CreateCashout({ )} {config.supported_tan_channels.indexOf(TanChannel.SMS) === - -1 ? undefined : ( + -1 ? undefined : ( <label onClick={() => { if (!resultAccount.body.contact_data?.phone) return; @@ -803,7 +837,7 @@ export function CreateCashout({ </button> </div> </form> - </div > - </div > + </div> + </div> ); } diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx index 415f88868..3f635db7e 100644 --- a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx +++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx @@ -16,7 +16,6 @@ import { AbsoluteTime, Amounts, - Duration, HttpStatusCode, TalerError, assertUnreachable, @@ -26,20 +25,19 @@ import { Loading, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; import { VNode, h } from "preact"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; +import { Time } from "../../components/Time.js"; import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js"; import { RouteDefinition } from "../../route.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; -import { Time } from "../../components/Time.js"; interface Props { id: string; routeClose: RouteDefinition; } export function ShowCashoutDetails({ id, routeClose }: Props): VNode { - const { i18n, dateLocale } = useTranslationContext(); + const { i18n } = useTranslationContext(); const cid = Number.parseInt(id, 10); const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid); @@ -70,11 +68,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { ); case HttpStatusCode.NotImplemented: return ( - <Attention - type="warning" - title={i18n.str`Cashout are disabled`} - > - <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate> + <Attention type="warning" title={i18n.str`Cashout are disabled`}> + <i18n.Translate> + Cashout should be enable by configuration and the conversion rate + should be initialized with fee, ratio and rounding mode. + </i18n.Translate> </Attention> ); default: @@ -92,10 +90,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { switch (info.case) { case HttpStatusCode.NotImplemented: { return ( - <Attention type="danger" - title={i18n.str`Cashout are disabled`} - > - <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate> + <Attention type="danger" title={i18n.str`Cashout are disabled`}> + <i18n.Translate> + Cashout should be enable by configuration and the conversion rate + should be initialized with fee, ratio and rounding mode. + </i18n.Translate> </Attention> ); } @@ -134,9 +133,12 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { <i18n.Translate>Created</i18n.Translate> </dt> <dd class="text-sm "> - <Time format="dd/MM/yyyy HH:mm:ss" - timestamp={AbsoluteTime.fromProtocolTimestamp(result.body.creation_time)} - // relative={Duration.fromSpec({ days: 1 })} + <Time + format="dd/MM/yyyy HH:mm:ss" + timestamp={AbsoluteTime.fromProtocolTimestamp( + result.body.creation_time, + )} + // relative={Duration.fromSpec({ days: 1 })} /> </dd> </div> |