diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/OperationState/views.tsx | 497 |
1 files changed, 288 insertions, 209 deletions
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index c86b8bd4b..ac3724eb8 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,121 +14,143 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AbsoluteTime, HttpStatusCode, TalerErrorCode, TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AbsoluteTime, + HttpStatusCode, + TalerErrorCode, + TranslatedString, + assertUnreachable, + stringifyWithdrawUri, +} from "@gnu-taler/taler-util"; +import { + Attention, + LocalNotificationBanner, + notifyInfo, + useLocalNotification, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../../components/QR.js"; import { useBankState } from "../../hooks/bank-state.js"; import { usePreferences } from "../../hooks/preferences.js"; import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { State } from "./index.js"; -export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { - return ( - <div>Payto from server is not valid "{payto}"</div> - ); +export function InvalidPaytoView({ payto }: State.InvalidPayto) { + return <div>Payto from server is not valid "{payto}"</div>; } -export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { - return ( - <div>Withdrawal uri from server is not valid "{uri}"</div> - ); +export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) { + return <div>Withdrawal uri from server is not valid "{uri}"</div>; } -export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { - return ( - <div>Reserve from server is not valid "{reserve}"</div> - ); +export function InvalidReserveView({ reserve }: State.InvalidReserve) { + return <div>Reserve from server is not valid "{reserve}"</div>; } -export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, account, id, onAuthorizationRequired, }: State.NeedConfirmation) { - const { i18n } = useTranslationContext() - const [settings] = usePreferences() - const [notification, notify, errorHandler] = useLocalNotification() - const [, updateBankState] = useBankState() +export function NeedConfirmationView({ + onAbort: doAbort, + onConfirm: doConfirm, + account, + id, + onAuthorizationRequired, +}: State.NeedConfirmation) { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + const [notification, notify, errorHandler] = useLocalNotification(); + const [, updateBankState] = useBankState(); async function onCancel() { errorHandler(async () => { if (!doAbort) return; - const resp = await doAbort() + const resp = await doAbort(); if (!resp) return; switch (resp.case) { - case HttpStatusCode.Conflict: return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) + case HttpStatusCode.Conflict: + return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: + assertUnreachable(resp); } - }) + }); } async function onConfirm() { errorHandler(async () => { if (!doConfirm) return; - const resp = await doConfirm() + const resp = await doConfirm(); if (!resp) { if (!settings.showWithdrawalSuccess) { - notifyInfo(i18n.str`Wire transfer completed!`) + notifyInfo(i18n.str`Wire transfer completed!`); } - return + return; } switch (resp.case) { - case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return notify({ - type: "error", - title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({ - type: "error", - title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ - type: "error", - title: i18n.str`Your balance is not enough.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + return notify({ + type: "error", + title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return notify({ + type: "error", + title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return notify({ + type: "error", + title: i18n.str`Your balance is not enough.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); case HttpStatusCode.Accepted: { updateBankState("currentChallenge", { operation: "confirm-withdrawal", id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request: id, - }) - return onAuthorizationRequired() + }); + return onAuthorizationRequired(); } - default: assertUnreachable(resp) + default: + assertUnreachable(resp); } - }) + }); } return ( @@ -144,23 +166,27 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon 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="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="text-sm font-semibold leading-6 text-gray-900" + <button + type="button" + class="text-sm font-semibold leading-6 text-gray-900" onClick={(e) => { - e.preventDefault() - onCancel() + e.preventDefault(); + onCancel(); }} > - <i18n.Translate>Cancel</i18n.Translate></button> - <button type="submit" + <i18n.Translate>Cancel</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) => { - e.preventDefault() - onConfirm() + e.preventDefault(); + onConfirm(); }} > <i18n.Translate>Transfer</i18n.Translate> @@ -171,61 +197,81 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon </div> </div> </div> - ); } export function FailedView({ error }: State.Failed) { const { i18n } = useTranslationContext(); switch (error.case) { - case HttpStatusCode.Unauthorized: return <Attention type="danger" - title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - case HttpStatusCode.Conflict: return <Attention type="danger" - title={i18n.str`The operation was rejected due to insufficient funds.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - case HttpStatusCode.NotFound: return <Attention type="danger" - title={i18n.str`The operation was rejected due to insufficient funds.`}> - <div class="mt-2 text-sm text-red-700"> - {error.detail.hint} - </div> - </Attention> - default: assertUnreachable(error) + case HttpStatusCode.Unauthorized: + return ( + <Attention + type="danger" + title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`} + > + <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div> + </Attention> + ); + case HttpStatusCode.Conflict: + return ( + <Attention + type="danger" + title={i18n.str`The operation was rejected due to insufficient funds.`} + > + <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div> + </Attention> + ); + case HttpStatusCode.NotFound: + return ( + <Attention + type="danger" + title={i18n.str`The operation was rejected due to insufficient funds.`} + > + <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div> + </Attention> + ); + default: + assertUnreachable(error); } } -export function AbortedView({ error, onClose }: State.Aborted) { - return ( - <div>aborted</div> - ); +export function AbortedView() { + return <div>aborted</div>; } -export function ConfirmedView({ error, onClose }: State.Confirmed) { +export function ConfirmedView({ routeClose }: State.Confirmed) { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = usePreferences() + const [settings, updateSettings] = usePreferences(); return ( <Fragment> - <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all "> - <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100"> - <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> + <svg + class="h-6 w-6 text-green-600" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M4.5 12.75l6 6 9-13.5" + /> </svg> </div> <div class="mt-3 text-center sm:mt-5"> - <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> + <h3 + class="text-base font-semibold leading-6 text-gray-900" + id="modal-title" + > <i18n.Translate>Withdrawal confirmed</i18n.Translate> </h3> <div class="mt-2"> <p class="text-sm text-gray-500"> <i18n.Translate> - The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet. + The wire transfer to the Taler operator has been initiated. You + will soon receive the requested amount in your Taler wallet. </i18n.Translate> </p> </div> @@ -234,132 +280,165 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) { <div class="mt-4"> <div class="flex items-center justify-between"> <span class="flex flex-grow flex-col"> - <span class="text-sm text-black font-medium leading-6 " id="availability-label"> + <span + class="text-sm text-black font-medium leading-6 " + id="availability-label" + > <i18n.Translate>Do not show this again</i18n.Translate> </span> </span> - <button type="button" data-enabled={!settings.showWithdrawalSuccess} 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={!settings.showWithdrawalSuccess} + 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={() => { - updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess); - }}> - <span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} 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> + updateSettings( + "showWithdrawalSuccess", + !settings.showWithdrawalSuccess, + ); + }} + > + <span + aria-hidden="true" + data-enabled={!settings.showWithdrawalSuccess} + 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> <div class="mt-5 sm:mt-6"> - <button type="button" + <a + href={routeClose.url({})} + type="button" 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" - onClick={async (e) => { - e.preventDefault(); - onClose() - }}> + > <i18n.Translate>Close</i18n.Translate> - </button> + </a> </div> </Fragment> - ); } -export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> { +export function ReadyView({ + uri, + onAbort: doAbort, +}: State.Ready): VNode<Record<string, never>> { const { i18n } = useTranslationContext(); - const [notification, notify, errorHandler] = useLocalNotification() + const [notification, notify, errorHandler] = useLocalNotification(); const talerWithdrawUri = stringifyWithdrawUri(uri); useEffect(() => { - //Taler Wallet WebExtension is listening to headers response and tab updates. - //In the SPA there is no header response with the Taler URI so - //this hack manually triggers the tab update after the QR is in the DOM. + // Taler Wallet WebExtension is listening to headers response and tab updates. + // In the SPA there is no header response with the Taler URI so + // this hack manually triggers the tab update after the QR is in the DOM. // WebExtension will be using // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated document.title = `${document.title} ${uri.withdrawalOperationId}`; - const meta = document.createElement("meta") - meta.setAttribute("name", "taler-uri") - meta.setAttribute("content", talerWithdrawUri) - document.head.insertBefore(meta, document.head.children.length ? document.head.children[0] : null) + const meta = document.createElement("meta"); + meta.setAttribute("name", "taler-uri"); + meta.setAttribute("content", talerWithdrawUri); + document.head.insertBefore( + meta, + document.head.children.length ? document.head.children[0] : null, + ); }, []); - async function onClose() { + async function onAbort() { errorHandler(async () => { - const hasError = await doClose() + const hasError = await doAbort(); if (!hasError) return; switch (hasError.case) { - case HttpStatusCode.Conflict: return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }) - case HttpStatusCode.BadRequest: return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - case HttpStatusCode.NotFound: return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: hasError.detail.hint as TranslatedString, - debug: hasError.detail, - }); - default: assertUnreachable(hasError) + case HttpStatusCode.Conflict: + return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case HttpStatusCode.BadRequest: + return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case HttpStatusCode.NotFound: + return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + default: + assertUnreachable(hasError); } - }) + }); } - return <Fragment> - <LocalNotificationBanner notification={notification} /> + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> - <div class="flex justify-end mt-4"> - <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={() => { - onClose() - }} - > - Cancel - </button> - </div> + <div class="flex justify-end mt-4"> + <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={onAbort} + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + </div> - <div class="bg-white shadow sm:rounded-lg mt-4"> - <div class="p-4"> - <h3 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>On this device</i18n.Translate> - </h3> - <div class="mt-2 sm:flex sm:items-start sm:justify-between"> - <div class="max-w-xl text-sm text-gray-500"> - <p> - <i18n.Translate>If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.</i18n.Translate> - </p> - </div> - <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center"> - <a href={talerWithdrawUri} - class="inline-flex items-center 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" - > - <i18n.Translate>Start</i18n.Translate> - </a> + <div class="bg-white shadow sm:rounded-lg mt-4"> + <div class="p-4"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>On this device</i18n.Translate> + </h3> + <div class="mt-2 sm:flex sm:items-start sm:justify-between"> + <div class="max-w-xl text-sm text-gray-500"> + <p> + <i18n.Translate> + If you are using a web browser on desktop you should access + your wallet with the GNU Taler WebExtension now or click the + link if your WebExtension have the "Inject Taler support" + option enabled. + </i18n.Translate> + </p> + </div> + <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center"> + <a + href={talerWithdrawUri} + class="inline-flex items-center 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" + > + <i18n.Translate>Start</i18n.Translate> + </a> + </div> </div> </div> </div> - </div> - <div class="bg-white shadow sm:rounded-lg mt-2"> - <div class="p-4"> - <h3 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>On a mobile phone</i18n.Translate> - </h3> - <div class="mt-2 sm:flex sm:items-start sm:justify-between"> - <div class="max-w-xl text-sm text-gray-500"> - <p> - <i18n.Translate>Scan the QR code with your mobile device.</i18n.Translate> - </p> + <div class="bg-white shadow sm:rounded-lg mt-2"> + <div class="p-4"> + <h3 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>On a mobile phone</i18n.Translate> + </h3> + <div class="mt-2 sm:flex sm:items-start sm:justify-between"> + <div class="max-w-xl text-sm text-gray-500"> + <p> + <i18n.Translate> + Scan the QR code with your mobile device. + </i18n.Translate> + </p> + </div> + </div> + <div class="mt-2 max-w-md ml-auto mr-auto"> + <QR text={talerWithdrawUri} /> </div> - </div> - <div class="mt-2 max-w-md ml-auto mr-auto"> - <QR text={talerWithdrawUri} /> </div> </div> - </div> - - </Fragment> - + </Fragment> + ); } |