aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx')
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx431
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>
- &nbsp;
- <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>
);