diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/business')
-rw-r--r-- | packages/demobank-ui/src/pages/business/CreateCashout.tsx | 544 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx | 136 |
2 files changed, 426 insertions, 254 deletions
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 254a0d81f..93bd2c89d 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -17,13 +17,13 @@ import { AbsoluteTime, Amounts, HttpStatusCode, - TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString, + assertUnreachable, encodeCrock, getRandomBytes, - parsePaytoUri + parsePaytoUri, } from "@gnu-taler/taler-util"; import { Attention, @@ -32,7 +32,7 @@ import { ShowInputErrorLabel, notifyInfo, useLocalNotification, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -40,24 +40,22 @@ import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js import { VersionHint, useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; -import { - useConversionInfo, - useEstimator -} from "../../hooks/circuit.js"; -import { - TanChannel, - undefinedIfEmpty -} from "../../utils.js"; -import { LoginForm } from "../LoginForm.js"; -import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { useBankState } from "../../hooks/bank-state.js"; +import { useConversionInfo, useEstimator } from "../../hooks/circuit.js"; +import { RouteDefinition } from "../../route.js"; +import { TanChannel, undefinedIfEmpty } from "../../utils.js"; +import { LoginForm } from "../LoginForm.js"; +import { + InputAmount, + RenderAmount, + doAutoFocus, +} from "../PaytoWireTransferForm.js"; interface Props { account: string; - focus?: boolean, - onAuthorizationRequired: () => void, - onCancel?: () => void; + focus?: boolean; + onAuthorizationRequired: () => void; + routeClose: RouteDefinition<Record<string, never>>; } type FormType = { @@ -70,12 +68,11 @@ type ErrorFrom<T> = { [P in keyof T]+?: string; }; - export function CreateCashout({ account: accountName, onAuthorizationRequired, focus, - onCancel, + routeClose, }: Props): VNode { const { i18n } = useTranslationContext(); const resultAccount = useAccountDetails(accountName); @@ -84,95 +81,130 @@ export function CreateCashout({ estimateByDebit: calculateFromDebit, } = useEstimator(); const { state: credentials } = useBackendState(); - const creds = credentials.status !== "loggedIn" ? undefined : credentials - const [, updateBankState] = useBankState() + const creds = credentials.status !== "loggedIn" ? undefined : credentials; + const [, updateBankState] = useBankState(); - const { api, config, hints } = useBankCoreApiContext() - const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, }); - const [notification, notify, handleError] = useLocalNotification() + const { api, config, hints } = useBankCoreApiContext(); + const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); + const [notification, notify, handleError] = useLocalNotification(); const info = useConversionInfo(); if (!config.allow_conversion) { - return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}> - <i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate> - </Attention> + return ( + <Fragment> + <Attention type="warning" title={i18n.str`Unable to create a cashout`}> + <i18n.Translate> + The bank configuration does not support cashout operations. + </i18n.Translate> + </Attention> + <div class="mt-5 sm:mt-6"> + <a + href={routeClose.url({})} + class="inline-flex w-full justify-center 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" + > + <i18n.Translate>Close</i18n.Translate> + </a> + </div> + </Fragment> + ); } - const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1 + const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1; if (!resultAccount) { - return <Loading /> + return <Loading />; } if (resultAccount instanceof TalerError) { - return <ErrorLoadingWithDebug error={resultAccount} /> + return <ErrorLoadingWithDebug error={resultAccount} />; } if (resultAccount.type === "fail") { switch (resultAccount.case) { - case HttpStatusCode.Unauthorized: return <LoginForm currentUser={accountName} /> - case HttpStatusCode.NotFound: return <LoginForm currentUser={accountName} /> - default: assertUnreachable(resultAccount) + case HttpStatusCode.Unauthorized: + return <LoginForm currentUser={accountName} />; + case HttpStatusCode.NotFound: + return <LoginForm currentUser={accountName} />; + default: + assertUnreachable(resultAccount); } } if (!info) { - return <Loading /> + return <Loading />; } if (info instanceof TalerError) { - return <ErrorLoadingWithDebug error={info} /> + return <ErrorLoadingWithDebug error={info} />; } if (info.type === "fail") { switch (info.case) { case HttpStatusCode.NotImplemented: { - return <Attention type="danger" title={i18n.str`Cashout not implemented`}> - </Attention>; + return ( + <Attention + type="danger" + title={i18n.str`Cashout not implemented`} + ></Attention> + ); } - default: assertUnreachable(info.case) + default: + assertUnreachable(info.case); } } - - const conversionInfo = info.body.conversion_rate + const conversionInfo = info.body.conversion_rate; if (!conversionInfo) { - return <div>conversion enabled but server replied without conversion_rate</div> + return ( + <div>conversion enabled but server replied without conversion_rate</div> + ); } const account = { balance: Amounts.parseOrThrow(resultAccount.body.balance.amount), - balanceIsDebit: resultAccount.body.balance.credit_debit_indicator == "debit", - debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold) - } + balanceIsDebit: + resultAccount.body.balance.credit_debit_indicator == "debit", + debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold), + }; - const { fiat_currency, regional_currency, fiat_currency_specification, regional_currency_specification } = 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 ? Amounts.sub(account.debitThreshold, account.balance).amount : Amounts.add(account.balance, account.debitThreshold).amount; - const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: fiatZero }; + const zeroCalc = { + debit: regionalZero, + credit: fiatZero, + beforeFee: fiatZero, + }; const [calc, setCalc] = useState(zeroCalc); const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee); - const sellRate = conversionInfo.cashout_ratio + const sellRate = conversionInfo.cashout_ratio; /** * can be in regional currency or fiat currency * 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 () => { if (Amounts.isNonZero(inputAmount)) { - const resp = await (form.isDebit ? - calculateFromDebit(inputAmount, sellFee) : - calculateFromCredit(inputAmount, sellFee)); - setCalc(resp) + const resp = await (form.isDebit + ? calculateFromDebit(inputAmount, sellFee) + : calculateFromCredit(inputAmount, sellFee)); + setCalc(resp); } - }) + }); } - doAsync() + doAsync(); }, [form.amount, form.isDebit]); const balanceAfter = Amounts.sub(account.balance, calc.debit).amount; @@ -198,10 +230,13 @@ export function CreateCashout({ const trimmedAmountStr = form.amount?.trim(); async function createCashout() { - const request_uid = encodeCrock(getRandomBytes(32)) + const request_uid = encodeCrock(getRandomBytes(32)); await handleError(async () => { - //new cashout api doesn't require channel - const validChannel = !OLD_CASHOUT_API || config.supported_tan_channels.length === 0 || form.channel + // new cashout api doesn't require channel + const validChannel = + !OLD_CASHOUT_API || + config.supported_tan_channels.length === 0 || + form.channel; if (!creds || !form.subject || !validChannel) return; const request = { @@ -210,10 +245,10 @@ export function CreateCashout({ amount_debit: Amounts.stringify(calc.debit), subject: form.subject, tan_channel: form.channel, - } - const resp = await api.createCashout(creds, request) + }; + const resp = await api.createCashout(creds, request); if (resp.type === "ok") { - notifyInfo(i18n.str`Cashout created`) + notifyInfo(i18n.str`Cashout created`); } else { switch (resp.case) { case HttpStatusCode.Accepted: { @@ -222,102 +257,127 @@ export function CreateCashout({ id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request, - }) - return onAuthorizationRequired() + }); + return onAuthorizationRequired(); } - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`Account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: 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 TalerErrorCode.BANK_BAD_CONVERSION: return notify({ - type: "error", - title: i18n.str`The conversion rate was incorrectly applied`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ - type: "error", - title: i18n.str`The account does not have sufficient funds`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case HttpStatusCode.NotImplemented: return notify({ - type: "error", - title: i18n.str`Cashouts are not supported`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({ - type: "error", - title: i18n.str`Missing cashout URI in the profile`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({ - type: "error", - title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: + 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 TalerErrorCode.BANK_BAD_CONVERSION: + return notify({ + type: "error", + title: i18n.str`The conversion rate was incorrectly applied`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return notify({ + type: "error", + title: i18n.str`The account does not have sufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotImplemented: + return notify({ + type: "error", + title: i18n.str`Cashouts are not supported`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return notify({ + type: "error", + title: i18n.str`Missing cashout URI in the profile`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: + return notify({ + type: "error", + title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); } - assertUnreachable(resp) + assertUnreachable(resp); } - }) + }); } - const cashoutDisabled = config.supported_tan_channels.length < 1 || !resultAccount.body.cashout_payto_uri - console.log("disab", cashoutDisabled) - const cashoutAccount = !resultAccount.body.cashout_payto_uri ? undefined : - parsePaytoUri(resultAccount.body.cashout_payto_uri); - const cashoutAccountName = !cashoutAccount ? undefined : cashoutAccount.targetPath + const cashoutDisabled = + config.supported_tan_channels.length < 1 || + !resultAccount.body.cashout_payto_uri; + console.log("disab", cashoutDisabled); + const cashoutAccount = !resultAccount.body.cashout_payto_uri + ? undefined + : parsePaytoUri(resultAccount.body.cashout_payto_uri); + const cashoutAccountName = !cashoutAccount + ? undefined + : cashoutAccount.targetPath; return ( <div> <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"> - <section class="mt-4 rounded-sm px-4 py-6 p-8 "> - <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout</i18n.Translate></h2> + <h2 id="summary-heading" class="font-medium text-lg"> + <i18n.Translate>Cashout</i18n.Translate> + </h2> <dl class="mt-4 space-y-4"> <div class="justify-between items-center flex"> - <dt class="text-sm text-gray-600"><i18n.Translate>Convertion rate</i18n.Translate></dt> + <dt class="text-sm text-gray-600"> + <i18n.Translate>Convertion rate</i18n.Translate> + </dt> <dd class="text-sm text-gray-900">{sellRate}</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>Balance</i18n.Translate></span> + <span> + <i18n.Translate>Balance</i18n.Translate> + </span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={account.balance} spec={regional_currency_specification} /> + <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>Fee</i18n.Translate></span> + <span> + <i18n.Translate>Fee</i18n.Translate> + </span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={sellFee} spec={fiat_currency_specification} /> + <RenderAmount + value={sellFee} + spec={fiat_currency_specification} + /> </dd> </div> - {cashoutAccountName ? + {cashoutAccountName ? ( <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>To account</i18n.Translate></span> + <span> + <i18n.Translate>To account</i18n.Translate> + </span> </dt> - <dd class="text-sm text-gray-900"> - {cashoutAccountName} - </dd> - </div> : + <dd class="text-sm text-gray-900">{cashoutAccountName}</dd> + </div> + ) : ( <div class="flex items-center justify-between border-t-2 afu pt-4"> <Attention type="warning" title={i18n.str`No cashout account`}> <i18n.Translate> @@ -325,17 +385,15 @@ export function CreateCashout({ </i18n.Translate> </Attention> </div> - } - + )} </dl> - </section> <form class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" autoCapitalize="none" autoCorrect="off" - onSubmit={e => { - e.preventDefault() + onSubmit={(e) => { + e.preventDefault(); }} > <div class="px-4 py-6 sm:p-8"> @@ -370,7 +428,6 @@ export function CreateCashout({ isDirty={form.subject !== undefined} /> </div> - </div> {/* amount */} @@ -384,14 +441,25 @@ export function CreateCashout({ ? 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" + <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> + 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 @@ -399,53 +467,78 @@ export function CreateCashout({ left currency={limit.currency} value={trimmedAmountStr} - onChange={cashoutDisabled ? undefined : (value) => { - form.amount = value; - updateForm(structuredClone(form)); - }} + onChange={ + cashoutDisabled + ? undefined + : (value) => { + form.amount = value; + updateForm(structuredClone(form)); + } + } /> <ShowInputErrorLabel message={errors?.amount} isDirty={form.amount !== undefined} /> </div> - </div> {Amounts.isZero(calc.credit) ? undefined : ( <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>Total cost</i18n.Translate></dt> + <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 spec={regional_currency_specification} /> + <RenderAmount + value={calc.debit} + negative + withColor + 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>Balance left</i18n.Translate></span> + <span> + <i18n.Translate>Balance left</i18n.Translate> + </span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={balanceAfter} spec={regional_currency_specification} /> + <RenderAmount + value={balanceAfter} + spec={regional_currency_specification} + /> </dd> </div> - {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : ( + {Amounts.isZero(sellFee) || + 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><i18n.Translate>Before fee</i18n.Translate></span> + <span> + <i18n.Translate>Before fee</i18n.Translate> + </span> </dt> <dd class="text-sm text-gray-900"> - <RenderAmount value={calc.beforeFee} spec={fiat_currency_specification} /> + <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> + <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 spec={fiat_currency_specification} /> + <RenderAmount + value={calc.credit} + withColor + spec={fiat_currency_specification} + /> </dd> </div> </dl> @@ -453,15 +546,20 @@ export function CreateCashout({ )} {/* channel, not shown if new cashout api */} - {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ? + {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels + .length === 0 ? ( <div class="sm:col-span-5"> - <Attention type="warning" title={i18n.str`No cashout channel available`}> + <Attention + type="warning" + title={i18n.str`No cashout channel available`} + > <i18n.Translate> - Before doing a cashout the server need to provide an second channel to confirm the operation + Before doing a cashout the server need to provide an + second channel to confirm the operation </i18n.Translate> </Attention> </div> - : + ) : ( <div class="sm:col-span-5"> <label class="block text-sm font-medium leading-6 text-gray-900" @@ -471,72 +569,124 @@ export function CreateCashout({ </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"> - {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined : - <label onClick={() => { - if (!resultAccount.body.contact_data?.email) return; - form.channel = TanChannel.EMAIL - updateForm(structuredClone(form)) - }} data-disabled={!resultAccount.body.contact_data?.email} data-selected={form.channel === TanChannel.EMAIL} - 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" /> + {config.supported_tan_channels.indexOf( + TanChannel.EMAIL, + ) === -1 ? undefined : ( + <label + onClick={() => { + if (!resultAccount.body.contact_data?.email) return; + form.channel = TanChannel.EMAIL; + updateForm(structuredClone(form)); + }} + data-disabled={ + !resultAccount.body.contact_data?.email + } + data-selected={form.channel === TanChannel.EMAIL} + 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 "> + <span + id="project-type-0-label" + class="block text-sm font-medium text-gray-900 " + > <i18n.Translate>Email</i18n.Translate> </span> - {!resultAccount.body.contact_data?.email && i18n.str`add a email in your profile to enable this option`} + {!resultAccount.body.contact_data?.email && + i18n.str`add a email in your profile to enable this option`} </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 + 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> - } - - {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined : - <label onClick={() => { - if (!resultAccount.body.contact_data?.phone) return; - form.channel = TanChannel.SMS - updateForm(structuredClone(form)) - }} data-disabled={!resultAccount.body.contact_data?.phone} data-selected={form.channel === TanChannel.SMS} - 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" /> + )} + + {config.supported_tan_channels.indexOf(TanChannel.SMS) === + -1 ? undefined : ( + <label + onClick={() => { + if (!resultAccount.body.contact_data?.phone) return; + form.channel = TanChannel.SMS; + updateForm(structuredClone(form)); + }} + data-disabled={ + !resultAccount.body.contact_data?.phone + } + data-selected={form.channel === TanChannel.SMS} + 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-1-label" class="block text-sm font-medium text-gray-900"> + <span + id="project-type-1-label" + class="block text-sm font-medium text-gray-900" + > <i18n.Translate>SMS</i18n.Translate> </span> - {!resultAccount.body.contact_data?.phone && i18n.str`add a phone number in your profile to enable this option`} + {!resultAccount.body.contact_data?.phone && + i18n.str`add a phone number in your profile to enable this option`} </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 + 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> <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - {onCancel ? - <button type="button" class="text-sm font-semibold leading-6 text-gray-900" - onClick={onCancel} - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - : <div /> - } - <button type="submit" + <a + href={routeClose.url({})} + type="button" + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" 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" disabled={!!errors} onClick={(e) => { - e.preventDefault() - createCashout() + e.preventDefault(); + createCashout(); }} > <i18n.Translate>Cashout</i18n.Translate> diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index 478d631fb..589e29793 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -17,93 +17,99 @@ import { Amounts, HttpStatusCode, TalerError, - TalerErrorCode, - TranslatedString + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, Loading, - LocalNotificationBanner, - ShowInputErrorLabel, - useLocalNotification, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { mutate } from "swr"; +import { VNode, h } from "preact"; import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; -import { useBankCoreApiContext } from "../../context/config.js"; -import { useBackendState } from "../../hooks/backend.js"; -import { - useCashoutDetails, useConversionInfo -} from "../../hooks/circuit.js"; -import { - undefinedIfEmpty -} from "../../utils.js"; +import { useCashoutDetails, useConversionInfo } from "../../hooks/circuit.js"; +import { RouteDefinition } from "../../route.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; interface Props { id: string; - onCancel: () => void; + routeClose: RouteDefinition<Record<string, never>>; } -export function ShowCashoutDetails({ - id, - onCancel, -}: Props): VNode { +export function ShowCashoutDetails({ id, routeClose }: Props): VNode { const { i18n, dateLocale } = useTranslationContext(); - const { state } = useBackendState(); - const cid = Number.parseInt(id, 10) + const cid = Number.parseInt(id, 10); const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid); const info = useConversionInfo(); if (Number.isNaN(cid)) { - return <Attention type="danger" title={i18n.str`cashout id should be a number`} /> + return ( + <Attention + type="danger" + title={i18n.str`cashout id should be a number`} + /> + ); } if (!result) { - return <Loading /> + return <Loading />; } if (result instanceof TalerError) { - return <ErrorLoadingWithDebug error={result} /> + return <ErrorLoadingWithDebug error={result} />; } if (result.type === "fail") { switch (result.case) { - case HttpStatusCode.NotFound: return <Attention type="warning" title={i18n.str`This cashout not found. Maybe already aborted.`}> - </Attention> - case HttpStatusCode.NotImplemented: return <Attention type="warning" title={i18n.str`Cashouts are not supported`}> - </Attention> - default: assertUnreachable(result) + case HttpStatusCode.NotFound: + return ( + <Attention + type="warning" + title={i18n.str`This cashout not found. Maybe already aborted.`} + ></Attention> + ); + case HttpStatusCode.NotImplemented: + return ( + <Attention + type="warning" + title={i18n.str`Cashouts are not supported`} + ></Attention> + ); + default: + assertUnreachable(result); } } if (!info) { - return <Loading /> + return <Loading />; } if (info instanceof TalerError) { - return <ErrorLoadingWithDebug error={info} /> + return <ErrorLoadingWithDebug error={info} />; } if (info.type === "fail") { switch (info.case) { case HttpStatusCode.NotImplemented: { - return <Attention type="danger" title={i18n.str`Cashout not implemented`} /> + return ( + <Attention type="danger" title={i18n.str`Cashout not implemented`} /> + ); } - default: assertUnreachable(info.case) + default: + assertUnreachable(info.case); } } - const { fiat_currency_specification, regional_currency_specification } = info.body + const { fiat_currency_specification, regional_currency_specification } = + info.body; return ( <div> <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"> - <section class="rounded-sm px-4"> - <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout detail</i18n.Translate></h2> + <h2 id="summary-heading" class="font-medium text-lg"> + <i18n.Translate>Cashout detail</i18n.Translate> + </h2> <dl class="mt-8 space-y-4"> <div class="justify-between items-center flex"> - <dt class="text-sm text-gray-600"><i18n.Translate>Subject</i18n.Translate></dt> + <dt class="text-sm text-gray-600"> + <i18n.Translate>Subject</i18n.Translate> + </dt> <dd class="text-sm ">{result.body.subject}</dd> </div> </dl> @@ -113,48 +119,64 @@ export function ShowCashoutDetails({ <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"> <dl class="space-y-4"> - - {result.body.creation_time.t_s !== "never" ? + {result.body.creation_time.t_s !== "never" ? ( <div class="justify-between items-center flex "> - <dt class=" text-gray-600"><i18n.Translate>Created</i18n.Translate></dt> + <dt class=" text-gray-600"> + <i18n.Translate>Created</i18n.Translate> + </dt> <dd class="text-sm "> - {format(result.body.creation_time.t_s * 1000, "dd/MM/yyyy HH:mm:ss", { locale: dateLocale })} + {format( + result.body.creation_time.t_s * 1000, + "dd/MM/yyyy HH:mm:ss", + { locale: dateLocale }, + )} </dd> </div> - : undefined} + ) : undefined} <div class="flex justify-between items-center border-t-2 afu pt-4"> - <dt class="text-gray-600"><i18n.Translate>Debited</i18n.Translate></dt> + <dt class="text-gray-600"> + <i18n.Translate>Debited</i18n.Translate> + </dt> <dd class=" font-medium"> - <RenderAmount value={Amounts.parseOrThrow(result.body.amount_debit)} negative withColor spec={regional_currency_specification} /> + <RenderAmount + value={Amounts.parseOrThrow(result.body.amount_debit)} + negative + withColor + 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-gray-600"> - <span><i18n.Translate>Credited</i18n.Translate></span> - + <span> + <i18n.Translate>Credited</i18n.Translate> + </span> </dt> <dd class="text-sm "> - <RenderAmount value={Amounts.parseOrThrow(result.body.amount_credit)} withColor spec={fiat_currency_specification} /> + <RenderAmount + value={Amounts.parseOrThrow(result.body.amount_credit)} + withColor + spec={fiat_currency_specification} + /> </dd> </div> - </dl> </div> </div> </div> - </div> - </div> <br /> <div style={{ display: "flex", justifyContent: "space-between" }}> - <button type="button" class="text-sm font-semibold leading-6 text-gray-900" - onClick={onCancel} + <a + href={routeClose.url({})} + class="text-sm font-semibold leading-6 text-gray-900" > - <i18n.Translate>Cancel</i18n.Translate></button> + <i18n.Translate>Close</i18n.Translate> + </a> </div> </div> ); |