diff options
Diffstat (limited to 'packages/demobank-ui/src')
14 files changed, 113 insertions, 33 deletions
diff --git a/packages/demobank-ui/src/hooks/bank-state.ts b/packages/demobank-ui/src/hooks/bank-state.ts new file mode 100644 index 000000000..addbbfc0f --- /dev/null +++ b/packages/demobank-ui/src/hooks/bank-state.ts @@ -0,0 +1,64 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + Codec, + buildCodecForObject, + codecForString, + codecOptional +} from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; + +interface BankState { + currentWithdrawalOperationId: string | undefined; + currentChallengeId: string | undefined; +} + +export const codecForBankState = (): Codec<BankState> => + buildCodecForObject<BankState>() + .property("currentWithdrawalOperationId", codecOptional(codecForString())) + .property("currentChallengeId", codecOptional(codecForString())) + .build("BankState"); + +const defaultBankState: BankState = { + currentWithdrawalOperationId: undefined, + currentChallengeId: undefined, +}; + +const BANK_STATE_KEY = buildStorageKey( + "bank-state", + codecForBankState(), +); + +export function useBankState(): [ + Readonly<BankState>, + <T extends keyof BankState>(key: T, value: BankState[T]) => void, + () => void, +] { + const { value, update } = useLocalStorage( + BANK_STATE_KEY, + defaultBankState, + ); + + function updateField<T extends keyof BankState>(k: T, v: BankState[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + function reset() { + update(defaultBankState) + } + return [value, updateField, reset]; +} diff --git a/packages/demobank-ui/src/hooks/preferences.ts b/packages/demobank-ui/src/hooks/preferences.ts index a1525ac80..d303ac0d8 100644 --- a/packages/demobank-ui/src/hooks/preferences.ts +++ b/packages/demobank-ui/src/hooks/preferences.ts @@ -19,14 +19,11 @@ import { TranslatedString, buildCodecForObject, codecForBoolean, - codecForNumber, - codecForString, - codecOptional + codecForNumber } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser"; interface Preferences { - currentWithdrawalOperationId: string | undefined; showWithdrawalSuccess: boolean; showDemoDescription: boolean; showInstallWallet: boolean; @@ -42,7 +39,6 @@ export function getAllBooleanPreferences(): Array<keyof Preferences> { export function getLabelForPreferences(k: keyof Preferences, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString { switch (k) { - case "currentWithdrawalOperationId": return i18n.str`Current withdrawal operation` case "maxWithdrawalAmount": return i18n.str`Max withdrawal amount` case "showWithdrawalSuccess": return i18n.str`Show withdrawal confirmation` case "showDemoDescription": return i18n.str`Show demo description` @@ -54,7 +50,6 @@ export function getLabelForPreferences(k: keyof Preferences, i18n: ReturnType<ty export const codecForPreferences = (): Codec<Preferences> => buildCodecForObject<Preferences>() - .property("currentWithdrawalOperationId", codecOptional(codecForString())) .property("showWithdrawalSuccess", (codecForBoolean())) .property("showDemoDescription", (codecForBoolean())) .property("showInstallWallet", (codecForBoolean())) @@ -64,7 +59,6 @@ export const codecForPreferences = (): Codec<Preferences> => .build("Settings"); const defaultPreferences: Preferences = { - currentWithdrawalOperationId: undefined, showWithdrawalSuccess: true, showDemoDescription: true, showInstallWallet: true, diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 737a00b57..9e591e95d 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -24,6 +24,7 @@ import { getAllBooleanPreferences, getLabelForPreferences, usePreferences } from import { RenderAmount } from "./PaytoWireTransferForm.js"; import { useSettingsContext } from "../context/settings.js"; import { useBankCoreApiContext } from "../context/config.js"; +import { useBankState } from "../hooks/bank-state.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -40,6 +41,7 @@ export function BankFrame({ const backend = useBackendState(); const settings = useSettingsContext(); const [preferences, updatePreferences] = usePreferences(); + const [, , resetBankState] = useBankState() const [error, resetError] = useErrorBoundary(); @@ -65,7 +67,7 @@ export function BankFrame({ iconLinkURL={settings.iconLinkURL ?? "#"} onLogout={backend.state.status !== "loggedIn" ? undefined : () => { backend.logOut() - updatePreferences("currentWithdrawalOperationId", undefined); + resetBankState(); }} sites={!settings.topNavSites ? [] : Object.entries(settings.topNavSites)} supportedLangs={["en", "es", "de"]} diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 477146d1e..b214a400d 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -24,9 +24,11 @@ import { useBackendState } from "../../hooks/backend.js"; import { usePreferences } from "../../hooks/preferences.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { Props, State } from "./index.js"; +import { useBankState } from "../../hooks/bank-state.js"; export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> { - const [settings, updateSettings] = usePreferences() + const [settings] = usePreferences() + const [bankState, updateBankState] = useBankState(); const { state: credentials } = useBackendState() const creds = credentials.status !== "loggedIn" ? undefined : credentials const { api } = useBankCoreApiContext() @@ -46,11 +48,11 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive setFailure(resp) return; } - updateSettings("currentWithdrawalOperationId", resp.body.withdrawal_id) + updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id) } - const withdrawalOperationId = settings.currentWithdrawalOperationId + const withdrawalOperationId = bankState.currentWithdrawalOperationId useEffect(() => { if (withdrawalOperationId === undefined) { doSilentStart() @@ -77,7 +79,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive if (!creds) return; const resp = await api.abortWithdrawalById(creds, wid); if (resp.type === "ok") { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onClose(); } else { return resp; @@ -140,7 +142,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "aborted", error: undefined, onClose: async () => { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onClose() }, } @@ -155,7 +157,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "aborted", error: undefined, onClose: async () => { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onClose() }, } @@ -163,14 +165,14 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive if (data.status === "confirmed") { if (!settings.showWithdrawalSuccess) { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onClose() } return { status: "confirmed", error: undefined, onClose: async () => { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onClose() }, } diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index 98eb7169f..5ebd66dac 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -24,6 +24,7 @@ import { undefinedIfEmpty } from "../../utils.js"; import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { State } from "./index.js"; +import { useBankState } from "../../hooks/bank-state.js"; export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( @@ -45,6 +46,7 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon const { i18n } = useTranslationContext() const [settings] = usePreferences() const [notification, notify, errorHandler] = useLocalNotification() + const [, updateBankState] = useBankState() const captchaNumbers = useMemo(() => { return { @@ -135,7 +137,7 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon debug: resp.detail, }); case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`The operation needs a confirmation to complete.`, diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index bbe33eb57..1a431a939 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -21,6 +21,7 @@ import { useState } from "preact/hooks"; import { PaytoWireTransferForm, doAutoFocus } from "./PaytoWireTransferForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; import { usePreferences } from "../hooks/preferences.js"; +import { useBankState } from "../hooks/bank-state.js"; /** * Let the user choose a payment option, @@ -28,7 +29,7 @@ import { usePreferences } from "../hooks/preferences.js"; */ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode { const { i18n } = useTranslationContext(); - const [settings] = usePreferences(); + const [bankState] = useBankState(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); @@ -59,7 +60,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ <div class="mt-1 flex items-center text-sm text-gray-500"> <i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate> </div> - {!!settings.currentWithdrawalOperationId && + {!!bankState.currentWithdrawalOperationId && <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre"> <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true"> <circle cx="3" cy="3" r="3" /> diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 2ef93d35c..0c6c9ada2 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -47,6 +47,7 @@ import { } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; +import { useBankState } from "../hooks/bank-state.js"; const logger = new Logger("PaytoWireTransferForm"); @@ -74,6 +75,7 @@ export function PaytoWireTransferForm({ const [iban, setIban] = useState<string | undefined>(toAccount); const [subject, setSubject] = useState<string | undefined>(); const [amount, setAmount] = useState<string | undefined>(); + const [, updateBankState] = useBankState() const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>( undefined, @@ -190,7 +192,7 @@ export function PaytoWireTransferForm({ debug: resp.detail, }) case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`The operation needs a confirmation to complete.`, diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index bee36e7ad..6e13ae657 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -39,6 +39,7 @@ import { OperationState } from "./OperationState/index.js"; import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; +import { useBankState } from "../hooks/bank-state.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(InputAmount); @@ -51,7 +52,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { onCancel: () => void; }): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = usePreferences() + const [settings] = usePreferences() + const [bankState, updateBankState] = useBankState(); const { state: credentials } = useBackendState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials @@ -60,11 +62,11 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`); const [notification, notify, handleError] = useLocalNotification() - if (!!settings.currentWithdrawalOperationId) { + if (!!bankState.currentWithdrawalOperationId) { return <Attention type="warning" title={i18n.str`There is an operation already`}> <span ref={focus ? doAutoFocus : undefined} /> <i18n.Translate> - To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a> + To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${bankState.currentWithdrawalOperationId}`}>here</a> </i18n.Translate> </Attention> } @@ -99,7 +101,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { i18n.str`Server responded with an invalid withdraw URI`, i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`); } else { - updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) + updateBankState("currentWithdrawalOperationId", uri.withdrawalOperationId) goToConfirmOperation(uri.withdrawalOperationId); } } else { diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 6f18e1283..206b51008 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -48,6 +48,7 @@ import { LoginForm } from "./LoginForm.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; +import { useBankState } from "../hooks/bank-state.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -75,6 +76,7 @@ export function WithdrawalConfirmationQuestion({ const { state: credentials } = useBackendState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials const withdrawalInfo = useWithdrawalDetails(withdrawUri.withdrawalOperationId) + const [, updateBankState] = useBankState() if (!withdrawalInfo) { return <Loading /> } @@ -154,7 +156,7 @@ export function WithdrawalConfirmationQuestion({ debug: resp.detail, }) case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`The operation needs a confirmation to complete.`, diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx index 7060b7a98..4bb3b4d7b 100644 --- a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx @@ -20,12 +20,12 @@ import { stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { + Attention, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Attention } from "@gnu-taler/web-util/browser"; import { useBankCoreApiContext } from "../context/config.js"; -import { usePreferences } from "../hooks/preferences.js"; +import { useBankState } from "../hooks/bank-state.js"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; const logger = new Logger("AccountPage"); @@ -44,7 +44,8 @@ export function WithdrawalOperationPage({ }); const parsedUri = parseWithdrawUri(uri); const { i18n } = useTranslationContext(); - const [settings, updateSettings] = usePreferences(); + const [, updateBankState] = useBankState(); + if (!parsedUri) { return <Attention type="danger" title={i18n.str`The Withdrawal URI is not valid`}> @@ -56,7 +57,7 @@ export function WithdrawalOperationPage({ <WithdrawalQRCode withdrawUri={parsedUri} onClose={() => { - updateSettings("currentWithdrawalOperationId", undefined) + updateBankState("currentWithdrawalOperationId", undefined) onContinue() }} /> diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index 1bf21f62e..28875bde6 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -10,6 +10,7 @@ import { LoginForm } from "../LoginForm.js"; import { ProfileNavigation } from "../ProfileNavigation.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { AccountForm } from "../admin/AccountForm.js"; +import { useBankState } from "../../hooks/bank-state.js"; export function ShowAccountDetails({ account, @@ -30,6 +31,7 @@ export function ShowAccountDetails({ const [update, setUpdate] = useState(false); const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountReconfiguration | undefined>(); const [notification, notify, handleError] = useLocalNotification() + const [, updateBankState] = useBankState() const result = useAccountDetails(account); if (!result) { @@ -97,7 +99,7 @@ export function ShowAccountDetails({ debug: resp.detail, }) case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`Cashout created but confirmation is required.`, diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx index ed074b9c4..0ff1cf725 100644 --- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -10,6 +10,7 @@ import { ProfileNavigation } from "../ProfileNavigation.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; import { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util"; +import { useBankState } from "../../hooks/bank-state.js"; export function UpdateAccountPassword({ account: accountName, @@ -30,6 +31,7 @@ export function UpdateAccountPassword({ const [current, setCurrent] = useState<string | undefined>(); const [password, setPassword] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>(); + const [, updateBankState] = useBankState() const accountIsTheCurrentUser = credentials.status === "loggedIn" ? credentials.username === accountName : false @@ -75,7 +77,7 @@ export function UpdateAccountPassword({ title: i18n.str`Your current password doesn't match, can't change to a new password.` }) case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`Cashout created but confirmation is required.`, diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 330ebf3a9..3f7d62935 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -10,6 +10,7 @@ import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { useBankState } from "../../hooks/bank-state.js"; export function RemoveAccount({ account, @@ -30,6 +31,7 @@ export function RemoveAccount({ const token = state.status !== "loggedIn" ? undefined : state.token const { api } = useBankCoreApiContext() const [notification, notify, handleError] = useLocalNotification() + const [, updateBankState] = useBankState() if (!result) { return <Loading /> @@ -90,7 +92,7 @@ export function RemoveAccount({ debug: resp.detail, }) case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`The operation needs a confirmation to complete.`, diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 9ee5cbeaf..d97a00a2e 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -50,6 +50,7 @@ import { import { LoginForm } from "../LoginForm.js"; import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { useBankState } from "../../hooks/bank-state.js"; interface Props { account: string; @@ -83,6 +84,7 @@ export function CreateCashout({ } = useEstimator(); const { state: credentials } = useBackendState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials + const [, updateBankState] = useBankState() const { api, config } = useBankCoreApiContext() const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, }); @@ -199,7 +201,7 @@ export function CreateCashout({ } else { switch (resp.case) { case HttpStatusCode.Accepted: { - resp.body.challenge_id; + updateBankState("currentChallengeId", resp.body.challenge_id) return notify({ type: "info", title: i18n.str`Cashout created but confirmation is required.`, |