aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/OperationState/views.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src/pages/OperationState/views.tsx')
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx497
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 &quot;{payto}&quot;</div>
- );
+export function InvalidPaytoView({ payto }: State.InvalidPayto) {
+ return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
}
-export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
- return (
- <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>
- );
+export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) {
+ return <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>;
}
-export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
- return (
- <div>Reserve from server is not valid &quot;{reserve}&quot;</div>
- );
+export function InvalidReserveView({ reserve }: State.InvalidReserve) {
+ return <div>Reserve from server is not valid &quot;{reserve}&quot;</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>
+ );
}