summaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/paths/instance/accounts/create
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/accounts/create')
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx197
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx236
3 files changed, 461 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
new file mode 100644
index 000000000..50cd801d8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Accounts/Create",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
new file mode 100644
index 000000000..d05375b6c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,197 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { safeConvertURL } from "../update/UpdatePage.js";
+
+type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>;
+ onBack?: () => void;
+}
+
+const accountAuthType = ["none", "basic"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+ const facadeURL = safeConvertURL(state.credit_facade_url);
+ const errors: FormErrors<Entity> = {
+ payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ username:
+ state.credit_facade_credentials.type === "basic" &&
+ !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" &&
+ !state.credit_facade_credentials.password
+ ? i18n.str`required`
+ : undefined,
+ }),
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !facadeURL
+ ? i18n.str`Invalid url`
+ : !facadeURL.href.endsWith("/")
+ ? i18n.str`URL should end with a '/'`
+ : facadeURL.searchParams.size > 0
+ ? i18n.str`URL should not contain params`
+ : facadeURL.hash
+ ? i18n.str`URL should not hash param`
+ : undefined,
+ repeatPassword: !state.credit_facade_credentials
+ ? undefined
+ : state.credit_facade_credentials.type === "basic" &&
+ (!state.credit_facade_credentials.password ||
+ state.credit_facade_credentials.password !== state.repeatPassword)
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ const credit_facade_url = !state.credit_facade_url
+ ? undefined
+ : facadeURL?.href;
+ const credit_facade_credentials:
+ | TalerMerchantApi.FacadeCredentials
+ | undefined =
+ credit_facade_url == undefined
+ ? undefined
+ : state.credit_facade_credentials?.type === "basic"
+ ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
+ : {
+ type: "none",
+ };
+
+ return onCreate({
+ payto_uri: state.payto_uri!,
+ credit_facade_credentials,
+ credit_facade_url,
+ });
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={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/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
new file mode 100644
index 000000000..9bab33f6f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,236 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-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/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ FacadeCredentials,
+ HttpStatusCode,
+ OperationFail,
+ OperationOk,
+ TalerError,
+ TalerMerchantApi,
+ TalerRevenueHttpClient,
+ assertUnreachable,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserFetchHttpLib,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = TalerMerchantApi.AccountAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={async (request: Entity) => {
+ const revenueAPI = !request.credit_facade_url
+ ? undefined
+ : new URL("./", request.credit_facade_url);
+
+ if (revenueAPI) {
+ const resp = await testRevenueAPI(
+ revenueAPI,
+ request.credit_facade_credentials,
+ );
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case TestRevenueErrorType.NO_CONFIG: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Server replied with "bad request".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.UNAUTHORIZED: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Unauthorized, try with another credentials.`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.NOT_FOUND: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Check facade URL, server replied with "not found".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.GENERIC_ERROR: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp.case);
+ }
+ }
+ }
+ }
+
+ return api.instance
+ .addBankAccount(state.token, request)
+ .then(() => {
+ onConfirm();
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
+
+export enum TestRevenueErrorType {
+ NO_CONFIG,
+ CLIENT_BAD_REQUEST,
+ UNAUTHORIZED,
+ NOT_FOUND,
+ GENERIC_ERROR,
+}
+
+export async function testRevenueAPI(
+ revenueAPI: URL,
+ creds: FacadeCredentials | undefined,
+): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> {
+ const api = new TalerRevenueHttpClient(
+ revenueAPI.href,
+ new BrowserFetchHttpLib(),
+ );
+ const auth =
+ creds === undefined
+ ? undefined
+ : creds.type === "none"
+ ? undefined
+ : creds.type === "basic"
+ ? {
+ username: creds.username,
+ password: creds.password,
+ }
+ : undefined;
+
+ try {
+ const config = await api.getConfig(auth);
+
+ if (config.type === "fail") {
+ switch (config.case) {
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NO_CONFIG,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+
+ const history = await api.getHistory(auth);
+
+ if (history.type === "fail") {
+ switch (history.case) {
+ case HttpStatusCode.BadRequest: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.CLIENT_BAD_REQUEST,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NOT_FOUND,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+ } catch (err) {
+ if (err instanceof TalerError) {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.GENERIC_ERROR,
+ detail: err.errorDetail,
+ };
+ }
+ }
+
+ return opFixedSuccess(undefined);
+}