fix #6363, breaking with merchant backend that accounts implemented
This commit is contained in:
parent
982fc51a97
commit
03d3cce827
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}, {});
|
@ -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">
|
||||
|
@ -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";
|
@ -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.`}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user