diff options
Diffstat (limited to 'packages/demobank-ui/src')
15 files changed, 494 insertions, 86 deletions
diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx index 14df5a274..880a8135b 100644 --- a/packages/demobank-ui/src/Routing.tsx +++ b/packages/demobank-ui/src/Routing.tsx @@ -50,6 +50,7 @@ import { ShowCashoutDetails } from "./pages/business/ShowCashoutDetails.js"; import { urlPattern, useCurrentLocation } from "./route.js"; import { useNavigationContext } from "./context/navigation.js"; import { useEffect } from "preact/hooks"; +import { ConversionConfig } from "./pages/ConversionConfig.js"; export function Routing(): VNode { const backend = useBackendState(); @@ -230,6 +231,7 @@ export const privatePages = { myAccountDetails: urlPattern(/\/my-profile/, () => "#/my-profile"), myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"), myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"), + conversionConfig: urlPattern(/\/conversion/, () => "#/conversion"), accountDetails: urlPattern<{ account: string }>( /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/, ({ account }) => `#/profile/${account}/details`, @@ -338,6 +340,7 @@ function PrivateRouting({ routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } @@ -356,6 +359,7 @@ function PrivateRouting({ routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } @@ -387,6 +391,7 @@ function PrivateRouting({ routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } @@ -413,6 +418,7 @@ function PrivateRouting({ routeHere={privatePages.accountDetails} onUpdateSuccess={() => navigateTo(privatePages.home.url({}))} routeMyAccountCashout={privatePages.myAccountCashouts} + routeConversionConfig={privatePages.conversionConfig} routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} @@ -434,6 +440,7 @@ function PrivateRouting({ routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } @@ -451,6 +458,7 @@ function PrivateRouting({ routeMyAccountDelete={privatePages.myAccountDelete} routeMyAccountDetails={privatePages.myAccountDetails} routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} onAuthorizationRequired={() => navigateTo(privatePages.solveSecondFactor.url({})) } @@ -553,6 +561,19 @@ function PrivateRouting({ /> ); } + case "conversionConfig": { + return <ConversionConfig + routeMyAccountCashout={privatePages.myAccountCashouts} + routeMyAccountDelete={privatePages.myAccountDelete} + routeMyAccountDetails={privatePages.myAccountDetails} + routeMyAccountPassword={privatePages.myAccountPassword} + routeConversionConfig={privatePages.conversionConfig} + routeCancel={privatePages.home} + onUpdateSuccess={() => { + navigateTo(privatePages.home.url({})) + }} + />; + } case "homeWireTransfer": { return ( <AccountPage diff --git a/packages/demobank-ui/src/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts index 42b12ac65..c8bb1e108 100644 --- a/packages/demobank-ui/src/components/Transactions/index.ts +++ b/packages/demobank-ui/src/components/Transactions/index.ts @@ -55,8 +55,8 @@ export namespace State { amount?: string, }> | undefined; transactions: Transaction[]; - onPrev?: () => void; - onNext?: () => void; + onGoStart?: () => void; + onGoNext?: () => void; } } diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts index 2d217989c..40e1b0ced 100644 --- a/packages/demobank-ui/src/components/Transactions/state.ts +++ b/packages/demobank-ui/src/components/Transactions/state.ts @@ -39,9 +39,7 @@ export function useComponentState({ account, routeCreateWireTransfer }: Props): } const transactions = - txResult.data.type === "fail" - ? [] - : txResult.data.body.transactions + txResult.result .map((tx) => { const negative = tx.direction === "debit"; const cp = parsePaytoUri( @@ -76,7 +74,7 @@ export function useComponentState({ account, routeCreateWireTransfer }: Props): error: undefined, routeCreateWireTransfer, transactions, - onNext: txResult.isLastPage ? undefined : txResult.loadMore, - onPrev: txResult.isFirstPage ? undefined : txResult.loadMorePrev, + onGoNext: txResult.isLastPage ? undefined : txResult.loadNext, + onGoStart: txResult.isFirstPage ? undefined : txResult.loadFirst, }; } diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index 321a6ff3a..ba400b37a 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -25,8 +25,8 @@ import { useAccountDetails } from "../../hooks/access.js"; export function ReadyView({ transactions, routeCreateWireTransfer, - onNext, - onPrev, + onGoNext, + onGoStart, }: State.Ready): VNode { const { i18n, dateLocale } = useTranslationContext(); const { config } = useBankCoreApiContext() @@ -219,16 +219,16 @@ export function ReadyView({ <button name="first page" 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} + disabled={!onGoStart} + onClick={onGoStart} > <i18n.Translate>First page</i18n.Translate> </button> <button name="next page" 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} + disabled={!onGoNext} + onClick={onGoNext} > <i18n.Translate>Next</i18n.Translate> </button> diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index e07a3d1b1..a101dc83e 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -21,7 +21,7 @@ import { WithdrawalOperationStatus, } from "@gnu-taler/taler-util"; import { useEffect, useState } from "preact/hooks"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; +import { PAGE_SIZE } from "../utils.js"; import { useBackendState } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 @@ -156,8 +156,7 @@ export function usePublicAccounts( filterAccount: string | undefined, initial?: number, ) { - // const [offset, setOffset] = useState<number | undefined>(initial); - const offset = undefined; + const [offset, setOffset] = useState<number | undefined>(initial); const { api } = useBankCoreApiContext(); @@ -168,7 +167,7 @@ export function usePublicAccounts( return await api.getPublicAccounts( { account }, { - limit: MAX_RESULT_SIZE, + limit: PAGE_SIZE, offset: txid ? String(txid) : undefined, order: "asc", }, @@ -190,21 +189,24 @@ export function usePublicAccounts( keepPreviousData: true, }); - const isLastPage = data && data.body.public_accounts.length < PAGE_SIZE; - const isFirstPage = !initial; + const isLastPage = + data && data.type === "ok" && data.body.public_accounts.length <= PAGE_SIZE; + const isFirstPage = !offset; + const result = data && data.type == "ok" ? structuredClone(data.body.public_accounts) : [] + if (result.length == PAGE_SIZE+1) { + result.pop() + } const pagination = { + result, isLastPage, isFirstPage, - loadMore: () => { - if (isLastPage || data?.type !== "ok") return; - const list = data.body.public_accounts; - if (list.length < MAX_RESULT_SIZE) { - // setOffset(list[list.length-1].account_name); - } + loadNext: () => { + if (!result.length) return; + setOffset(result[result.length - 1].row_id); }, - loadMorePrev: () => { - null; + loadFirst: () => { + setOffset(0); }, }; @@ -241,7 +243,7 @@ export function useTransactions(account: string, initial?: number) { return await api.getTransactions( { username, token }, { - limit: MAX_RESULT_SIZE, + limit: PAGE_SIZE +1 , offset: txid ? String(txid) : undefined, order: "dec", }, @@ -262,21 +264,23 @@ export function useTransactions(account: string, initial?: number) { }); const isLastPage = - data && data.type === "ok" && data.body.transactions.length < PAGE_SIZE; - const isFirstPage = true; + data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE; + const isFirstPage = !offset; + const result = data && data.type == "ok" ? structuredClone(data.body.transactions) : [] + if (result.length == PAGE_SIZE+1) { + result.pop() + } const pagination = { + result, isLastPage, isFirstPage, - loadMore: () => { - if (isLastPage || data?.type !== "ok") return; - const list = data.body.transactions; - if (list.length < MAX_RESULT_SIZE) { - setOffset(list[list.length - 1].row_id); - } + loadNext: () => { + if (!result.length) return; + setOffset(result[result.length - 1].row_id); }, - loadMorePrev: () => { - null; + loadFirst: () => { + setOffset(0); }, }; diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 88ca7b947..7d8884797 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js"; +import { PAGE_SIZE } from "../utils.js"; import { useBackendState } from "./backend.js"; import { @@ -32,6 +32,7 @@ import { } from "@gnu-taler/taler-util"; import _useSWR, { SWRHook, mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; +import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 const useSWR = _useSWR as unknown as SWRHook; @@ -138,17 +139,16 @@ export function useBusinessAccounts() { credentials.status !== "loggedIn" ? undefined : credentials.token; const { api } = useBankCoreApiContext(); - // const [offset, setOffset] = useState<string | undefined>(); - const offset = undefined; + const [offset, setOffset] = useState<number | undefined>(); - function fetcher([token, offset]: [AccessToken, string]) { + function fetcher([token, offset]: [AccessToken, number]) { // FIXME: add account name filter return api.getAccounts( token, {}, { - limit: MAX_RESULT_SIZE, - offset, + limit: PAGE_SIZE+1, + offset: String(offset), order: "asc", }, ); @@ -157,7 +157,7 @@ export function useBusinessAccounts() { const { data, error } = useSWR< TalerCoreBankResultByMethod<"getAccounts">, TalerHttpError - >([token, offset, "getAccounts"], fetcher, { + >([token, offset ?? 0, "getAccounts"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -170,22 +170,23 @@ export function useBusinessAccounts() { }); const isLastPage = - data && data.type === "ok" && data.body.accounts.length < PAGE_SIZE; - const isFirstPage = false; + data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE; + const isFirstPage = !offset; + const result = data && data.type == "ok" ? structuredClone(data.body.accounts) : [] + if (result.length == PAGE_SIZE+1) { + result.pop() + } const pagination = { + result, isLastPage, isFirstPage, - loadMore: () => { - if (isLastPage || data?.type !== "ok") return; - const list = data.body.accounts; - if (list.length < MAX_RESULT_SIZE) { - // FIXME: define pagination - // setOffset(list[list.length - 1].row_id); - } + loadNext: () => { + if (!result.length) return; + setOffset(result[result.length - 1].row_id); }, - loadMorePrev: () => { - null; + loadFirst: () => { + setOffset(0); }, }; diff --git a/packages/demobank-ui/src/pages/ConversionConfig.tsx b/packages/demobank-ui/src/pages/ConversionConfig.tsx new file mode 100644 index 000000000..73a6ab3ee --- /dev/null +++ b/packages/demobank-ui/src/pages/ConversionConfig.tsx @@ -0,0 +1,333 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AmountString, + Amounts, + HttpStatusCode, + OperationOk, + OperationResult, + TalerBankConversionApi, + TranslatedString, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { + LocalNotificationBanner, + ShowInputErrorLabel, + useLocalNotification, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { useBankCoreApiContext } from "../context/config.js"; +import { useBackendState } from "../hooks/backend.js"; +import { RouteDefinition } from "../route.js"; +import { ProfileNavigation } from "./ProfileNavigation.js"; +import { useState } from "preact/hooks"; +import { undefinedIfEmpty } from "../utils.js"; +import { InputAmount } from "./PaytoWireTransferForm.js"; + +interface Props { + routeMyAccountDetails: RouteDefinition; + routeMyAccountDelete: RouteDefinition; + routeMyAccountPassword: RouteDefinition; + routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; + routeCancel: RouteDefinition; + onUpdateSuccess: () => void; +} + +type FormType<T> = { + [k in keyof T]: string | undefined; +} + +type ErrorsType<T> = { + [k in keyof T]?: TranslatedString; +} + + +type FormHandler<T> = { + [k in keyof T]?: { + value: string | undefined; + onUpdate: (s: string) => void; + error: TranslatedString | undefined; + } +} +function useFormState<T>(defaultValue: FormType<T>, validate: (f: FormType<T>) => ErrorsType<T>): FormHandler<T> { + const [form, updateForm] = useState<FormType<T>>(defaultValue) + + const errors = undefinedIfEmpty<ErrorsType<T>>(validate(form)) + + const p = (Object.keys(form) as Array<keyof T>) + console.log("FORM", p) + const handler = p.reduce((prev, fieldName) => { + console.log("fie;d", fieldName) + const currentValue = form[fieldName] + const currentError = errors !== undefined ? errors[fieldName] : undefined + prev[fieldName] = { + error: currentError, + value: currentValue, + onUpdate: (newValue) => { + updateForm({ ...form, [fieldName]: newValue }) + } + } + return prev + }, {} as FormHandler<T>) + + return handler +} + +/** + * Show histories of public accounts. + */ +export function ConversionConfig({ + routeMyAccountCashout, + routeMyAccountDelete, + routeMyAccountDetails, + routeMyAccountPassword, + routeConversionConfig, + routeCancel, + onUpdateSuccess, +}: Props): VNode { + const { i18n } = useTranslationContext(); + + const { state: credentials } = useBackendState(); + const creds = + credentials.status !== "loggedIn" || !credentials.isUserAdministrator + ? undefined + : credentials; + const { api, config } = useBankCoreApiContext(); + + const [notification, notify, handleError] = useLocalNotification(); + + if (!creds) { + return <div>only admin can setup conversion</div>; + } + + const form = useFormState<TalerBankConversionApi.ConversionRate>({ + cashin_min_amount: undefined, + cashin_tiny_amount: undefined, + cashin_fee: undefined, + cashin_ratio: undefined, + cashin_rounding_mode: undefined, + cashout_min_amount: undefined, + cashout_tiny_amount: undefined, + cashout_fee: undefined, + cashout_ratio: undefined, + cashout_rounding_mode: undefined, + }, (state) => { + return ({ + cashin_min_amount: !state.cashin_min_amount ? i18n.str`required` : + !Amounts.parse(`${config.currency}:${state.cashin_min_amount}`) ? i18n.str`invalid` : + undefined, + + }) + }) + + + async function doUpdate() { + if (!creds) return + await handleError(async () => { + const resp = await api + .getConversionInfoAPI() + .updateConversionRate(creds.token, { + + } as any) + if (resp.type === "ok") { + onUpdateSuccess() + } else { + switch (resp.case) { + case HttpStatusCode.Unauthorized: { + return notify({ + type: "error", + title: i18n.str`Wrong credentials`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + } + case HttpStatusCode.NotImplemented: { + return notify({ + type: "error", + title: i18n.str`Conversion is disabled`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + } + default: + assertUnreachable(resp); + } + } + }); + } + + + return ( + <div> + <ProfileNavigation current="conversion" + routeMyAccountCashout={routeMyAccountCashout} + routeMyAccountDelete={routeMyAccountDelete} + routeMyAccountDetails={routeMyAccountDetails} + routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} + /> + + <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"> + <LocalNotificationBanner notification={notification} /> + + <div class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + <i18n.Translate>Conversion</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-6 pt-6"> + <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"> + <label + for="cashout_amount_min" + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Minimum amount`}</label> + <InputAmount + name="cashout_amount_min" + left + currency={config.currency} + value={form.cashin_min_amount?.value ?? ""} + onChange={form.cashin_min_amount?.onUpdate} + /> + <ShowInputErrorLabel + message={form.cashin_min_amount?.error} + isDirty={form.cashin_min_amount?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Only cashout operation above this threshold will be allowed</i18n.Translate> + </p> + </div> + </div> + </div> + + <div class="px-6 pt-6"> + <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"> + <label + for="cashout_amount_tiny" + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Minimum difference`}</label> + <InputAmount + name="cashout_amount_tiny" + left + currency={config.currency} + value={form.cashin_min_amount?.value ?? ""} + onChange={form.cashin_min_amount?.onUpdate} + /> + <ShowInputErrorLabel + message={form.cashin_min_amount?.error} + isDirty={form.cashin_min_amount?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Smallest difference between two amounts</i18n.Translate> + </p> + </div> + </div> + </div> + + <div class="px-6 pt-6"> + <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"> + <label + for="cashin_fee" + class="block text-sm font-medium leading-6 text-gray-900" + >{i18n.str`Fee`}</label> + <InputAmount + name="cashin_fee" + left + currency={config.currency} + value={form.cashin_min_amount?.value ?? ""} + onChange={form.cashin_fee?.onUpdate} + /> + <ShowInputErrorLabel + message={form.cashin_fee?.error} + isDirty={form.cashin_fee?.value !== undefined} + /> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate>Operation fee</i18n.Translate> + </p> + </div> + </div> + </div> + + <div class="px-6 pt-6"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="password" + > + {i18n.str`Ratio`} + </label> + <div class="mt-2"> + <input + type="number" + 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" + name="current" + id="cashout_ratio" + data-error={!!form.cashin_ratio?.error && form.cashout_ratio?.value !== undefined} + value={form.cashout_ratio?.value ?? ""} + onChange={(e) => { + form.cashout_ratio?.onUpdate(e.currentTarget.value); + }} + autocomplete="off" + /> + <ShowInputErrorLabel + message={form.cashin_ratio?.error} + isDirty={form.cashout_ratio?.value !== undefined} + /> + </div> + <p class="mt-2 text-sm text-gray-500"> + <i18n.Translate> + Your current password, for security + </i18n.Translate> + </p> + </div> + + <div class="flex items-center justify-between mt-6 gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> + <a name="cancel" + href={routeCancel.url({})} + class="text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </a> + <button + type="submit" + name="update conversion" + class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + onClick={async () => { + doUpdate() + }} + > + <i18n.Translate>Update</i18n.Translate> + </button> + </div> + </form> + </div> + </div> + ); +} + diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index f90b985e6..b351785d9 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -88,10 +88,7 @@ export function LoginForm({ .createAccessToken(password, { // scope: "readwrite" as "write", // FIX: different than merchant scope: "readwrite", - duration: { - d_us: "forever", // FIX: should return shortest - // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, + duration: { d_us: "forever" }, refreshable: true, }); if (resp.type === "ok") { diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx index ba02d07b9..8b7a8205f 100644 --- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx +++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx @@ -13,13 +13,13 @@ 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 { assertUnreachable } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useBankCoreApiContext } from "../context/config.js"; -import { useBackendState } from "../hooks/backend.js"; -import { assertUnreachable } from "@gnu-taler/taler-util"; import { useNavigationContext } from "../context/navigation.js"; -import { EmptyObject, RouteDefinition } from "../route.js"; +import { useBackendState } from "../hooks/backend.js"; +import { RouteDefinition } from "../route.js"; export function ProfileNavigation({ current, @@ -27,20 +27,24 @@ export function ProfileNavigation({ routeMyAccountDelete, routeMyAccountDetails, routeMyAccountPassword, + routeConversionConfig }: { - current: "details" | "delete" | "credentials" | "cashouts", + current: "details" | "delete" | "credentials" | "cashouts" | "conversion", routeMyAccountDetails: RouteDefinition; routeMyAccountDelete: RouteDefinition; routeMyAccountPassword: RouteDefinition; routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; }): VNode { const { i18n } = useTranslationContext(); const { config } = useBankCoreApiContext(); const { state: credentials } = useBackendState(); - const nonAdminUser = + const isAdminUser = credentials.status !== "loggedIn" ? false - : !credentials.isUserAdministrator; + : credentials.isUserAdministrator; + const nonAdminUser = !isAdminUser; + const { navigateTo } = useNavigationContext(); return ( <div> @@ -71,6 +75,10 @@ export function ProfileNavigation({ navigateTo(routeMyAccountCashout.url({})); return; } + case "conversion": { + navigateTo(routeConversionConfig.url({})); + return; + } default: assertUnreachable(op); } @@ -88,9 +96,14 @@ export function ProfileNavigation({ <i18n.Translate>Credentials</i18n.Translate> </option> {config.allow_conversion ? ( - <option value="cashouts" selected={current == "cashouts"}> - <i18n.Translate>Cashouts</i18n.Translate> - </option> + <Fragment> + <option value="cashouts" selected={current == "cashouts"}> + <i18n.Translate>Cashouts</i18n.Translate> + </option> + <option value="conversion" selected={current == "cashouts"}> + <i18n.Translate>Conversion</i18n.Translate> + </option> + </Fragment> ) : undefined} </select> </div> @@ -165,6 +178,23 @@ export function ProfileNavigation({ ></span> </a> ) : undefined} + {config.allow_conversion && isAdminUser ? ( + <a + name="conversion config" + href={routeConversionConfig.url({})} + data-selected={current == "conversion"} + 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> + <i18n.Translate>Conversion</i18n.Translate> + </span> + <span + aria-hidden="true" + data-selected={current == "conversion"} + class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5" + ></span> + </a> + ) : undefined} </nav> </div> </div> diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx index 14f4acdb8..fe64778dd 100644 --- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx +++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx @@ -31,6 +31,7 @@ interface Props { routeMyAccountPassword: RouteDefinition; routeMyAccountCashout: RouteDefinition; routeCreateCashout: RouteDefinition; + routeConversionConfig:RouteDefinition; } export function CashoutListForAccount({ @@ -41,6 +42,7 @@ export function CashoutListForAccount({ routeMyAccountCashout, routeMyAccountDelete, routeMyAccountDetails, + routeConversionConfig, routeMyAccountPassword, routeClose, }: Props): VNode { @@ -61,6 +63,7 @@ export function CashoutListForAccount({ routeMyAccountDelete={routeMyAccountDelete} routeMyAccountDetails={routeMyAccountDetails} routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} /> ) : ( <h1 class="text-base font-semibold leading-6 text-gray-900"> diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index db76e83d9..6aad8997a 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -51,6 +51,7 @@ export function ShowAccountDetails({ routeMyAccountDetails, routeHere, routeMyAccountPassword, + routeConversionConfig, }: { routeClose: RouteDefinition; routeHere: RouteDefinition<{ account: string }>; @@ -58,6 +59,7 @@ export function ShowAccountDetails({ routeMyAccountDelete: RouteDefinition; routeMyAccountPassword: RouteDefinition; routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; onUpdateSuccess: () => void; onAuthorizationRequired: () => void; account: string; @@ -184,6 +186,7 @@ export function ShowAccountDetails({ <ProfileNavigation current="details" routeMyAccountCashout={routeMyAccountCashout} routeMyAccountDelete={routeMyAccountDelete} + routeConversionConfig={routeConversionConfig} routeMyAccountDetails={routeMyAccountDetails} routeMyAccountPassword={routeMyAccountPassword} /> diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx index dfa0adf17..305f041ec 100644 --- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -45,6 +45,7 @@ export function UpdateAccountPassword({ routeMyAccountDelete, routeMyAccountDetails, routeMyAccountPassword, + routeConversionConfig, focus, routeHere, }: { @@ -54,6 +55,7 @@ export function UpdateAccountPassword({ routeMyAccountDelete: RouteDefinition; routeMyAccountPassword: RouteDefinition; routeMyAccountCashout: RouteDefinition; + routeConversionConfig: RouteDefinition; focus?: boolean; onAuthorizationRequired: () => void; onUpdateSuccess: () => void; @@ -152,6 +154,7 @@ export function UpdateAccountPassword({ routeMyAccountDelete={routeMyAccountDelete} routeMyAccountDetails={routeMyAccountDetails} routeMyAccountPassword={routeMyAccountPassword} + routeConversionConfig={routeConversionConfig} /> ) : ( <h1 class="text-base font-semibold leading-6 text-gray-900"> diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index d8c129507..811c3e37a 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -62,8 +62,10 @@ export function AccountList({ } } + const onGoStart = result.isFirstPage ? undefined : result.loadFirst + const onGoNext = result.isLastPage ? undefined : result.loadNext - const { accounts } = result.data.body; + const accounts = result.result; return ( <Fragment> <div class="px-4 sm:px-6 lg:px-8 mt-4"> @@ -93,7 +95,9 @@ export function AccountList({ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> {!accounts.length ? ( - <div></div> + <div> + {/* FIXME: ADDD empty list */} + </div> ) : ( <table class="min-w-full divide-y divide-gray-300"> <thead> @@ -208,6 +212,30 @@ export function AccountList({ </table> )} </div> + <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 + name="first page" + 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={!onGoStart} + onClick={onGoStart} + > + <i18n.Translate>First page</i18n.Translate> + </button> + <button + name="next page" + 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={!onGoNext} + onClick={onGoNext} + > + <i18n.Translate>Next</i18n.Translate> + </button> + </div> + </nav> + </div> </div> </div> diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index 8e353b5e7..e09164ffb 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -55,18 +55,6 @@ export function CreateNewAccount({ 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.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, submitAccount); if (resp.type === "ok") { notifyInfo( diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index ab0b60d72..8b0febe42 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -119,8 +119,7 @@ export enum CashoutStatus { PENDING = "pending", } -export const PAGE_SIZE = 20; -export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; +export const PAGE_SIZE = 5; type Translator = ReturnType<typeof useTranslationContext>["i18n"]; |