diff options
author | Sebastian <sebasjm@gmail.com> | 2023-12-04 09:43:03 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-12-04 09:43:23 -0300 |
commit | 8407a1d52e3a89f9c005f9820586d2d0a123c177 (patch) | |
tree | 2e467910a656f5b34758b4b4d2ceb9e8f41cfc00 /packages/demobank-ui | |
parent | 8616c67de8de79a39298299eac9dc368749bfc7a (diff) |
api sync, withdrawal info without password, account creation WIP
Diffstat (limited to 'packages/demobank-ui')
19 files changed, 318 insertions, 169 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index 59bb4a16b..115a2e014 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -21,6 +21,7 @@ import { Fragment, h, VNode } from "preact"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; import { useConversionInfo } from "../../hooks/circuit.js"; +import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { const { i18n } = useTranslationContext(); @@ -57,7 +58,7 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { return <Loading /> } if (resp instanceof TalerError) { - return <ErrorLoading error={resp} /> + return <ErrorLoadingWithDebug error={resp} /> } if (!cashouts.length) return <div /> const txByDate = cashouts.reduce((prev, cur) => { @@ -105,9 +106,9 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { return (<tr key={idx} class="border-b border-gray-200 hover:bg-gray-200 last:border-none"> <td onClick={(e) => { - e.preventDefault(); - onSelected(item.id); - }} class="relative py-2 pl-2 pr-2 text-sm "> + 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> @@ -132,26 +133,26 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { </dl> */} </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> + 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> + 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> + 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 onClick={(e) => { - e.preventDefault(); - onSelected(item.id); - }}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"> + e.preventDefault(); + onSelected(item.id); + }} 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>) diff --git a/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx new file mode 100644 index 000000000..8cdac4561 --- /dev/null +++ b/packages/demobank-ui/src/components/ErrorLoadingWithDebug.tsx @@ -0,0 +1,9 @@ +import { ErrorLoading } from "@gnu-taler/web-util/browser"; +import { usePreferences } from "../hooks/preferences.js"; +import { VNode, h } from "preact"; +import { TalerError } from "@gnu-taler/taler-util"; + +export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode { + const [pref] = usePreferences(); + return <ErrorLoading error={error} showDetail={pref.showDebugInfo} /> +} diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 1e09c444a..fc1cff129 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -14,8 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AccessToken, TalerBankIntegrationResultByMethod, TalerCoreBankResultByMethod, TalerHttpError } from "@gnu-taler/taler-util"; -import { useState } from "preact/hooks"; +import { AccessToken, TalerBankIntegrationResultByMethod, TalerCoreBankResultByMethod, TalerHttpError, WithdrawalOperationStatus } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; import { useBackendState } from "./backend.js"; @@ -56,18 +56,17 @@ export function useAccountDetails(account: string) { return undefined; } -// FIXME: should poll export function useWithdrawalDetails(wid: string) { - // const { state: credentials } = useBackendState(); const { api } = useBankCoreApiContext(); + const [latestStatus, setLatestStatus] = useState<WithdrawalOperationStatus>() - async function fetcher([wid]: [string]) { - return await api.getIntegrationAPI().getWithdrawalOperationById(wid) + async function fetcher([wid, old_state]: [string, WithdrawalOperationStatus | undefined]) { + return await api.getWithdrawalById(wid, old_state === undefined ? undefined : { old_state, timeoutMs: 15000 }) } - const { data, error } = useSWR<TalerBankIntegrationResultByMethod<"getWithdrawalOperationById">, TalerHttpError>( - [wid, "getWithdrawalById"], fetcher, { - refreshInterval: 1000, + const { data, error } = useSWR<TalerCoreBankResultByMethod<"getWithdrawalById">, TalerHttpError>( + [wid, latestStatus, "getWithdrawalById"], fetcher, { + refreshInterval: 3000, refreshWhenHidden: false, revalidateOnFocus: false, revalidateOnReconnect: false, @@ -78,6 +77,14 @@ export function useWithdrawalDetails(wid: string) { keepPreviousData: true, }); + const currentStatus = data !== undefined && data.type === "ok" ? data.body.status : undefined; + + useEffect(() => { + if (currentStatus !== undefined && currentStatus !== latestStatus) { + setLatestStatus(currentStatus) + } + }, [currentStatus]) + if (data) return data; if (error) return error; return undefined; @@ -110,12 +117,12 @@ export function useTransactionDetails(account: string, tid: number) { return undefined; } -export function usePublicAccounts(filterAccount: string |undefined ,initial?: number) { +export function usePublicAccounts(filterAccount: string | undefined, initial?: number) { const [offset, setOffset] = useState<number | undefined>(initial); const { api } = useBankCoreApiContext(); - async function fetcher([account, txid]: [string | undefined , number | undefined]) { - return await api.getPublicAccounts({account},{ + async function fetcher([account, txid]: [string | undefined, number | undefined]) { + return await api.getPublicAccounts({ account }, { limit: MAX_RESULT_SIZE, offset: txid ? String(txid) : undefined, order: "asc" diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 1762c6b56..0ac9ed8f1 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -132,17 +132,6 @@ export function BankFrame({ ); } -function MaybeShowDebugInfo({ info }: { info: any }): VNode { - const [settings] = usePreferences() - if (settings.showDebugInfo) { - return <pre class="whitespace-break-spaces "> - {info} - </pre> - } - return <Fragment /> -} - - function WelcomeAccount({ account: accountName }: { account: string }): VNode { const { i18n } = useTranslationContext(); return <a href="#/my-profile" class="underline underline-offset-2"> diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index da924104a..57ede87a7 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -134,6 +134,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive if (result.type === "fail") { switch (result.case) { + case "invalid-id": case "not-found": { return { status: "aborted", @@ -144,7 +145,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive }, } } - default: assertUnreachable(result.case) + default: assertUnreachable(result) } } @@ -180,9 +181,9 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive status: "ready", error: undefined, uri: parsedUri, - onClose: !creds ? (async () => { - onClose(); - return undefined + onClose: !creds ? (async () => { + onClose(); + return undefined }) : doAbort, } } diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 2de6de373..e1d32002b 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -96,7 +96,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on : undefined, }); - async function doRegistrationAndLogin(name: string, username: string, password: string) { + async function doRegistrationAndLogin(name: string, username: string, password: string, onComplete: () => void) { await handleError(async () => { const creationResponse = await api.createAccount("" as AccessToken, { name, username, password }); if (creationResponse.type === "fail") { @@ -137,6 +137,12 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on description: creationResponse.detail.hint as TranslatedString, debug: creationResponse.detail, }) + case "user-cant-set-debt": return notify({ + type: "error", + title: i18n.str`Only admin is allow to set debt limit.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) default: assertUnreachable(creationResponse) } } @@ -165,16 +171,18 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on default: assertUnreachable(resp) } } + onComplete() }) } async function doRegistrationStep() { if (!username || !password || !name) return; - await doRegistrationAndLogin(name, username, password) - setUsername(undefined); - setPassword(undefined); - setRepeatPassword(undefined); - onComplete(); + await doRegistrationAndLogin(name, username, password, () => { + setUsername(undefined); + setPassword(undefined); + setRepeatPassword(undefined); + onComplete(); + }) } async function doRandomRegistration(tries: number = 3) { @@ -183,8 +191,8 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on const pass = settings.simplePasswordForRandomAccounts ? "123" : getRandomPassword(); const username = `_${user.first}-${user.second}_` const name = `${user.first}, ${user.second}` - await doRegistrationAndLogin(name, username, pass) - onComplete(); + await doRegistrationAndLogin(name, username, pass, onComplete) + } return ( diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx index 5e6081b11..a68c085c9 100644 --- a/packages/demobank-ui/src/pages/WireTransfer.tsx +++ b/packages/demobank-ui/src/pages/WireTransfer.tsx @@ -8,6 +8,7 @@ import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { LoginForm } from "./LoginForm.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { useBackendState } from "../hooks/backend.js"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: () => void }): VNode { const { i18n } = useTranslationContext(); @@ -19,7 +20,7 @@ export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { o return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.type === "fail") { switch (result.case) { diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index e21c0917b..f8913f0ec 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -25,13 +25,14 @@ import { WithdrawUriResult } from "@gnu-taler/taler-util"; import { + Attention, ErrorLoading, Loading, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; import { mutate } from "swr"; import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser"; @@ -45,6 +46,7 @@ import { useBackendState } from "../hooks/backend.js"; import { useWithdrawalDetails } from "../hooks/access.js"; import { OperationState } from "./OperationState/index.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -54,6 +56,7 @@ interface Props { details: { account: PaytoUri, reserve: string, + username: string, amount: AmountJson, } } @@ -75,15 +78,16 @@ export function WithdrawalConfirmationQuestion({ return <Loading /> } if (withdrawalInfo instanceof TalerError) { - return <ErrorLoading error={withdrawalInfo} /> + return <ErrorLoadingWithDebug error={withdrawalInfo} /> } if (withdrawalInfo.type === "fail") { - switch(withdrawalInfo.case) { - case "not-found": return <OperationNotFound onClose={onAborted} /> - default: assertUnreachable(withdrawalInfo.case) + switch (withdrawalInfo.case) { + case "not-found": return <OperationNotFound onClose={onAborted} /> + case "invalid-id": return <OperationNotFound onClose={onAborted} /> + default: assertUnreachable(withdrawalInfo) } } - + const captchaNumbers = useMemo(() => { return { a: Math.floor(Math.random() * 10), @@ -200,67 +204,70 @@ export function WithdrawalConfirmationQuestion({ </h3> <div class="mt-3 text-sm leading-6"> - <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> - <div class="px-4 sm:px-0"> - <h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</i18n.Translate></h2> - </div> - <form - 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() - }} - > - <div class="px-4 py-6 sm:p-8"> - <label for="withdraw-amount">{i18n.str`What is`} - <em> - {captchaNumbers.a} + {captchaNumbers.b} - </em> - ? - </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={captchaAnswer ?? ""} - required + <ShouldBeSameUser username={details.username}> + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</i18n.Translate></h2> + </div> + <form + 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() + }} + > + <div class="px-4 py-6 sm:p-8"> + <label for="withdraw-amount">{i18n.str`What is`} + <em> + {captchaNumbers.a} + {captchaNumbers.b} + </em> + ? + </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={captchaAnswer ?? ""} + required - name="answer" - id="answer" - autocomplete="off" - onChange={(e): void => { - setCaptchaAnswer(e.currentTarget.value) - }} - /> + name="answer" + id="answer" + autocomplete="off" + onChange={(e): void => { + setCaptchaAnswer(e.currentTarget.value) + }} + /> + </div> + <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} /> </div> - <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== 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="text-sm font-semibold leading-6 text-gray-900" - onClick={doCancel} - > - <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" - disabled={!!errors} - onClick={(e) => { - e.preventDefault() - doTransfer() - }} - > - <i18n.Translate>Transfer</i18n.Translate> - </button> - </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="text-sm font-semibold leading-6 text-gray-900" + onClick={doCancel} + > + <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" + disabled={!!errors} + onClick={(e) => { + e.preventDefault() + doTransfer() + }} + > + <i18n.Translate>Transfer</i18n.Translate> + </button> + </div> - </form> - </div> + </form> + </div> + </ShouldBeSameUser> </div> <div class="px-4 mt-4 "> <div class="w-full"> @@ -325,6 +332,26 @@ export function WithdrawalConfirmationQuestion({ </div> </div> - </Fragment> + </Fragment > ); } + +export function ShouldBeSameUser({ username, children }: { username: string, children: ComponentChildren }): VNode { + const { state: credentials } = useBackendState(); + const { i18n } = useTranslationContext() + if (credentials.status === "loggedOut") { + return <Attention type="info" title={i18n.str`Authentication required`}> + <p>You should login as "{username}"</p> + </Attention> + } + if (credentials.username !== username) { + return <Attention type="warning" title={i18n.str`This operation was created with other username`}> + <p> + You can switch to account "{username}" and complete the operation. + </p> + </Attention> + } + return <Fragment> + {children} + </Fragment> +}
\ No newline at end of file diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 0c3d83c3b..f07790493 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -30,6 +30,7 @@ import { useWithdrawalDetails } from "../hooks/access.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; const logger = new Logger("WithdrawalQRCode"); @@ -53,12 +54,13 @@ export function WithdrawalQRCode({ return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.type === "fail") { switch (result.case) { + case "invalid-id": case "not-found": return <OperationNotFound onClose={onClose} /> - default: assertUnreachable(result.case) + default: assertUnreachable(result) } } @@ -159,6 +161,7 @@ export function WithdrawalQRCode({ <WithdrawalConfirmationQuestion withdrawUri={withdrawUri} details={{ + username: data.username, account, reserve: data.selected_reserve_pub, amount: Amounts.parseOrThrow(data.amount) diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index b5579c199..06a88c1c6 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -13,6 +13,7 @@ import { ProfileNavigation } from "../ProfileNavigation.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { AccountForm } from "../admin/AccountForm.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; export function ShowAccountDetails({ account, @@ -31,7 +32,7 @@ export function ShowAccountDetails({ credentials.username === account : false const [update, setUpdate] = useState(false); - const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>(); + const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountReconfiguration | undefined>(); const [notification, notify, handleError] = useLocalNotification() const result = useAccountDetails(account); @@ -39,7 +40,7 @@ export function ShowAccountDetails({ return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.type === "fail") { switch (result.case) { @@ -55,15 +56,7 @@ export function ShowAccountDetails({ const resp = await api.updateAccount({ token: creds.token, username: account, - }, { - cashout_payto_uri: submitAccount.cashout_payto_uri, - challenge_contact_data: undefinedIfEmpty({ - email: submitAccount.contact_data?.email, - phone: submitAccount.contact_data?.phone, - }), - is_taler_exchange: false, - name: submitAccount.name, - }); + }, submitAccount); if (resp.type === "ok") { notifyInfo(i18n.str`Account updated`); diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index c2afb195a..c8abde74b 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,10 +1,13 @@ -import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { AmountString, Amounts, PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; -import { doAutoFocus } from "../PaytoWireTransferForm.js"; +import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { useBackendContext } from "../../context/backend.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { getRandomPassword } from "../rnd.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const EMAIL_REGEX = @@ -13,6 +16,11 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; export type AccountFormData = TalerCorebankApi.AccountData & { username: string } +type MM = { + "create": (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void, + "update": (a: TalerCorebankApi.AccountReconfiguration | undefined) => void, + "show": undefined +} /** * Create valid account object to update or create * Take template as initial values for the form @@ -21,7 +29,7 @@ export type AccountFormData = TalerCorebankApi.AccountData & { username: string * @param param0 * @returns */ -export function AccountForm({ +export function AccountForm<T extends keyof MM>({ template, username, purpose, @@ -34,9 +42,10 @@ export function AccountForm({ children: ComponentChildren, username?: string, noCashout?: boolean, + admin?: boolean, template: TalerCorebankApi.AccountData | undefined; - onChange: (a: AccountFormData | undefined) => void; - purpose: "create" | "update" | "show"; + onChange: MM[T]; + purpose: T; }): VNode { const initial = initializeFromTemplate(username, template); const [form, setForm] = useState(initial); @@ -45,11 +54,20 @@ export function AccountForm({ >(undefined); const { i18n } = useTranslationContext(); + const { config } = useBankCoreApiContext() + const [debitAmount, setDebitAmount] = useState<string>() + + const [isExchange, setIsExchange] = useState<boolean>(); + const [isPublic, setIsPublic] = useState<boolean>(); + function updateForm(newForm: typeof initial): void { const parsed = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined);; + const trimmedAmountStr = debitAmount?.trim(); + const parsedAmount = Amounts.parse(`${config.currency}:${trimmedAmountStr}`); + const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({ cashout_payto_uri: (!newForm.cashout_payto_uri ? undefined @@ -74,20 +92,52 @@ export function AccountForm({ ? i18n.str`phone number can't have other than numbers` : undefined, }), + debit_threshold: !trimmedAmountStr + ? i18n.str`required` + : !parsedAmount + ? i18n.str`not valid` + : Amounts.isZero(parsedAmount) + ? i18n.str`should be greater than 0` + : undefined, name: !newForm.name ? i18n.str`required` : undefined, username: !newForm.username ? i18n.str`required` : undefined, }); setErrors(errors); setForm(newForm); + if (!onChange) return; + if (errors) { onChange(undefined) } else { - const cashout = !newForm.cashout_payto_uri? undefined :buildPayto("iban", newForm.cashout_payto_uri, undefined) - const account: AccountFormData = { - ...newForm as any, - cashout_payto_uri: !cashout ? undefined : stringifyPaytoUri(cashout) + const cashout = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined) + const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout) + switch (purpose) { + case "create": { + const result: TalerCorebankApi.RegisterAccountRequest = { + cashout_payto_uri: cashoutURI, + name: newForm.name!, + password: getRandomPassword(), + username: newForm.username!, + challenge_contact_data: undefinedIfEmpty({ + email: newForm.contact_data?.email, + phone: newForm.contact_data?.phone, + }), + debit_threshold: newForm.debit_threshold as AmountString, + // , + // internal_payto_uri + } + onChange(result) + return; + } + case "update": { + const result: TalerCorebankApi.AccountReconfiguration = { + cashout_payto_uri: cashoutURI + } + onChange(result as any) + return; + } + case "show": } - onChange(account); } } @@ -181,7 +231,6 @@ export function AccountForm({ for="email" > {i18n.str`Email`} - {purpose === "create" && <b style={{ color: "red" }}> *</b>} </label> <div class="mt-2"> <input @@ -216,7 +265,6 @@ export function AccountForm({ for="phone" > {i18n.str`Phone`} - {purpose === "create" && <b style={{ color: "red" }}> *</b>} </label> <div class="mt-2"> <input @@ -246,7 +294,6 @@ export function AccountForm({ </div> </div> - {!noCashout && <div class="sm:col-span-5"> <label @@ -254,7 +301,6 @@ export function AccountForm({ for="cashout" > {i18n.str`Cashout IBAN`} - {purpose !== "show" && <b style={{ color: "red" }}> *</b>} </label> <div class="mt-2"> <input @@ -269,7 +315,7 @@ export function AccountForm({ onChange={(e) => { form.cashout_payto_uri = e.currentTarget.value as PaytoString; if (!form.cashout_payto_uri) { - form.cashout_payto_uri= undefined + form.cashout_payto_uri = undefined } updateForm(structuredClone(form)); }} @@ -286,6 +332,54 @@ export function AccountForm({ </div> } + <div class="sm:col-span-5"> + <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label> + <InputAmount + name="debit" + left + currency={config.currency} + value={debitAmount ?? ""} + onChange={(e) => { + setDebitAmount(e); + }} + /> + <ShowInputErrorLabel + message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined} + isDirty={form.debit_threshold !== undefined} + /> + <p class="mt-2 text-sm text-gray-500" >allow user debt</p> + </div> + + <div class="sm:col-span-5"> + <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"> + <i18n.Translate>Is an exchange</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={!!isExchange} 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={() => setIsExchange(!isExchange)}> + <span aria-hidden="true" data-enabled={!!isExchange} 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="sm:col-span-5"> + <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"> + <i18n.Translate>Is public</i18n.Translate> + </span> + </span> + <button type="button" data-enabled={!!isPublic} 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={() => setIsPublic(!isPublic)}> + <span aria-hidden="true" data-enabled={!!isPublic} 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> </div> {children} diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index 8350cefd9..71f18a3db 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -7,6 +7,8 @@ import { useBankCoreApiContext } from "../../context/config.js"; import { useBusinessAccounts } from "../../hooks/circuit.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { usePreferences } from "../../hooks/preferences.js"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; interface Props { onCreateAccount: () => void; @@ -26,7 +28,7 @@ export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAcc return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.data.type === "fail") { switch (result.data.case) { diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx index b1acb8160..28078bc09 100644 --- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx +++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx @@ -9,6 +9,7 @@ import { WireTransfer } from "../WireTransfer.js"; import { AccountList } from "./AccountList.js"; import { useBankCoreApiContext } from "../../context/config.js"; import { format, getDate, getHours, getMonth, getYear, setDate, setHours, setMonth, setYear, sub } from "date-fns"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; /** @@ -90,7 +91,7 @@ function Metrics(): VNode { const resp = useLastMonitorInfo(params.current, params.previous, metricType); if (!resp) return <Fragment />; if (resp instanceof TalerError) { - return <ErrorLoading error={resp} /> + return <ErrorLoadingWithDebug error={resp} /> } if (resp.current.type !== "ok" || resp.previous.type !== "ok") { return <Fragment /> diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index 2b0be6056..6ff723a31 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -17,31 +17,33 @@ export function CreateNewAccount({ onCreateSuccess: () => void; }): VNode { const { i18n } = useTranslationContext(); - // const { createAccount } = useAdminAccountAPI(); const { state: credentials } = useBackendState() const token = credentials.status !== "loggedIn" ? undefined : credentials.token const { api } = useBankCoreApiContext(); - const [submitAccount, setSubmitAccount] = useState<AccountFormData | undefined>(); + const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.RegisterAccountRequest | undefined>(); const [notification, notify, handleError] = useLocalNotification() async function doCreate() { if (!submitAccount || !token) return; await handleError(async () => { - const account: TalerCorebankApi.RegisterAccountRequest = { - cashout_payto_uri: submitAccount.cashout_payto_uri, - challenge_contact_data: submitAccount.contact_data, - internal_payto_uri: submitAccount.payto_uri, - name: submitAccount.name, - username: submitAccount.username,//FIXME: not in account data - password: getRandomPassword(), - }; + // const account: TalerCorebankApi.RegisterAccountRequest = { + // cashout_payto_uri: submitAccount.cashout_payto_uri, + // challenge_contact_data: submitAccount.challenge_contact_data, + // internal_payto_uri: submitAccount.internal_payto_uri, + // debit_threshold: submitAccount.debit_threshold, + // is_public: submitAccount.is_public, + // is_taler_exchange: submitAccount.is_taler_exchange, + // name: submitAccount.name, + // username: submitAccount.username, + // password: getRandomPassword(), + // }; - const resp = await api.createAccount(token, account); + const resp = await api.createAccount(token, submitAccount); if (resp.type === "ok") { mutate(() => true)// clean account list notifyInfo( - i18n.str`Account created with password "${account.password}". The user must change the password on the next login.`, + i18n.str`Account created with password "${submitAccount.password}". The user must change the password on the next login.`, ); onCreateSuccess(); } else { @@ -82,6 +84,12 @@ export function CreateNewAccount({ description: resp.detail.hint as TranslatedString, debug: resp.detail, }) + case "user-cant-set-debt": return notify({ + type: "error", + title: i18n.str`Only admin is allow to set debt limit.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) default: assertUnreachable(resp) } } @@ -105,6 +113,7 @@ export function CreateNewAccount({ </div> <AccountForm template={undefined} + admin purpose="create" onChange={(a) => { setSubmitAccount(a); diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 051a86ad6..5ee887128 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -9,6 +9,7 @@ import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; export function RemoveAccount({ account, @@ -34,7 +35,7 @@ export function RemoveAccount({ return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.type === "fail") { switch (result.case) { diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 67fc47e60..b2ff41e63 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -49,6 +49,7 @@ import { LoginForm } from "../LoginForm.js"; import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { getRandomPassword, getRandomUsername } from "../rnd.js"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; interface Props { account: string; @@ -97,7 +98,7 @@ export function CreateCashout({ return <Loading /> } if (resultAccount instanceof TalerError) { - return <ErrorLoading error={resultAccount} /> + return <ErrorLoadingWithDebug error={resultAccount} /> } if (resultAccount.type === "fail") { switch (resultAccount.case) { @@ -111,7 +112,7 @@ export function CreateCashout({ } if (info instanceof TalerError) { - return <ErrorLoading error={info} /> + return <ErrorLoadingWithDebug error={info} /> } const conversionInfo = info.body.conversion_rate diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index 6fd9eb18c..fcbf0c408 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -42,6 +42,7 @@ import { import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { LocalNotificationBanner } from "@gnu-taler/web-util/browser"; import { RenderAmount } from "../PaytoWireTransferForm.js"; +import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js"; interface Props { id: string; @@ -70,7 +71,7 @@ export function ShowCashoutDetails({ return <Loading /> } if (result instanceof TalerError) { - return <ErrorLoading error={result} /> + return <ErrorLoadingWithDebug error={result} /> } if (result.type === "fail") { switch (result.case) { @@ -86,7 +87,7 @@ export function ShowCashoutDetails({ } if (info instanceof TalerError) { - return <ErrorLoading error={info} /> + return <ErrorLoadingWithDebug error={info} /> } const errors = undefinedIfEmpty({ diff --git a/packages/demobank-ui/src/stories.test.ts b/packages/demobank-ui/src/stories.test.ts index fac363e5b..a060a6b48 100644 --- a/packages/demobank-ui/src/stories.test.ts +++ b/packages/demobank-ui/src/stories.test.ts @@ -18,7 +18,7 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { AccessToken, TalerCorebankApi, setupI18n } from "@gnu-taler/taler-util"; +import { AccessToken, AmountString, TalerCorebankApi, setupI18n } from "@gnu-taler/taler-util"; import { parseGroupImport } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; import * as components from "./components/index.examples.js"; @@ -74,6 +74,7 @@ function DefaultTestingContext({ num_fractional_normal_digits: 2, num_fractional_trailing_zero_digits: 2, }, + default_debit_threshold: "ARS:10" as AmountString, version: "1:0:0", } const ctx2 = create(BankCoreApiProviderTesting, { diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 18abf28a9..805d68660 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -108,12 +108,12 @@ export async function withRuntimeErrorHandling<T>(i18n: Translator, cb: () => Pr ? error.message : JSON.stringify(error)) as TranslatedString ) - } + } } } -export function buildRequestErrorMessage( i18n: Translator, cause: TalerError): ErrorNotification { +export function buildRequestErrorMessage(i18n: Translator, cause: TalerError): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { |