aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx47
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx202
-rw-r--r--packages/merchant-backoffice-ui/src/components/index.stories.ts17
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts55
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx12
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/stories.tsx3
9 files changed, 319 insertions, 50 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
index 7bcebd706..0d53c4d08 100644
--- a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
@@ -82,6 +82,12 @@ export interface FormType<T> {
const FormContext = createContext<FormType<unknown>>(null!);
+/**
+ * FIXME:
+ * USE MEMORY EVENTS INSTEAD OF CONTEXT
+ * @deprecated
+ */
+
export function useFormContext<T>() {
return useContext<FormType<T>>(FormContext);
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
new file mode 100644
index 000000000..2c1961639
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
@@ -0,0 +1,47 @@
+/*
+ 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 { h } from "preact";
+import { tests } from "@gnu-taler/web-util/lib/index.browser";
+import { InputPaytoForm } from "./InputPaytoForm.js";
+import { FormProvider } from "./FormProvider.js";
+import { useState } from "preact/hooks";
+
+export default {
+ title: "Components/Form/PayTo",
+ component: InputPaytoForm,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(() => {
+ const initial = {
+ accounts: [],
+ };
+ const [form, updateForm] = useState<Partial<typeof initial>>(initial);
+ return (
+ <FormProvider valueHandler={updateForm} object={form}>
+ <InputPaytoForm name="accounts" label="Accounts:" />
+ </FormProvider>
+ );
+}, {});
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 3cd36a6e0..98fe2f91a 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -28,6 +28,8 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { MerchantBackend } from "../../declaration.js";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
@@ -50,6 +52,13 @@ type Entity = {
instruction?: string;
[name: string]: string | undefined;
};
+ auth: {
+ type: "unset" | "basic" | "none";
+ url?: string;
+ username?: string;
+ password?: string;
+ repeat?: string;
+ };
};
function isEthereumAddress(address: string) {
@@ -162,8 +171,15 @@ const targets = [
"bitcoin",
"ethereum",
];
+const accountAuthType = ["none", "basic"];
const noTargetValue = targets[0];
-const defaultTarget = { target: noTargetValue, options: {} };
+const defaultTarget: Partial<Entity> = {
+ target: noTargetValue,
+ options: {},
+ auth: {
+ type: "unset" as const,
+ },
+};
export function InputPaytoForm<T>({
name,
@@ -187,7 +203,7 @@ export function InputPaytoForm<T>({
}
const { i18n } = useTranslationContext();
- const ops = value.options!;
+ const ops = value.options ?? {};
const url = tryUrl(`payto://${value.target}${payToPath}`);
if (url) {
Object.keys(ops).forEach((opt_key) => {
@@ -222,6 +238,24 @@ export function InputPaytoForm<T>({
? i18n.str`required`
: undefined,
}),
+ auth: !value.auth
+ ? undefined
+ : undefinedIfEmpty({
+ username:
+ value.auth.type === "basic" && !value.auth.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ value.auth.type === "basic" && !value.auth.password
+ ? i18n.str`required`
+ : undefined,
+ repeat:
+ value.auth.type === "basic" && !value.auth.repeat
+ ? i18n.str`required`
+ : value.auth.repeat !== value.auth.password
+ ? i18n.str`is not the same`
+ : undefined,
+ }),
};
const hasErrors = Object.keys(errors).some(
@@ -229,10 +263,31 @@ export function InputPaytoForm<T>({
);
const submit = useCallback((): void => {
+ const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
const alreadyExists =
- paytos.findIndex((x: string) => x === paytoURL) !== -1;
+ accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
if (!alreadyExists) {
- onChange([paytoURL, ...paytos] as any);
+ const newValue: MerchantBackend.Instances.MerchantBankAccount = {
+ payto_uri: paytoURL,
+ };
+ if (value.auth) {
+ if (value.auth.url) {
+ newValue.credit_facade_url = value.auth.url;
+ }
+ if (value.auth.type === "none") {
+ newValue.credit_facade_credentials = {
+ type: "none",
+ };
+ }
+ if (value.auth.type === "basic") {
+ newValue.credit_facade_credentials = {
+ type: "basic",
+ username: value.auth.username ?? "",
+ password: value.auth.password ?? "",
+ };
+ }
+ }
+ onChange([newValue, ...accounts] as any);
}
valueHandler(defaultTarget);
}, [value]);
@@ -339,37 +394,126 @@ export function InputPaytoForm<T>({
</Fragment>
)}
+ {/**
+ * Show additional fields apart from the payto
+ */}
{value.target !== noTargetValue && (
- <Input
- name="options.receiver-name"
- label={i18n.str`Name`}
- tooltip={i18n.str`Bank account owner's name.`}
- />
- )}
+ <Fragment>
+ <Input
+ name="options.receiver-name"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`Bank account owner's name.`}
+ />
+ <InputWithAddon
+ name="auth.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="auth.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ // if (str === "unset") {
+ // return "Without change";
+ // }
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {value.auth?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="auth.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="auth.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="auth.repeat"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ {/* <InputWithAddon
+ name="options.credit_credentials"
+ label={i18n.str`Account info`}
+ inputType={showKey ? "text" : "password"}
+ help="From where the merchant can download information about incoming wire transfers to this account"
+ expand
+ tooltip={i18n.str`Useful to validate the purchase`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfter={
+ <span class="icon">
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ <span style={{ display: "flex" }}>
+ <button
+ data-tooltip={
+ showKey
+ ? i18n.str`show secret key`
+ : i18n.str`hide secret key`
+ }
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setShowKey(!showKey);
+ }}
+ >
+ {showKey ? (
+ <i18n.Translate>hide</i18n.Translate>
+ ) : (
+ <i18n.Translate>show</i18n.Translate>
+ )}
+ </button>
+ </span>
+ }
+ /> */}
+ </Fragment>
+ )}
+ {/**
+ * Show the values in the list
+ */}
<div class="field is-horizontal">
<div class="field-label is-normal" />
<div class="field-body" style={{ display: "block" }}>
- {paytos.map((v: any, i: number) => (
- <div
- key={i}
- class="tags has-addons mt-3 mb-0 mr-3"
- style={{ flexWrap: "nowrap" }}
- >
- <span
- class="tag is-medium is-info mb-0"
- style={{ maxWidth: "90%" }}
+ {paytos.map(
+ (v: MerchantBackend.Instances.MerchantBankAccount, i: number) => (
+ <div
+ key={i}
+ class="tags has-addons mt-3 mb-0 mr-3"
+ style={{ flexWrap: "nowrap" }}
>
- {v}
- </span>
- <a
- class="tag is-medium is-danger is-delete mb-0"
- onClick={() => {
- onChange(paytos.filter((f: any) => f !== v) as any);
- }}
- />
- </div>
- ))}
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ {v.payto_uri}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(paytos.filter((f: any) => f !== v) as any);
+ }}
+ />
+ </div>
+ ),
+ )}
{!paytos.length && i18n.str`No accounts yet.`}
{required && (
<span class="icon has-text-danger is-right">
diff --git a/packages/merchant-backoffice-ui/src/components/index.stories.ts b/packages/merchant-backoffice-ui/src/components/index.stories.ts
new file mode 100644
index 000000000..c57ddab14
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/index.stories.ts
@@ -0,0 +1,17 @@
+/*
+ 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/>
+ */
+
+export * as payto from "./form/InputPaytoForm.stories.js";
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index 3a3bdd6f3..bbdc9708a 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -86,7 +86,7 @@ export function DefaultInstanceFormFields({
/>
<InputPaytoForm<Entity>
- name="payto_uris"
+ name="accounts"
label={i18n.str`Bank account`}
tooltip={i18n.str`URI specifying bank account for crediting revenue.`}
/>
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index b21af32d1..58e14a114 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -262,15 +262,45 @@ export namespace MerchantBackend {
// header.
token?: string;
}
+ type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials;
+
+ interface NoFacadeCredentials {
+ type: "none";
+ }
+
+ interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+
+ interface MerchantBankAccount {
+ // The payto:// URI where the wallet will send coins.
+ payto_uri: string;
+
+ // Optional base URL for a facade where the
+ // merchant backend can see incoming wire
+ // transfers to reconcile its accounting
+ // with that of the exchange. Used by
+ // taler-merchant-wirewatch.
+ credit_facade_url?: string;
+
+ // Credentials for accessing the credit facade.
+ credit_facade_credentials?: FacadeCredentials;
+ }
//POST /private/instances
interface InstanceConfigurationMessage {
- // The URI where the wallet will send coins. A merchant may have
+ // Bank accounts of the merchant. A merchant may have
// multiple accounts, thus this is an array. Note that by
- // removing URIs from this list the respective account is set to
+ // removing accounts from this list the respective account is set to
// inactive and thus unavailable for new contracts, but preserved
// in the database as existing offers and contracts may still refer
// to it.
- payto_uris: string[];
+ accounts: MerchantBankAccount[];
// Name of the merchant instance to create (will become $INSTANCE).
id: string;
@@ -326,10 +356,11 @@ export namespace MerchantBackend {
// PATCH /private/instances/$INSTANCE
interface InstanceReconfigurationMessage {
- // The URI where the wallet will send coins. A merchant may have
- // multiple accounts, thus this is an array. Note that by
- // removing URIs from this list
- payto_uris: string[];
+ // Bank accounts of the merchant. A merchant may have
+ // multiple accounts, thus this is an array. Note that removing
+ // URIs from this list deactivates the specified accounts
+ // (they will no longer be used for future contracts).
+ accounts: MerchantBankAccount[];
// Merchant name corresponding to this instance.
name: string;
@@ -491,6 +522,16 @@ export namespace MerchantBackend {
// salt used to compute h_wire
salt: HashCode;
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ credit_facade_credentials?: FacadeCredentials;
+
// true if this account is active,
// false if it is historic.
active: boolean;
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
index 0ef1f1270..4087908a2 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -47,7 +47,7 @@ interface Props {
function with_defaults(id?: string): Partial<Entity> {
return {
id,
- payto_uris: [],
+ accounts: [],
user_type: "business",
default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
default_wire_fee_amortization: 1,
@@ -75,12 +75,14 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
: value.user_type !== "business" && value.user_type !== "individual"
? i18n.str`should be business or individual`
: undefined,
- payto_uris:
- !value.payto_uris || !value.payto_uris.length
+ accounts:
+ !value.accounts || !value.accounts.length
? i18n.str`required`
: undefinedIfEmpty(
- value.payto_uris.map((p) => {
- return !PAYTO_REGEX.test(p) ? i18n.str`is not valid` : undefined;
+ value.accounts.map((p) => {
+ return !PAYTO_REGEX.test(p.payto_uri)
+ ? i18n.str`is not valid`
+ : undefined;
}),
),
default_max_deposit_fee: !value.default_max_deposit_fee
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
index 2b57ab429..ecf6e2ae5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -53,14 +53,23 @@ interface Props {
function convert(
from: MerchantBackend.Instances.QueryInstancesResponse,
): Entity {
- const { accounts, ...rest } = from;
- const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri);
+ const { accounts: qAccounts, ...rest } = from;
+ const accounts = qAccounts
+ .filter((a) => a.active)
+ .map(
+ (a) =>
+ ({
+ payto_uri: a.payto_uri,
+ credit_facade_url: a.credit_facade_url,
+ credit_facade_credentials: a.credit_facade_credentials,
+ } as MerchantBackend.Instances.MerchantBankAccount),
+ );
const defaults = {
default_wire_fee_amortization: 1,
default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours
default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours
};
- return { ...defaults, ...rest, payto_uris };
+ return { ...defaults, ...rest, accounts };
}
function getTokenValuePart(t?: string): string | undefined {
@@ -103,12 +112,14 @@ export function UpdatePage({
: value.user_type !== "business" && value.user_type !== "individual"
? i18n.str`should be business or individual`
: undefined,
- payto_uris:
- !value.payto_uris || !value.payto_uris.length
+ accounts:
+ !value.accounts || !value.accounts.length
? i18n.str`required`
: undefinedIfEmpty(
- value.payto_uris.map((p) => {
- return !PAYTO_REGEX.test(p) ? i18n.str`is not valid` : undefined;
+ value.accounts.map((p) => {
+ return !PAYTO_REGEX.test(p.payto_uri)
+ ? i18n.str`is not valid`
+ : undefined;
}),
),
default_max_deposit_fee: !value.default_max_deposit_fee
diff --git a/packages/merchant-backoffice-ui/src/stories.tsx b/packages/merchant-backoffice-ui/src/stories.tsx
index ccfde4ef2..2c61e5586 100644
--- a/packages/merchant-backoffice-ui/src/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/stories.tsx
@@ -22,6 +22,7 @@ import { strings } from "./i18n/strings.js";
import * as admin from "./paths/admin/index.stories.js";
import * as instance from "./paths/instance/index.stories.js";
+import * as components from "./components/index.stories.js";
import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
@@ -33,7 +34,7 @@ function SortStories(a: any, b: any): number {
function main(): void {
renderStories(
- { admin, instance },
+ { admin, instance, components },
{
strings,
},