diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx | 431 |
1 files changed, 242 insertions, 189 deletions
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index 52ff713e2..6fd9eb18c 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { + Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util"; @@ -32,7 +33,7 @@ import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { - useCashoutDetails + useCashoutDetails, useConversionInfo } from "../../hooks/circuit.js"; import { undefinedIfEmpty, @@ -40,6 +41,7 @@ import { } from "../../utils.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; +import { RenderAmount } from "../PaytoWireTransferForm.js"; interface Props { id: string; @@ -58,7 +60,12 @@ export function ShowCashoutDetails({ const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid); const [code, setCode] = useState<string | undefined>(undefined); const [notification, notify, handleError] = useLocalNotification() + const info = useConversionInfo(); + if (Number.isNaN(cid)) { + //TODO: better error message + return <div>cashout id should be a number</div> + } if (!result) { return <Loading /> } @@ -74,206 +81,252 @@ export function ShowCashoutDetails({ default: assertUnreachable(result) } } - if (Number.isNaN(cid)) { - //TODO: better error message - return <div>cashout id should be a number</div> + if (!info) { + return <Loading /> + } + + if (info instanceof TalerError) { + return <ErrorLoading error={info} /> } + const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, }); const isPending = String(result.body.status).toUpperCase() === "PENDING"; + const { fiat_currency_specification, regional_currency_specification } = info.body + async function doAbortCashout() { + if (!creds) return; + await handleError(async () => { + const resp = await api.abortCashoutById(creds, cid); + if (resp.type === "ok") { + onCancel(); + } else { + switch (resp.case) { + case "not-found": return notify({ + type: "error", + title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "already-confirmed": return notify({ + type: "error", + title: i18n.str`Cashout was already confimed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "cashout-not-supported": return notify({ + type: "error", + title: i18n.str`Cashout operation is not supported.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: { + assertUnreachable(resp) + } + } + } + }) + } + async function doConfirmCashout() { + if (!creds || !code) return; + await handleError(async () => { + const resp = await api.confirmCashoutById(creds, cid, { + tan: code, + }); + if (resp.type === "ok") { + mutate(() => true)//clean cashout state + } else { + switch (resp.case) { + case "not-found": return notify({ + type: "error", + title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "no-enough-balance": return notify({ + type: "error", + title: i18n.str`The account does not have sufficient funds`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "incorrect-exchange-rate": return notify({ + type: "error", + title: i18n.str`The exchange rate was incorrectly applied`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "already-aborted": return notify({ + type: "error", + title: i18n.str`The cashout operation is already aborted.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "no-cashout-payto": return notify({ + type: "error", + title: i18n.str`Missing destination account.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "too-many-attempts": return notify({ + type: "error", + title: i18n.str`Too many failed attempts.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "cashout-not-supported": return notify({ + type: "error", + title: i18n.str`Cashout operation is not supported.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "invalid-code": return notify({ + type: "error", + title: i18n.str`The code for this cashout is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } + } + }) + } + return ( <div> <LocalNotificationBanner notification={notification} /> - <h1>Cashout details {id}</h1> - <form class="pure-form"> - <fieldset> - <label> - <i18n.Translate>Subject</i18n.Translate> - </label> - <input readOnly value={result.body.subject} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Created</i18n.Translate> - </label> - <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Confirmed</i18n.Translate> - </label> - <input readOnly value={result.body.confirmation_time === undefined ? "-" : - (result.body.confirmation_time.t_s === "never" ? - i18n.str`never` : - format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")) - } /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Debited</i18n.Translate> - </label> - <input readOnly value={result.body.amount_debit} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Credit</i18n.Translate> - </label> - <input readOnly value={result.body.amount_credit} /> - </fieldset> - <fieldset> - <label> - <i18n.Translate>Status</i18n.Translate> - </label> - <input readOnly value={result.body.status} /> - </fieldset> - {/* <fieldset> - <label> - <i18n.Translate>Destination</i18n.Translate> - </label> - <input readOnly value={result.body.credit_payto_uri} /> - </fieldset> */} - {isPending ? ( - <fieldset> - <label> - <i18n.Translate>Code</i18n.Translate> - </label> - <input - value={code ?? ""} - onChange={(e) => { - setCode(e.currentTarget.value); + <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> + <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> + <dd class="text-sm ">{result.body.subject}</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>Status</i18n.Translate></span> + </dt> + <dd data-status={result.body.status} class="text-sm uppercase data-[status=pending]:text-yellow-600 data-[status=aborted]:text-red-600 data-[status=confirmed]:text-green-600" > + {result.body.status} + </dd> + </div> + </dl> + </section> + <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"> + <div class="px-4 py-6 sm:p-8"> + <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" ? + <div class="justify-between items-center flex "> + <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")} + </dd> + </div> + : 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> + <dd class=" font-medium"> + <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> + + </dt> + <dd class="text-sm "> + <RenderAmount value={Amounts.parseOrThrow(result.body.amount_credit)} withColor spec={fiat_currency_specification} /> + </dd> + </div> + + {result.body.confirmation_time && result.body.confirmation_time.t_s !== "never" ? + <div class="flex justify-between items-center border-t-2 afu pt-4"> + <dt class=" font-medium"><i18n.Translate>Confirmed</i18n.Translate></dt> + <dd class=" font-medium"> + {format(result.body.confirmation_time.t_s * 1000, "dd/MM/yyyy HH:mm:ss")} + </dd> + </div> + : undefined} + </dl> + </div> + </div> + </div> + + </div> + + {!isPending ? undefined : + <Fragment> + + <div /> + <form + class="bg-white shadow-sm ring-1 ring-gray-900/5" + autoCapitalize="none" + autoCorrect="off" + onSubmit={e => { + e.preventDefault() }} - /> - <ShowInputErrorLabel - message={errors?.code} - isDirty={code !== undefined} - /> - </fieldset> - ) : undefined} - </form> + > + <div class="px-4 py-6 sm:p-8"> + <label for="withdraw-amount"> + Enter the confirmation code + </label> + <div class="mt-2"> + <div class="relative rounded-md shadow-sm"> + <input + type="text" + // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + aria-describedby="answer" + autoFocus + class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={code ?? ""} + required + + name="answer" + id="answer" + autocomplete="off" + onChange={(e): void => { + setCode(e.currentTarget.value) + }} + /> + </div> + <ShowInputErrorLabel message={errors?.code} isDirty={code !== undefined} /> + </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"> + <button type="button" + class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" + onClick={doAbortCashout} + > + <i18n.Translate>Abort</i18n.Translate></button> + <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) => { + doConfirmCashout() + }} + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> + + </form> + </Fragment>} + </div> + <br /> <div style={{ display: "flex", justifyContent: "space-between" }}> - <button - class="pure-button pure-button-secondary btn-cancel" - onClick={(e) => { - e.preventDefault(); - onCancel(); - }} + <button type="button" class="text-sm font-semibold leading-6 text-gray-900" + onClick={onCancel} > - {i18n.str`Back`} - </button> - {isPending ? ( - <div> - <button - type="submit" - class="pure-button pure-button-primary button-error" - onClick={async (e) => { - e.preventDefault(); - if (!creds) return; - await handleError(async () => { - const resp = await api.abortCashoutById(creds, cid); - if (resp.type === "ok") { - onCancel(); - } else { - switch (resp.case) { - case "not-found": return notify({ - type: "error", - title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "already-confirmed": return notify({ - type: "error", - title: i18n.str`Cashout was already confimed.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "cashout-not-supported": return notify({ - type: "error", - title: i18n.str`Cashout operation is not supported.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: { - assertUnreachable(resp) - } - } - } - }) - }} - > - {i18n.str`Abort`} - </button> - - <button - type="submit" - disabled={!code} - class="pure-button pure-button-primary " - onClick={async (e) => { - e.preventDefault(); - if (!creds || !code) return; - await handleError(async () => { - const resp = await api.confirmCashoutById(creds, cid, { - tan: code, - }); - if (resp.type === "ok") { - mutate(() => true)//clean cashout state - } else { - switch (resp.case) { - case "not-found": return notify({ - type: "error", - title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "no-enough-balance": return notify({ - type: "error", - title: i18n.str`The account does not have sufficient funds`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "incorrect-exchange-rate": return notify({ - type: "error", - title: i18n.str`The exchange rate was incorrectly applied`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "already-aborted": return notify({ - type: "error", - title: i18n.str`The cashout operation is already aborted.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "no-cashout-payto": return notify({ - type: "error", - title: i18n.str`Missing destination account.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "too-many-attempts": return notify({ - type: "error", - title: i18n.str`Too many failed attempts.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "cashout-not-supported": return notify({ - type: "error", - title: i18n.str`Cashout operation is not supported.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) - } - } - }) - }} - > - {i18n.str`Confirm`} - </button> - </div> - ) : ( - <div /> - )} + <i18n.Translate>Cancel</i18n.Translate></button> </div> </div> ); |