summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-11-24 14:43:19 -0300
committerSebastian <sebasjm@gmail.com>2023-11-24 14:43:19 -0300
commit6f7512be74005f169f98654f11360fd4fff9162f (patch)
treec0b609dfffb816e3182b440769de39f568008f45
parent3df64dd455f51345823c4ace802ea070ca27dbb8 (diff)
downloadwallet-core-6f7512be74005f169f98654f11360fd4fff9162f.tar.gz
wallet-core-6f7512be74005f169f98654f11360fd4fff9162f.tar.bz2
wallet-core-6f7512be74005f169f98654f11360fd4fff9162f.zip
show cashout details
-rw-r--r--packages/demobank-ui/src/Routing.tsx2
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx57
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts41
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx17
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx1
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx2
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx2
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx4
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx3
-rw-r--r--packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx2
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx93
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx52
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx431
13 files changed, 422 insertions, 285 deletions
diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx
index 733d55a0f..8ed66d4cf 100644
--- a/packages/demobank-ui/src/Routing.tsx
+++ b/packages/demobank-ui/src/Routing.tsx
@@ -258,7 +258,7 @@ export function Routing(): VNode {
<ShowCashoutDetails
id={cid}
onCancel={() => {
- route("/account");
+ route("/my-cashouts");
}}
/>
)}
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 651a7a034..59bb4a16b 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -93,7 +93,7 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
{Object.entries(txByDate).map(([date, txs], idx) => {
return <Fragment key={idx}>
<tr class="border-t border-gray-200">
- <th colSpan={4} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">
+ <th colSpan={6} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">
{date}
</th>
</tr>
@@ -102,9 +102,12 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
const confirmationTime = item.confirmation_time
? item.confirmation_time.t_s === "never" ? i18n.str`never` : format(item.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")
: "-"
- return (<tr key={idx} class="border-b border-gray-200 last:border-none">
+ return (<tr key={idx} class="border-b border-gray-200 hover:bg-gray-200 last:border-none">
- <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <td onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }} class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">{creationTime}</div>
{/* <dl class="font-normal sm:hidden">
<dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
@@ -128,18 +131,28 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
</dd>
</dl> */}
</td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{confirmationTime}</td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} spec={resp.body.regional_currency_specification} /></td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} spec={resp.body.fiat_currency_specification} /></td>
+ <td onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }}class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 cursor-pointer">{confirmationTime}</td>
+ <td onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }}class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} spec={resp.body.regional_currency_specification} /></td>
+ <td onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }}class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} spec={resp.body.fiat_currency_specification} /></td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.status}</td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
- <a href="#" onClick={(e) => {
+ <td onClick={(e) => {
e.preventDefault();
onSelected(item.id);
- }}>
- {item.subject}
- </a>
+ }}class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 cursor-pointer">{item.status}</td>
+ <td onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }} class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ {item.subject}
</td>
</tr>)
})}
@@ -150,27 +163,9 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
</table>
- {/* <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
- <div class="flex flex-1 justify-between sm:justify-end">
- <button
- class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- disabled={!onPrev}
- onClick={onPrev}
- >
- <i18n.Translate>First page</i18n.Translate>
- </button>
- <button
- class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- disabled={!onNext}
- onClick={onNext}
- >
- <i18n.Translate>Next</i18n.Translate>
- </button>
- </div>
- </nav> */}
+
</div>
</div>
);
- // }
}
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index b483a5420..bb9f5801e 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -18,7 +18,7 @@ import { useState } from "preact/hooks";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
import { useBackendState } from "./backend.js";
-import { AccessToken, AmountJson, AmountString, Amounts, OperationOk, TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError } from "@gnu-taler/taler-util";
+import { AccessToken, AmountJson, AmountString, Amounts, OperationOk, TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError, opFixedSuccess } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "../pages/WithdrawalOperationPage.js";
@@ -174,6 +174,43 @@ type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number }
function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
return c !== undefined
}
+export function useOnePendingCashouts(account: string) {
+ const { state: credentials } = useBackendState();
+ const { api, config } = useBankCoreApiContext();
+ const token = credentials.status !== "loggedIn" ? undefined : credentials.token
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token })
+ if (list.type !== "ok") {
+ return list;
+ }
+ const pendingCashout = list.body.cashouts.find(c => c.status === "pending")
+ if (!pendingCashout) return opFixedSuccess(undefined)
+ const cashoutInfo = await api.getCashoutById({ username, token }, pendingCashout?.cashout_id)
+ if (cashoutInfo.type !== "ok") {
+ return cashoutInfo;
+ }
+ return opFixedSuccess({ ...cashoutInfo.body, id: pendingCashout.cashout_id })
+ }
+
+ const { data, error } = useSWR<OperationOk<CashoutWithId | undefined> | TalerCoreBankErrorsByMethod<"getAccountCashouts"> | TalerCoreBankErrorsByMethod<"getCashoutById">, TalerHttpError>(
+ !config.allow_conversion ? undefined : [account, token, "getAccountCashouts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
export function useCashouts(account: string) {
const { state: credentials } = useBackendState();
const { api, config } = useBankCoreApiContext();
@@ -182,13 +219,11 @@ export function useCashouts(account: string) {
async function fetcher([username, token]: [string, AccessToken]) {
const list = await api.getAccountCashouts({ username, token })
if (list.type !== "ok") {
- console.error(list)
return list;
}
const all: Array<CashoutWithId | undefined> = await Promise.all(list.body.cashouts.map(c => {
return api.getCashoutById({ username, token }, c.cashout_id).then(r => {
if (r.type === "fail") {
- console.error("failed", r)
return undefined
}
return { ...r.body, id: c.cashout_id }
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index cfee684fa..d760543c6 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -21,6 +21,8 @@ import { Transactions } from "../../components/Transactions/index.js";
import { usePreferences } from "../../hooks/preferences.js";
import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
+import { useCashouts, useOnePendingCashouts } from "../../hooks/circuit.js";
+import { TalerError } from "@gnu-taler/taler-util";
export function InvalidIbanView({ error }: State.InvalidIban) {
return (
@@ -57,8 +59,23 @@ export function ReadyView({ account, limit, goToConfirmOperation }: State.Ready)
return <Fragment>
<ShowDemoInfo />
+ <PendingCashouts account={account}/>
<PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
<Transactions account={account} />
</Fragment>;
}
+
+function PendingCashouts({account}: {account: string}):VNode {
+ const { i18n } = useTranslationContext();
+ const result = useOnePendingCashouts(account)
+ if (!result || result instanceof TalerError || result.type !== "ok" || !result.body) {
+ return <Fragment />
+ }
+
+ return <Attention title={i18n.str`You have pending cashout operation to complete`} >
+ <i18n.Translate>
+ Cashout with subject "{result.body.subject}", look for the code and complete the operation <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href={`#/cashout/${result.body.id}`}>here</a>.
+ </i18n.Translate>
+ </Attention>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 34c39e9d3..24012cd2b 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -46,7 +46,6 @@ export function BankFrame({
useEffect(() => {
if (error) {
const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
- console.log(error)
if (error instanceof Error) {
notifyException(i18n.str`Internal error, please report.`, error)
} else {
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 76d20867e..bbe33eb57 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -33,7 +33,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
return (
- <div class="mt-2">
+ <div class="mt-4">
<fieldset>
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index e035c7fed..33bf18abc 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -447,7 +447,7 @@ export function InputAmount(
<input
type="number"
data-left={left}
- class="text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
+ class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
placeholder="0.00" aria-describedby="price-currency"
ref={ref}
name={name}
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
index 1a4b4b865..61a55fe16 100644
--- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
@@ -3,7 +3,7 @@ import { Fragment, VNode, h } from "preact";
import { useBankCoreApiContext } from "../context/config.js";
import { assertUnreachable } from "./WithdrawalOperationPage.js";
-export function ProfileNavigation({ current }: { current: "details" | "credentials" | "cashouts" }): VNode {
+export function ProfileNavigation({ current, noCashout }: { noCashout?: boolean, current: "details" | "credentials" | "cashouts" }): VNode {
const { i18n } = useTranslationContext()
const { config } = useBankCoreApiContext()
return <div>
@@ -44,7 +44,7 @@ export function ProfileNavigation({ current }: { current: "details" | "credentia
<span><i18n.Translate>Credentials</i18n.Translate></span>
<span aria-hidden="true" data-selected={current == "credentials"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
</a>
- {config.allow_conversion ?
+ {config.allow_conversion && !noCashout ?
<a href="#/my-cashouts" data-selected={current == "cashouts"} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
<span>Cashouts</span>
<span aria-hidden="true" data-selected={current == "cashouts"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 3ef3f568c..4332284e8 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -99,7 +99,7 @@ export function ShowAccountDetails({
<Fragment>
<LocalNotificationBanner notification={notification} />
{accountIsTheCurrentUser ?
- <ProfileNavigation current="details" />
+ <ProfileNavigation current="details" noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} />
:
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Account "{account}"</i18n.Translate>
@@ -128,6 +128,7 @@ export function ShowAccountDetails({
<AccountForm
focus={update}
+ noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined}
username={account}
template={result.body}
purpose={update ? "update" : "show"}
diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
index d7f5155c9..3c00ad1b8 100644
--- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -81,7 +81,7 @@ export function UpdateAccountPassword({
<Fragment>
<LocalNotificationBanner notification={notification} />
{accountIsTheCurrentUser ?
- <ProfileNavigation current="credentials" /> :
+ <ProfileNavigation current="credentials" noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} /> :
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Account "{accountName}"</i18n.Translate>
</h1>
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index b38d40012..526deeeab 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -27,11 +27,13 @@ export function AccountForm({
purpose,
onChange,
focus,
+ noCashout,
children,
}: {
focus?: boolean,
children: ComponentChildren,
username?: string,
+ noCashout?: boolean,
template: TalerCorebankApi.AccountData | undefined;
onChange: (a: AccountFormData | undefined) => void;
purpose: "create" | "update" | "show";
@@ -44,14 +46,14 @@ export function AccountForm({
const { i18n } = useTranslationContext();
function updateForm(newForm: typeof initial): void {
-
+ console.log(newForm)
const parsed = !newForm.cashout_payto_uri
? undefined
: buildPayto("iban", newForm.cashout_payto_uri, undefined);;
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
cashout_payto_uri: (!newForm.cashout_payto_uri
- ? i18n.str`required`
+ ? undefined
: !parsed
? i18n.str`does not follow the pattern`
: !parsed.isKnown || parsed.targetType !== "iban"
@@ -81,10 +83,10 @@ export function AccountForm({
if (errors) {
onChange(undefined)
} else {
- const cashout = buildPayto("iban", newForm.cashout_payto_uri!, undefined)
+ const cashout = !newForm.cashout_payto_uri? undefined :buildPayto("iban", newForm.cashout_payto_uri, undefined)
const account: AccountFormData = {
...newForm as any,
- cashout_payto_uri: stringifyPaytoUri(cashout)
+ cashout_payto_uri: !cashout ? undefined : stringifyPaytoUri(cashout)
}
onChange(account);
}
@@ -194,6 +196,9 @@ export function AccountForm({
onChange={(e) => {
if (form.contact_data) {
form.contact_data.email = e.currentTarget.value;
+ if (!form.contact_data.email) {
+ form.contact_data.email = undefined
+ }
updateForm(structuredClone(form));
}
}}
@@ -220,12 +225,15 @@ export function AccountForm({
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="phone"
id="phone"
- disabled={purpose !== "create"}
+ disabled={purpose === "show"}
value={form.contact_data?.phone ?? ""}
data-error={!!errors?.contact_data?.phone && form.contact_data?.phone !== undefined}
onChange={(e) => {
if (form.contact_data) {
form.contact_data.phone = e.currentTarget.value;
+ if (!form.contact_data.email) {
+ form.contact_data.email = undefined
+ }
updateForm(structuredClone(form));
}
}}
@@ -240,44 +248,45 @@ export function AccountForm({
</div>
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="cashout"
- >
- {i18n.str`Cashout IBAN`}
- {purpose !== "show" && <b style={{ color: "red" }}> *</b>}
- </label>
- <div class="mt-2">
- <input
- type="text"
- ref={focus && purpose === "update" ? doAutoFocus : undefined}
- data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="cashout"
- id="cashout"
- disabled={purpose === "show"}
- value={form.cashout_payto_uri ?? ""}
- onChange={(e) => {
- form.cashout_payto_uri = e.currentTarget.value as PaytoString;
- updateForm(structuredClone(form));
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.cashout_payto_uri}
- isDirty={form.cashout_payto_uri !== undefined}
- />
+ {!noCashout &&
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="cashout"
+ >
+ {i18n.str`Cashout IBAN`}
+ {purpose !== "show" && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ ref={focus && purpose === "update" ? doAutoFocus : undefined}
+ data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="cashout"
+ id="cashout"
+ disabled={purpose === "show"}
+ value={form.cashout_payto_uri ?? ""}
+ onChange={(e) => {
+ form.cashout_payto_uri = e.currentTarget.value as PaytoString;
+ if (!form.cashout_payto_uri) {
+ form.cashout_payto_uri= undefined
+ }
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.cashout_payto_uri}
+ isDirty={form.cashout_payto_uri !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
- </p>
- </div>
- <div class="sm:col-span-5">
- <pre>
- {JSON.stringify(errors, undefined, 2)}
- </pre>
- </div>
+ }
+
</div>
</div>
{children}
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 2f77f3960..3d3f30250 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -18,7 +18,8 @@ import {
TalerError,
TranslatedString,
encodeCrock,
- getRandomBytes
+ getRandomBytes,
+ parsePaytoUri
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -83,7 +84,7 @@ export function CreateCashout({
const creds = credentials.status !== "loggedIn" ? undefined : credentials
const { api, config } = useBankCoreApiContext()
- const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount: "2" });
+ const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, });
const [notification, notify, handleError] = useLocalNotification()
const info = useConversionInfo();
@@ -171,7 +172,7 @@ export function CreateCashout({
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
: Amounts.cmp(calc.credit, sellFee) === -1
- ? i18n.str`the total amount to transfer does not cover the fees`
+ ? i18n.str`need to be higher due to fees`
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
@@ -242,7 +243,9 @@ export function CreateCashout({
}
})
}
-
+ const cashoutAccount = !resultAccount.body.cashout_payto_uri ? undefined :
+ parsePaytoUri(resultAccount.body.cashout_payto_uri);
+ const cashoutAccountName = !cashoutAccount ? undefined : cashoutAccount.targetPath
return (
<div>
<LocalNotificationBanner notification={notification} />
@@ -275,6 +278,24 @@ export function CreateCashout({
<RenderAmount value={sellFee} spec={fiat_currency_specification} />
</dd>
</div>
+ {cashoutAccountName ?
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span><i18n.Translate>To account</i18n.Translate></span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ {cashoutAccountName }
+ </dd>
+ </div> :
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <Attention type="warning" title={i18n.str`No cashout account`}>
+ <i18n.Translate>
+ Before doing a cashout you need to complete your profile
+ </i18n.Translate>
+ </Attention>
+ </div>
+ }
+
</dl>
</section>
@@ -301,9 +322,10 @@ export function CreateCashout({
<input
ref={focus ? doAutoFocus : undefined}
type="text"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ class="block w-full rounded-md disabled:bg-gray-200 border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="subject"
id="subject"
+ disabled={!resultAccount.body.cashout_payto_uri}
data-error={!!errors?.subject && form.subject !== undefined}
value={form.subject ?? ""}
onChange={(e) => {
@@ -346,7 +368,7 @@ export function CreateCashout({
left
currency={limit.currency}
value={trimmedAmountStr}
- onChange={(value) => {
+ onChange={!resultAccount.body.cashout_payto_uri ? undefined : (value) => {
form.amount = value;
updateForm(structuredClone(form));
}}
@@ -411,7 +433,7 @@ export function CreateCashout({
{/* channel */}
<div class="sm:col-span-5">
- <label
+ <label
class="block text-sm font-medium leading-6 text-gray-900"
for="channel"
>
@@ -421,16 +443,19 @@ export function CreateCashout({
<div class="mt-2 max-w-xl text-sm text-gray-500">
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
- <label onClick={()=>{
+ <label onClick={() => {
+ if (!resultAccount.body.contact_data?.email) return;
form.channel = TanChannel.EMAIL
updateForm(structuredClone(form))
- }} data-selected={form.channel === TanChannel.EMAIL} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ }} data-disabled={!resultAccount.body.contact_data?.email} data-selected={form.channel === TanChannel.EMAIL}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
<input type="radio" name="channel" value="Newsletter" class="sr-only" />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
<i18n.Translate>Email</i18n.Translate>
</span>
+ {!resultAccount.body.contact_data?.email && i18n.str`add a email in your profile to enable this option`}
</span>
</span>
<svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -438,16 +463,19 @@ export function CreateCashout({
</svg>
</label>
- <label onClick={()=>{
+ <label onClick={() => {
+ if (!resultAccount.body.contact_data?.phone) return;
form.channel = TanChannel.SMS
updateForm(structuredClone(form))
- }} data-selected={form.channel === TanChannel.SMS} class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
- <input type="radio" name="channel" value="Existing Customers" class="sr-only" />
+ }} data-disabled={!resultAccount.body.contact_data?.phone} data-selected={form.channel === TanChannel.SMS}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600">
+ <input type="radio" name="channel" value="Existing Customers" class="sr-only" />
<span class="flex flex-1">
<span class="flex flex-col">
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
<i18n.Translate>SMS</i18n.Translate>
</span>
+ {!resultAccount.body.contact_data?.phone && i18n.str`add a phone number in your profile to enable this option`}
</span>
</span>
<svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index 52ff713e2..6fd9eb18c 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ Amounts,
TalerError,
TranslatedString
} from "@gnu-taler/taler-util";
@@ -32,7 +33,7 @@ import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser";
import { useBankCoreApiContext } from "../../context/config.js";
import { useBackendState } from "../../hooks/backend.js";
import {
- useCashoutDetails
+ useCashoutDetails, useConversionInfo
} from "../../hooks/circuit.js";
import {
undefinedIfEmpty,
@@ -40,6 +41,7 @@ import {
} from "../../utils.js";
import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
interface Props {
id: string;
@@ -58,7 +60,12 @@ export function ShowCashoutDetails({
const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
const [code, setCode] = useState<string | undefined>(undefined);
const [notification, notify, handleError] = useLocalNotification()
+ const info = useConversionInfo();
+ if (Number.isNaN(cid)) {
+ //TODO: better error message
+ return <div>cashout id should be a number</div>
+ }
if (!result) {
return <Loading />
}
@@ -74,206 +81,252 @@ export function ShowCashoutDetails({
default: assertUnreachable(result)
}
}
- if (Number.isNaN(cid)) {
- //TODO: better error message
- return <div>cashout id should be a number</div>
+ if (!info) {
+ return <Loading />
+ }
+
+ if (info instanceof TalerError) {
+ return <ErrorLoading error={info} />
}
+
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
});
const isPending = String(result.body.status).toUpperCase() === "PENDING";
+ const { fiat_currency_specification, regional_currency_specification } = info.body
+ async function doAbortCashout() {
+ if (!creds) return;
+ await handleError(async () => {
+ const resp = await api.abortCashoutById(creds, cid);
+ if (resp.type === "ok") {
+ onCancel();
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "already-confirmed": return notify({
+ type: "error",
+ title: i18n.str`Cashout was already confimed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "cashout-not-supported": return notify({
+ type: "error",
+ title: i18n.str`Cashout operation is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: {
+ assertUnreachable(resp)
+ }
+ }
+ }
+ })
+ }
+ async function doConfirmCashout() {
+ if (!creds || !code) return;
+ await handleError(async () => {
+ const resp = await api.confirmCashoutById(creds, cid, {
+ tan: code,
+ });
+ if (resp.type === "ok") {
+ mutate(() => true)//clean cashout state
+ } else {
+ switch (resp.case) {
+ case "not-found": return notify({
+ type: "error",
+ title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "no-enough-balance": return notify({
+ type: "error",
+ title: i18n.str`The account does not have sufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "incorrect-exchange-rate": return notify({
+ type: "error",
+ title: i18n.str`The exchange rate was incorrectly applied`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "already-aborted": return notify({
+ type: "error",
+ title: i18n.str`The cashout operation is already aborted.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case "no-cashout-payto": return notify({
+ type: "error",
+ title: i18n.str`Missing destination account.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "too-many-attempts": return notify({
+ type: "error",
+ title: i18n.str`Too many failed attempts.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "cashout-not-supported": return notify({
+ type: "error",
+ title: i18n.str`Cashout operation is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ case "invalid-code": return notify({
+ type: "error",
+ title: i18n.str`The code for this cashout is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ })
+ default: assertUnreachable(resp)
+ }
+ }
+ })
+ }
+
return (
<div>
<LocalNotificationBanner notification={notification} />
- <h1>Cashout details {id}</h1>
- <form class="pure-form">
- <fieldset>
- <label>
- <i18n.Translate>Subject</i18n.Translate>
- </label>
- <input readOnly value={result.body.subject} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Created</i18n.Translate>
- </label>
- <input readOnly value={result.body.creation_time.t_s === "never" ? i18n.str`never` : format(result.body.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Confirmed</i18n.Translate>
- </label>
- <input readOnly value={result.body.confirmation_time === undefined ? "-" :
- (result.body.confirmation_time.t_s === "never" ?
- i18n.str`never` :
- format(result.body.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss"))
- } />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Debited</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_debit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Credit</i18n.Translate>
- </label>
- <input readOnly value={result.body.amount_credit} />
- </fieldset>
- <fieldset>
- <label>
- <i18n.Translate>Status</i18n.Translate>
- </label>
- <input readOnly value={result.body.status} />
- </fieldset>
- {/* <fieldset>
- <label>
- <i18n.Translate>Destination</i18n.Translate>
- </label>
- <input readOnly value={result.body.credit_payto_uri} />
- </fieldset> */}
- {isPending ? (
- <fieldset>
- <label>
- <i18n.Translate>Code</i18n.Translate>
- </label>
- <input
- value={code ?? ""}
- onChange={(e) => {
- setCode(e.currentTarget.value);
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+
+ <section class="rounded-sm px-4">
+ <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout detail</i18n.Translate></h2>
+ <dl class="mt-8 space-y-4">
+ <div class="justify-between items-center flex">
+ <dt class="text-sm text-gray-600"><i18n.Translate>Subject</i18n.Translate></dt>
+ <dd class="text-sm ">{result.body.subject}</dd>
+ </div>
+
+
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span><i18n.Translate>Status</i18n.Translate></span>
+ </dt>
+ <dd data-status={result.body.status} class="text-sm uppercase data-[status=pending]:text-yellow-600 data-[status=aborted]:text-red-600 data-[status=confirmed]:text-green-600" >
+ {result.body.status}
+ </dd>
+ </div>
+ </dl>
+ </section>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <dl class="space-y-4">
+
+ {result.body.creation_time.t_s !== "never" ?
+ <div class="justify-between items-center flex ">
+ <dt class=" text-gray-600"><i18n.Translate>Created</i18n.Translate></dt>
+ <dd class="text-sm ">
+ {format(result.body.creation_time.t_s * 1000, "dd/MM/yyyy HH:mm:ss")}
+ </dd>
+ </div>
+ : undefined}
+
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-gray-600"><i18n.Translate>Debited</i18n.Translate></dt>
+ <dd class=" font-medium">
+ <RenderAmount value={Amounts.parseOrThrow(result.body.amount_debit)} negative withColor spec={regional_currency_specification} />
+ </dd>
+ </div>
+
+ <div class="flex items-center justify-between border-t-2 afu pt-4">
+ <dt class="flex items-center text-gray-600">
+ <span><i18n.Translate>Credited</i18n.Translate></span>
+
+ </dt>
+ <dd class="text-sm ">
+ <RenderAmount value={Amounts.parseOrThrow(result.body.amount_credit)} withColor spec={fiat_currency_specification} />
+ </dd>
+ </div>
+
+ {result.body.confirmation_time && result.body.confirmation_time.t_s !== "never" ?
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class=" font-medium"><i18n.Translate>Confirmed</i18n.Translate></dt>
+ <dd class=" font-medium">
+ {format(result.body.confirmation_time.t_s * 1000, "dd/MM/yyyy HH:mm:ss")}
+ </dd>
+ </div>
+ : undefined}
+ </dl>
+ </div>
+ </div>
+ </div>
+
+ </div>
+
+ {!isPending ? undefined :
+ <Fragment>
+
+ <div />
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
}}
- />
- <ShowInputErrorLabel
- message={errors?.code}
- isDirty={code !== undefined}
- />
- </fieldset>
- ) : undefined}
- </form>
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <label for="withdraw-amount">
+ Enter the confirmation code
+ </label>
+ <div class="mt-2">
+ <div class="relative rounded-md shadow-sm">
+ <input
+ type="text"
+ // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ aria-describedby="answer"
+ autoFocus
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={code ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCode(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.code} isDirty={code !== undefined} />
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button"
+ class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ onClick={doAbortCashout}
+ >
+ <i18n.Translate>Abort</i18n.Translate></button>
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!!errors}
+ onClick={(e) => {
+ doConfirmCashout()
+ }}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
+ </Fragment>}
+ </div>
+
<br />
<div style={{ display: "flex", justifyContent: "space-between" }}>
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onCancel();
- }}
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
>
- {i18n.str`Back`}
- </button>
- {isPending ? (
- <div>
- <button
- type="submit"
- class="pure-button pure-button-primary button-error"
- onClick={async (e) => {
- e.preventDefault();
- if (!creds) return;
- await handleError(async () => {
- const resp = await api.abortCashoutById(creds, cid);
- if (resp.type === "ok") {
- onCancel();
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "already-confirmed": return notify({
- type: "error",
- title: i18n.str`Cashout was already confimed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "cashout-not-supported": return notify({
- type: "error",
- title: i18n.str`Cashout operation is not supported.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: {
- assertUnreachable(resp)
- }
- }
- }
- })
- }}
- >
- {i18n.str`Abort`}
- </button>
- &nbsp;
- <button
- type="submit"
- disabled={!code}
- class="pure-button pure-button-primary "
- onClick={async (e) => {
- e.preventDefault();
- if (!creds || !code) return;
- await handleError(async () => {
- const resp = await api.confirmCashoutById(creds, cid, {
- tan: code,
- });
- if (resp.type === "ok") {
- mutate(() => true)//clean cashout state
- } else {
- switch (resp.case) {
- case "not-found": return notify({
- type: "error",
- title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "no-enough-balance": return notify({
- type: "error",
- title: i18n.str`The account does not have sufficient funds`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "incorrect-exchange-rate": return notify({
- type: "error",
- title: i18n.str`The exchange rate was incorrectly applied`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "already-aborted": return notify({
- type: "error",
- title: i18n.str`The cashout operation is already aborted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case "no-cashout-payto": return notify({
- type: "error",
- title: i18n.str`Missing destination account.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "too-many-attempts": return notify({
- type: "error",
- title: i18n.str`Too many failed attempts.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case "cashout-not-supported": return notify({
- type: "error",
- title: i18n.str`Cashout operation is not supported.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
- }
- }
- })
- }}
- >
- {i18n.str`Confirm`}
- </button>
- </div>
- ) : (
- <div />
- )}
+ <i18n.Translate>Cancel</i18n.Translate></button>
</div>
</div>
);