aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/merchant-backoffice-ui/src/InstanceRoutes.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx150
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts117
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts126
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx43
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx81
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx60
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx244
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx118
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts53
11 files changed, 1050 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index c3c20bcc4..5ee17220d 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -65,6 +65,11 @@ import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js";
import ValidatorListPage from "./paths/instance/otp_devices/list/index.js";
import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js";
+
+import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js";
+import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js";
+// import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js";
+
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
import TransferListPage from "./paths/instance/transfers/list/index.js";
import InstanceUpdatePage, {
@@ -118,6 +123,10 @@ export enum InstancePaths {
otp_devices_update = "/otp-devices/:vid/update",
otp_devices_new = "/otp-devices/new",
+ token_family_list = "/tokenfamilies",
+ token_family_update = "/tokenfamilies/:slug/update",
+ token_family_new = "/tokenfamilies/new",
+
interface = "/interface",
}
@@ -486,6 +495,45 @@ export function InstanceRoutes({
route(InstancePaths.transfers_list);
}}
/>
+ {/* *
+ * Token family pages
+ */}
+ <Route
+ path={InstancePaths.token_family_list}
+ component={TokenFamilyListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.token_family_new);
+ }}
+ onSelect={(slug: string) => {
+ route(InstancePaths.token_family_update.replace(":slug", slug));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ {/* <Route
+ path={InstancePaths.token_family_update}
+ component={TokenFamilyUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.token_family_list)}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ /> */}
+ <Route
+ path={InstancePaths.token_family_new}
+ component={TokenFamilyCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ />
{/**
* Webhooks pages
*/}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 8aac5f543..447020acf 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -123,6 +123,16 @@ export function Sidebar({
</span>
</a>
</li>
+ <li>
+ <a href={"/tokenfamilies"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-key" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Token Families</i18n.Translate>
+ </span>
+ </a>
+ </li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
diff --git a/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx
new file mode 100644
index 000000000..3cb739aea
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { useBackendContext } from "../../context/backend.js";
+import { MerchantBackend } from "../../declaration.js";
+import {
+ TokenFamilyCreateSchema as createSchema,
+ TokenFamilyUpdateSchema as updateSchema,
+} from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+import { InputDate } from "../form/InputDate.js";
+import { InputDuration } from "../form/InputDuration.js";
+import { InputSelector } from "../form/InputSelector.js";
+
+type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail;
+
+interface Props {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+ alreadyExist?: boolean;
+}
+
+export function TokenFamilyForm({ onSubscribe, initial, alreadyExist }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity>>({
+ slug: "",
+ name: "",
+ description: "",
+ description_i18n: {},
+ kind: MerchantBackend.TokenFamilies.TokenFamilyKind.Discount,
+ duration: { d_us: "forever" },
+ valid_after: { t_s: "never" },
+ valid_before: { t_s: "never" },
+ });
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ // (alreadyExist ? updateSchema : createSchema).validateSync(value, {
+ // abortEarly: false,
+ // });
+ createSchema.validateSync(value, {
+ abortEarly: false,
+ });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): Entity | undefined => {
+ // HACK: Think about how this can be done better
+ return value as Entity;
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <FormProvider<Entity>
+ name="token_family"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {/* {alreadyExist ? undefined : ( */}
+ <InputWithAddon<Entity>
+ name="slug"
+ addonBefore={`${backend.url}/tokenfamily/`}
+ label={i18n.str`Slug`}
+ tooltip={i18n.str`token family slug to use in URLs (for internal use only)`}
+ />
+ {/* )}
+ {alreadyExist ? undefined : ( */}
+ <InputSelector<Entity>
+ name="kind"
+ label={i18n.str`Kind`}
+ tooltip={i18n.str`token family kind`}
+ values={["discount", "subscription"]}
+ />
+ {/* )} */}
+ <Input<Entity>
+ name="name"
+ inputType="text"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`user-readable token family name`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`token family description for customers`}
+ />
+ <InputDate<Entity>
+ name="valid_after"
+ label={i18n.str`Valid After`}
+ tooltip={i18n.str`token family can issue tokens after this date`}
+ withTimestampSupport
+ />
+ <InputDate<Entity>
+ name="valid_before"
+ label={i18n.str`Valid Before`}
+ tooltip={i18n.str`token family can issue tokens until this date`}
+ withTimestampSupport
+ />
+ <InputDuration<Entity>
+ name="duration"
+ label={i18n.str`Duration`}
+ tooltip={i18n.str`validity duration of a issued token`}
+ withForever
+ />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index dc53e3e83..9808f8b66 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1526,6 +1526,123 @@ export namespace MerchantBackend {
}
}
+ namespace TokenFamilies {
+ // Kind of the token family.
+ enum TokenFamilyKind {
+ Discount = "discount",
+ Subscription = "subscription",
+ }
+
+ // POST /private/tokenfamilies
+ interface TokenFamilyAddDetail {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ // If not specified, merchant backend will use the current time.
+ valid_after?: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ // PATCH /private/tokenfamilies/$SLUG
+ interface TokenFamilyPatchDetail {
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+ }
+
+ // GET /private/tokenfamilies
+ interface TokenFamilySummaryResponse {
+ // All configured token families of this instance.
+ token_families: TokenFamilyEntry[];
+ }
+
+ interface TokenFamilyEntry {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ // GET /private/tokenfamilies/$SLUG
+ interface TokenFamilyDetail {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+
+ // How many tokens have been issued for this family.
+ issued: Integer;
+
+ // How many tokens have been redeemed for this family.
+ redeemed: Integer;
+ }
+
+ }
+
interface ContractTerms {
// Human-readable description of the whole purchase
summary: string;
diff --git a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts
new file mode 100644
index 000000000..0266fe536
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts
@@ -0,0 +1,126 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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 {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { MerchantBackend } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface TokenFamilyAPI {
+ createTokenFamily: (
+ data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail,
+ ) => Promise<void>;
+ updateTokenFamily: (
+ slug: string,
+ data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
+ ) => Promise<void>;
+ deleteTokenFamily: (slug: string) => Promise<void>;
+}
+
+export function useTokenFamilyAPI(): TokenFamilyAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+
+ const { request } = useBackendInstanceRequest();
+
+ const createTokenFamily = async (
+ data: MerchantBackend.TokenFamilies.TokenFamilyAddDetail,
+ ): Promise<void> => {
+ const res = await request(`/private/tokenfamilies`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*"\/private\/tokenfamilies.*/);
+ };
+
+ const updateTokenFamily = async (
+ tokenFamilySlug: string,
+ data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
+ ): Promise<void> => {
+ const r = await request(`/private/tokenfamilies/${tokenFamilySlug}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*"\/private\/tokenfamilies.*/);
+ };
+
+ const deleteTokenFamily = async (tokenFamilySlug: string): Promise<void> => {
+ await request(`/private/tokenfamilies/${tokenFamilySlug}`, {
+ method: "DELETE",
+ });
+ await mutate([`/private/tokenfamilies`]);
+ };
+
+ return { createTokenFamily, updateTokenFamily, deleteTokenFamily };
+}
+
+export function useInstanceTokenFamilies(): HttpResponse<
+ (MerchantBackend.TokenFamilies.TokenFamilyEntry)[],
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher, multiFetcher } = useBackendInstanceRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<MerchantBackend.TokenFamilies.TokenFamilySummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/tokenfamilies`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+
+ if (list) {
+ return { ok: true, data: list.data.token_families };
+ }
+ return { loading: true };
+}
+
+export function useTokenFamilyDetails(
+ tokenFamilySlug: string,
+): HttpResponse<
+ MerchantBackend.TokenFamilies.TokenFamilyDetail,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.TokenFamilies.TokenFamilyDetail>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/tokenfamilies/${tokenFamilySlug}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx
new file mode 100644
index 000000000..d9ac4202c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/TokenFamily/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx
new file mode 100644
index 000000000..bf5b94e95
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+import { TokenFamilyCreateForm } from "../../../../components/token/TokenFamilyCreateForm.js";
+import { Test } from "../../../../components/token/Test.js";
+
+type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {/* <TokenFamilyCreateForm onSubscribe={addFormSubmitter} /> */}
+
+ <Test />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx
new file mode 100644
index 000000000..5abab05b0
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useTokenFamilyAPI } from "../../../../hooks/tokenfamily.js";
+
+export type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode {
+ const { createTokenFamily } = useTokenFamilyAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request) => {
+ return createTokenFamily(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create token family`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx
new file mode 100644
index 000000000..3d9bf9018
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx
@@ -0,0 +1,244 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { format } from "date-fns";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.TokenFamilies.TokenFamilyEntry;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (tokenFamily: Entity) => void;
+ onSelect: (tokenFamily: Entity) => void;
+ onUpdate: (
+ slug: string,
+ data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Token Families</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add token family`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (tokenFamily: Entity) => void;
+ onUpdate: (
+ slug: string,
+ data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
+ ) => Promise<void>;
+ onDelete: (tokenFamily: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Slug</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Valid After</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Valid Before</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Kind</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <Fragment key={i.slug}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.slug}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.name}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.valid_after.t_s === "never"
+ ? "never"
+ : format(new Date(i.valid_after.t_s * 1000), "yyyy/MM/dd hh:mm:ss")}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.valid_before.t_s === "never"
+ ? "never"
+ : format(new Date(i.valid_before.t_s * 1000), "yyyy/MM/dd hh:mm:ss")}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.kind}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to token family update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this token family from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There are no token families yet, add the first one by pressing the + sign.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx
new file mode 100644
index 000000000..a5d7413c0
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx
@@ -0,0 +1,118 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceTokenFamilies,
+ useTokenFamilyAPI,
+} from "../../../../hooks/tokenfamily.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (slug: string) => void;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function TokenFamilyList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceTokenFamilies();
+ const { deleteTokenFamily, updateTokenFamily } = useTokenFamilyAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <CardTable
+ instances={result.data}
+ onCreate={onCreate}
+ onUpdate={(slug, fam) =>
+ updateTokenFamily(slug, fam)
+ .then(() =>
+ setNotif({
+ message: i18n.str`token family updated successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not update the token family`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ onSelect={(tokenFamily) => onSelect(tokenFamily.slug)}
+ onDelete={(fam) =>
+ deleteTokenFamily(fam.slug)
+ .then(() =>
+ setNotif({
+ message: i18n.str`token family delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the token family`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
index c97d41204..1704b0555 100644
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ b/packages/merchant-backoffice-ui/src/schemas/index.ts
@@ -23,6 +23,7 @@ import { isAfter, isFuture } from "date-fns";
import * as yup from "yup";
import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants.js";
import { Amounts } from "@gnu-taler/taler-util";
+import { MerchantBackend } from "../declaration.js";
yup.setLocale({
mixed: {
@@ -243,3 +244,55 @@ export const NonInventoryProductSchema = yup.object().shape({
.required()
.test("amount", "the amount is not valid", currencyWithAmountIsValid),
});
+
+const timestampSchema = yup.object().shape({
+ t_s: yup.mixed().test(
+ 'is-timestamp',
+ 'Invalid timestamp',
+ value => typeof value === 'number' || value === 'never'
+ )
+}).required();
+
+const durationSchema = yup.object().shape({
+ d_us: yup.mixed().test(
+ 'is-duration',
+ 'Invalid duration',
+ value => typeof value === 'number' || value === 'forever'
+ )
+}).required();
+
+const tokenFamilyKindSchema = yup.mixed().oneOf(Object.values(MerchantBackend.TokenFamilies.TokenFamilyKind)).required();
+
+export const TokenFamilyCreateSchema = yup.object().shape({
+ slug: yup.string().ensure().required(),
+ name: yup.string().required(),
+ description: yup.string().required(),
+ // description_i18n: yup.lazy((obj) =>
+ // yup.object().shape(
+ // Object.keys(obj || {}).reduce((acc, key) => {
+ // acc[key] = yup.string().required();
+ // return acc;
+ // }, {})
+ // )
+ // ).optional(),
+ valid_after: timestampSchema.optional(),
+ valid_before: timestampSchema,
+ duration: durationSchema,
+ kind: tokenFamilyKindSchema,
+});
+
+export const TokenFamilyUpdateSchema = yup.object().shape({
+ name: yup.string().required(),
+ description: yup.string().required(),
+ // description_i18n: yup.lazy((obj) =>
+ // yup.object().shape(
+ // Object.keys(obj).reduce((acc, key) => {
+ // acc[key] = yup.string().required();
+ // return acc;
+ // }, {})
+ // )
+ // ),
+ valid_after: timestampSchema,
+ valid_before: timestampSchema,
+ duration: durationSchema,
+});