diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/SolveChallengePage.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/SolveChallengePage.tsx | 806 |
1 files changed, 464 insertions, 342 deletions
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx b/packages/demobank-ui/src/pages/SolveChallengePage.tsx index 095a0f492..6d2d6512e 100644 --- a/packages/demobank-ui/src/pages/SolveChallengePage.tsx +++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx @@ -18,13 +18,12 @@ import { AbsoluteTime, Amounts, HttpStatusCode, - Logger, TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString, assertUnreachable, - parsePaytoUri + parsePaytoUri, } from "@gnu-taler/taler-util"; import { Attention, @@ -32,7 +31,7 @@ import { LocalNotificationBanner, ShowInputErrorLabel, useLocalNotification, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; @@ -43,40 +42,41 @@ import { useWithdrawalDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js"; import { useConversionInfo } from "../hooks/circuit.js"; +import { RouteDefinition } from "../route.js"; import { undefinedIfEmpty } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; -const logger = new Logger("SolveChallenge"); - export function SolveChallengePage({ - onContinue, + onChallengeCompleted, + routeClose, }: { - onContinue: () => void; + onChallengeCompleted: () => void; + routeClose: RouteDefinition<Record<string, never>>; }): VNode { - const { api } = useBankCoreApiContext() + const { api } = useBankCoreApiContext(); const { i18n } = useTranslationContext(); const [bankState, updateBankState] = useBankState(); const [code, setCode] = useState<string | undefined>(undefined); - const [notification, notify, handleError] = useLocalNotification() + const [notification, notify, handleError] = useLocalNotification(); const { state } = useBackendState(); - const creds = state.status !== "loggedIn" ? undefined : state + const creds = state.status !== "loggedIn" ? undefined : state; if (!bankState.currentChallenge) { - return <div> - <span>no challenge to solve </span> - <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={() => { - onContinue() - }} - > - <i18n.Translate>Continue</i18n.Translate> - </button> - </div> + return ( + <div> + <span>no challenge to solve </span> + <a + href={routeClose.url({})} + 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" + > + <i18n.Translate>Continue</i18n.Translate> + </a> + </div> + ); } - const ch = bankState.currentChallenge + const ch = bankState.currentChallenge; const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, }); @@ -86,34 +86,38 @@ export function SolveChallengePage({ await handleError(async () => { const resp = await api.sendChallenge(creds, ch.id); if (resp.type === "ok") { - const newCh = structuredClone(ch) - newCh.sent = AbsoluteTime.now() - newCh.info = resp.body - updateBankState("currentChallenge", newCh) + const newCh = structuredClone(ch); + newCh.sent = AbsoluteTime.now(); + newCh.info = resp.body; + updateBankState("currentChallenge", newCh); } else { switch (resp.case) { - case HttpStatusCode.NotFound: 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 HttpStatusCode.Unauthorized: 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 TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: 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, - }) - default: assertUnreachable(resp) + case HttpStatusCode.NotFound: + 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 HttpStatusCode.Unauthorized: + 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 TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: + 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, + }); + default: + assertUnreachable(resp); } } - }) + }); } async function completeChallenge() { @@ -121,55 +125,68 @@ export function SolveChallengePage({ await handleError(async () => { { const resp = await api.confirmChallenge(creds, ch.id, { - tan: code + tan: code, }); if (resp.type === "fail") { - setCode("") + setCode(""); switch (resp.case) { - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`Challenge not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.Unauthorized: return notify({ - type: "error", - title: i18n.str`This user is not authorized to complete this challenge.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.TooManyRequests: return notify({ - type: "error", - title: i18n.str`Too many attemps, try another code.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return notify({ - type: "error", - title: i18n.str`The confirmation code is wrong, try again.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return notify({ - type: "error", - title: i18n.str`The operation expired.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`Challenge not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.Unauthorized: + return notify({ + type: "error", + title: i18n.str`This user is not authorized to complete this challenge.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.TooManyRequests: + return notify({ + type: "error", + title: i18n.str`Too many attemps, try another code.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: + return notify({ + type: "error", + title: i18n.str`The confirmation code is wrong, try again.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: + return notify({ + type: "error", + title: i18n.str`The operation expired.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: + assertUnreachable(resp); } } } { const resp = await (async (ch: ChallengeInProgess) => { switch (ch.operation) { - case "delete-account": return await api.deleteAccount(creds, ch.id) - case "update-account": return await api.updateAccount(creds, ch.request, ch.id) - case "update-password": return await api.updatePassword(creds, ch.request, ch.id) - case "create-transaction": return await api.createTransaction(creds, ch.request, ch.id) - case "confirm-withdrawal": return await api.confirmWithdrawalById(creds, ch.request, ch.id) - case "create-cashout": return await api.createCashout(creds, ch.request, ch.id) - default: assertUnreachable(ch) + case "delete-account": + return await api.deleteAccount(creds, ch.id); + case "update-account": + return await api.updateAccount(creds, ch.request, ch.id); + case "update-password": + return await api.updatePassword(creds, ch.request, ch.id); + case "create-transaction": + return await api.createTransaction(creds, ch.request, ch.id); + case "confirm-withdrawal": + return await api.confirmWithdrawalById(creds, ch.request, ch.id); + case "create-cashout": + return await api.createCashout(creds, ch.request, ch.id); + default: + assertUnreachable(ch); } })(ch); @@ -180,36 +197,43 @@ export function SolveChallengePage({ title: i18n.str`The operation failed.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, - }) + }); } - // another challenge required + // another challenge required, save the request and the ID + // @ts-expect-error no need to check the type of request, since it will be the same as the previous request updateBankState("currentChallenge", { operation: ch.operation, id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), - request: ch.request as any, - }) + request: ch.request, + }); return notify({ type: "info", title: i18n.str`The operation needs another confirmation to complete.`, - }) + }); } - updateBankState("currentChallenge", undefined) - return onContinue() + updateBankState("currentChallenge", undefined); + return onChallengeCompleted(); } - }) + }); } const subtitle = ((op): TranslatedString => { switch (op) { - case "delete-account": return i18n.str`Account delete` - case "update-account": return i18n.str`Account update` - case "update-password": return i18n.str`Password update` - case "create-transaction": return i18n.str`Wire transfer` - case "confirm-withdrawal": return i18n.str`Withdrawal` - case "create-cashout": return i18n.str`Cashout` + case "delete-account": + return i18n.str`Account delete`; + case "update-account": + return i18n.str`Account update`; + case "update-password": + return i18n.str`Password update`; + case "create-transaction": + return i18n.str`Wire transfer`; + case "confirm-withdrawal": + return i18n.str`Withdrawal`; + case "create-cashout": + return i18n.str`Cashout`; } - })(ch.operation) + })(ch.operation); return ( <Fragment> @@ -217,25 +241,29 @@ export function SolveChallengePage({ <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"> - <span class="text-sm text-black font-semibold leading-6 " id="availability-label"> + <span + class="text-sm text-black font-semibold leading-6 " + id="availability-label" + > <i18n.Translate>Confirm the operation</i18n.Translate> </span> </h2> - <span> - {subtitle} - </span> + <span>{subtitle}</span> </div> <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"> - <ChallengeDetails challenge={bankState.currentChallenge} onStart={startChallenge} /> - {ch.info && + <ChallengeDetails + challenge={bankState.currentChallenge} + onStart={startChallenge} + /> + {ch.info && ( <div class="mt-3 text-sm leading-6"> <form class="bg-white shadow-sm ring-1 ring-gray-900/5" autoCapitalize="none" autoCorrect="off" - onSubmit={e => { - e.preventDefault() + onSubmit={(e) => { + e.preventDefault(); }} > <div class="px-4 py-6 sm:p-8"> @@ -252,314 +280,408 @@ export function SolveChallengePage({ 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) + setCode(e.currentTarget.value); }} /> </div> - <ShowInputErrorLabel message={errors?.code} isDirty={code !== undefined} /> + <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" + <a + href={routeClose.url({})} 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={() => { - updateBankState("currentChallenge", undefined) - onContinue() - }} > <i18n.Translate>Cancel</i18n.Translate> - </button> - <button type="submit" + </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) => { - completeChallenge() + completeChallenge(); + e.preventDefault(); }} > <i18n.Translate>Confirm</i18n.Translate> </button> </div> </form> - - {/* <ShouldBeSameUser username={details.username}> */} - {/* </ShouldBeSameUser> */} </div> - } + )} </div> </div> </Fragment> - ); } -function ChallengeDetails({ challenge, onStart }: { challenge: ChallengeInProgess, onStart: () => void }): VNode { +function ChallengeDetails({ + challenge, + onStart, +}: { + challenge: ChallengeInProgess; + onStart: () => void; +}): VNode { const { i18n, dateLocale } = useTranslationContext(); const { config } = useBankCoreApiContext(); - return <div class="px-4 mt-4 "> - <div class="w-full"> - <div class="flex justify-center"> - - {challenge.info ? - <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" - onClick={(e) => { - onStart() - }} - > - <i18n.Translate>Send again</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" - onClick={(e) => { - onStart() - }} - > - <i18n.Translate>Send code</i18n.Translate> - </button> - } - </div> - <div class="mt-6 border-t border-gray-100"> - <h2 class="text-base font-semibold leading-7 text-gray-900"> - <span class="text-sm text-black font-semibold leading-6 " id="availability-label"> - <i18n.Translate>Operation details</i18n.Translate> - </span> - </h2> - <dl class="divide-y divide-gray-100"> - {((): VNode => { - switch (challenge.operation) { - case "delete-account": return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Account</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{challenge.request}</dd> - </div> - case "create-transaction": { - const payto = parsePaytoUri(challenge.request.payto_uri)! - return <Fragment> - {challenge.request.amount && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={Amounts.parseOrThrow(challenge.request.amount)} spec={config.currency_specification} /> - </dd> - </div> - } - {payto.isKnown && payto.targetType === "iban" && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">To account</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {payto.iban} - </dd> - </div> - } - </Fragment> - } - case "confirm-withdrawal": return <ShowWithdrawalDetails id={challenge.request} /> - case "create-cashout": { - return <ShowCashoutDetails request={challenge.request} /> - } - case "update-account": { - return <Fragment> - {challenge.request.cashout_payto_uri !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Cashout account</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.cashout_payto_uri} - </dd> - </div> - } - {challenge.request.contact_data?.email !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Email</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.contact_data?.email} - </dd> - </div> - } - {challenge.request.contact_data?.phone !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Phone</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.contact_data?.phone} - </dd> - </div> - } - {challenge.request.debit_threshold !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Debit threshold</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={Amounts.parseOrThrow(challenge.request.debit_threshold)} spec={config.currency_specification} /> - </dd> - </div> - } - {challenge.request.is_public !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Is this account public?</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.is_public ? "enable" : "disable"} - </dd> - </div> - } - {challenge.request.name !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Name</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.name} - </dd> - </div> - } - {challenge.request.tan_channel !== undefined && + return ( + <div class="px-4 mt-4 "> + <div class="w-full"> + <div class="flex justify-center"> + {challenge.info ? ( + <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" + onClick={(e) => { + onStart(); + e.preventDefault(); + }} + > + <i18n.Translate>Send again</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" + onClick={(e) => { + onStart(); + e.preventDefault(); + }} + > + <i18n.Translate>Send code</i18n.Translate> + </button> + )} + </div> + <div class="mt-6 border-t border-gray-100"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <span + class="text-sm text-black font-semibold leading-6 " + id="availability-label" + > + <i18n.Translate>Operation details</i18n.Translate> + </span> + </h2> + <dl class="divide-y divide-gray-100"> + {((): VNode => { + switch (challenge.operation) { + case "delete-account": + return ( <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Authentication channel</dt> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Account + </dt> <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.tan_channel} + {challenge.request} </dd> </div> - } - </Fragment> + ); + case "create-transaction": { + const payto = parsePaytoUri(challenge.request.payto_uri)!; + return ( + <Fragment> + {challenge.request.amount && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Amount + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + <RenderAmount + value={Amounts.parseOrThrow( + challenge.request.amount, + )} + spec={config.currency_specification} + /> + </dd> + </div> + )} + {payto.isKnown && payto.targetType === "iban" && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + To account + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {payto.iban} + </dd> + </div> + )} + </Fragment> + ); + } + case "confirm-withdrawal": + return <ShowWithdrawalDetails id={challenge.request} />; + case "create-cashout": { + return <ShowCashoutDetails request={challenge.request} />; + } + case "update-account": { + return ( + <Fragment> + {challenge.request.cashout_payto_uri !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Cashout account + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.cashout_payto_uri} + </dd> + </div> + )} + {challenge.request.contact_data?.email !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Email + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.contact_data?.email} + </dd> + </div> + )} + {challenge.request.contact_data?.phone !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Phone + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.contact_data?.phone} + </dd> + </div> + )} + {challenge.request.debit_threshold !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Debit threshold + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + <RenderAmount + value={Amounts.parseOrThrow( + challenge.request.debit_threshold, + )} + spec={config.currency_specification} + /> + </dd> + </div> + )} + {challenge.request.is_public !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Is this account public? + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.is_public ? "enable" : "disable"} + </dd> + </div> + )} + {challenge.request.name !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Name + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.name} + </dd> + </div> + )} + {challenge.request.tan_channel !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Authentication channel + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.tan_channel} + </dd> + </div> + )} + </Fragment> + ); + } + case "update-password": { + return ( + <Fragment> + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + New password + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.request.new_password} + </dd> + </div> + </Fragment> + ); + } + default: + assertUnreachable(challenge); } - case "update-password": { - return <Fragment> - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">New password</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.request.new_password} - </dd> - </div> - </Fragment> - } - default: assertUnreachable(challenge) - } - })()} + })()} - {challenge.info && - <h2 class="text-base font-semibold leading-7 text-gray-900"> - <span class="text-sm text-black font-semibold leading-6 " id="availability-label"> - <i18n.Translate>Challenge details</i18n.Translate> - </span> - </h2> - } - {challenge.sent.t_ms !== "never" && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900"><i18n.Translate>Sent at</i18n.Translate></dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss", { locale: dateLocale })} - </dd> - </div> - } - {challenge.info && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900"> - {((ch: TalerCorebankApi.TanChannel): VNode => { - switch (ch) { - case TalerCorebankApi.TanChannel.SMS: return <i18n.Translate>To phone</i18n.Translate> - case TalerCorebankApi.TanChannel.EMAIL: return <i18n.Translate>To email</i18n.Translate> - default: assertUnreachable(ch) - } - })(challenge.info.tan_channel)} - </dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {challenge.info.tan_info} - </dd> - </div> - } - - </dl> + {challenge.info && ( + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <span + class="text-sm text-black font-semibold leading-6 " + id="availability-label" + > + <i18n.Translate>Challenge details</i18n.Translate> + </span> + </h2> + )} + {challenge.sent.t_ms !== "never" && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + <i18n.Translate>Sent at</i18n.Translate> + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss", { + locale: dateLocale, + })} + </dd> + </div> + )} + {challenge.info && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + {((ch: TalerCorebankApi.TanChannel): VNode => { + switch (ch) { + case TalerCorebankApi.TanChannel.SMS: + return <i18n.Translate>To phone</i18n.Translate>; + case TalerCorebankApi.TanChannel.EMAIL: + return <i18n.Translate>To email</i18n.Translate>; + default: + assertUnreachable(ch); + } + })(challenge.info.tan_channel)} + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {challenge.info.tan_info} + </dd> + </div> + )} + </dl> + </div> </div> </div> - </div> + ); } function ShowWithdrawalDetails({ id }: { id: string }): VNode { - const { i18n } = useTranslationContext(); - const details = useWithdrawalDetails(id) + const details = useWithdrawalDetails(id); const { config } = useBankCoreApiContext(); if (!details) { - return <Loading /> + return <Loading />; } if (details instanceof TalerError) { - return <ErrorLoadingWithDebug error={details} /> + return <ErrorLoadingWithDebug error={details} />; } if (details.type === "fail") { switch (details.case) { case HttpStatusCode.BadRequest: - case HttpStatusCode.NotFound: return <OperationNotFound onClose={undefined} /> - default: assertUnreachable(details) + case HttpStatusCode.NotFound: + return <OperationNotFound routeClose={undefined} />; + default: + assertUnreachable(details); } } - return <Fragment> - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={Amounts.parseOrThrow(details.body.amount)} spec={config.currency_specification} /> - </dd> - </div> - {details.body.selected_reserve_pub !== undefined && - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Withdraw id</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" title={details.body.selected_reserve_pub}> - {details.body.selected_reserve_pub.substring(0, 16)}... - </dd> - </div> - } - {details.body.selected_exchange_account !== undefined && + return ( + <Fragment> <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">To account</dt> + <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {details.body.selected_exchange_account} + <RenderAmount + value={Amounts.parseOrThrow(details.body.amount)} + spec={config.currency_specification} + /> </dd> </div> - } - </Fragment> + {details.body.selected_reserve_pub !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + Withdraw id + </dt> + <dd + class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" + title={details.body.selected_reserve_pub} + > + {details.body.selected_reserve_pub.substring(0, 16)}... + </dd> + </div> + )} + {details.body.selected_exchange_account !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900"> + To account + </dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {details.body.selected_exchange_account} + </dd> + </div> + )} + </Fragment> + ); } -function ShowCashoutDetails({ request }: { request: TalerCorebankApi.CashoutRequest }): VNode { +function ShowCashoutDetails({ + request, +}: { + request: TalerCorebankApi.CashoutRequest; +}): VNode { const { i18n } = useTranslationContext(); const info = useConversionInfo(); 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); } } - - return <Fragment> - {request.subject !== undefined && + return ( + <Fragment> + {request.subject !== undefined && ( + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Subject</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + {request.subject} + </dd> + </div> + )} <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Subject</dt> + <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt> <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {request.subject} + <RenderAmount + value={Amounts.parseOrThrow(request.amount_credit)} + spec={info.body.regional_currency_specification} + /> </dd> </div> - } - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.regional_currency_specification} /> - </dd> - </div> - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.fiat_currency_specification} /> - </dd> - </div> - </Fragment> -}
\ No newline at end of file + <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> + <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt> + <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> + <RenderAmount + value={Amounts.parseOrThrow(request.amount_credit)} + spec={info.body.fiat_currency_specification} + /> + </dd> + </div> + </Fragment> + ); +} |