fix #6363, breaking with merchant backend that accounts implemented

This commit is contained in:
Sebastian 2023-04-26 14:20:18 -03:00
parent 982fc51a97
commit 03d3cce827
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
9 changed files with 319 additions and 50 deletions

View File

@ -82,6 +82,12 @@ export interface FormType<T> {
const FormContext = createContext<FormType<unknown>>(null!); const FormContext = createContext<FormType<unknown>>(null!);
/**
* FIXME:
* USE MEMORY EVENTS INSTEAD OF CONTEXT
* @deprecated
*/
export function useFormContext<T>() { export function useFormContext<T>() {
return useContext<FormType<T>>(FormContext); return useContext<FormType<T>>(FormContext);
} }

View File

@ -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>
);
}, {});

View File

@ -28,6 +28,8 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js"; import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js"; import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js"; import { InputProps, useField } from "./useField.js";
import { InputWithAddon } from "./InputWithAddon.js";
import { MerchantBackend } from "../../declaration.js";
export interface Props<T> extends InputProps<T> { export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean; isValid?: (e: any) => boolean;
@ -50,6 +52,13 @@ type Entity = {
instruction?: string; instruction?: string;
[name: string]: string | undefined; [name: string]: string | undefined;
}; };
auth: {
type: "unset" | "basic" | "none";
url?: string;
username?: string;
password?: string;
repeat?: string;
};
}; };
function isEthereumAddress(address: string) { function isEthereumAddress(address: string) {
@ -162,8 +171,15 @@ const targets = [
"bitcoin", "bitcoin",
"ethereum", "ethereum",
]; ];
const accountAuthType = ["none", "basic"];
const noTargetValue = targets[0]; 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>({ export function InputPaytoForm<T>({
name, name,
@ -187,7 +203,7 @@ export function InputPaytoForm<T>({
} }
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const ops = value.options!; const ops = value.options ?? {};
const url = tryUrl(`payto://${value.target}${payToPath}`); const url = tryUrl(`payto://${value.target}${payToPath}`);
if (url) { if (url) {
Object.keys(ops).forEach((opt_key) => { Object.keys(ops).forEach((opt_key) => {
@ -222,6 +238,24 @@ export function InputPaytoForm<T>({
? i18n.str`required` ? i18n.str`required`
: undefined, : 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( const hasErrors = Object.keys(errors).some(
@ -229,10 +263,31 @@ export function InputPaytoForm<T>({
); );
const submit = useCallback((): void => { const submit = useCallback((): void => {
const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
const alreadyExists = const alreadyExists =
paytos.findIndex((x: string) => x === paytoURL) !== -1; accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
if (!alreadyExists) { 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); valueHandler(defaultTarget);
}, [value]); }, [value]);
@ -339,18 +394,106 @@ export function InputPaytoForm<T>({
</Fragment> </Fragment>
)} )}
{/**
* Show additional fields apart from the payto
*/}
{value.target !== noTargetValue && ( {value.target !== noTargetValue && (
<Fragment>
<Input <Input
name="options.receiver-name" name="options.receiver-name"
label={i18n.str`Name`} label={i18n.str`Name`}
tooltip={i18n.str`Bank account owner's 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 is-horizontal">
<div class="field-label is-normal" /> <div class="field-label is-normal" />
<div class="field-body" style={{ display: "block" }}> <div class="field-body" style={{ display: "block" }}>
{paytos.map((v: any, i: number) => ( {paytos.map(
(v: MerchantBackend.Instances.MerchantBankAccount, i: number) => (
<div <div
key={i} key={i}
class="tags has-addons mt-3 mb-0 mr-3" class="tags has-addons mt-3 mb-0 mr-3"
@ -360,7 +503,7 @@ export function InputPaytoForm<T>({
class="tag is-medium is-info mb-0" class="tag is-medium is-info mb-0"
style={{ maxWidth: "90%" }} style={{ maxWidth: "90%" }}
> >
{v} {v.payto_uri}
</span> </span>
<a <a
class="tag is-medium is-danger is-delete mb-0" class="tag is-medium is-danger is-delete mb-0"
@ -369,7 +512,8 @@ export function InputPaytoForm<T>({
}} }}
/> />
</div> </div>
))} ),
)}
{!paytos.length && i18n.str`No accounts yet.`} {!paytos.length && i18n.str`No accounts yet.`}
{required && ( {required && (
<span class="icon has-text-danger is-right"> <span class="icon has-text-danger is-right">

View File

@ -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";

View File

@ -86,7 +86,7 @@ export function DefaultInstanceFormFields({
/> />
<InputPaytoForm<Entity> <InputPaytoForm<Entity>
name="payto_uris" name="accounts"
label={i18n.str`Bank account`} label={i18n.str`Bank account`}
tooltip={i18n.str`URI specifying bank account for crediting revenue.`} tooltip={i18n.str`URI specifying bank account for crediting revenue.`}
/> />

View File

@ -262,15 +262,45 @@ export namespace MerchantBackend {
// header. // header.
token?: string; 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 //POST /private/instances
interface InstanceConfigurationMessage { 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 // 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 // inactive and thus unavailable for new contracts, but preserved
// in the database as existing offers and contracts may still refer // in the database as existing offers and contracts may still refer
// to it. // to it.
payto_uris: string[]; accounts: MerchantBankAccount[];
// Name of the merchant instance to create (will become $INSTANCE). // Name of the merchant instance to create (will become $INSTANCE).
id: string; id: string;
@ -326,10 +356,11 @@ export namespace MerchantBackend {
// PATCH /private/instances/$INSTANCE // PATCH /private/instances/$INSTANCE
interface InstanceReconfigurationMessage { interface InstanceReconfigurationMessage {
// 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 // multiple accounts, thus this is an array. Note that removing
// removing URIs from this list // URIs from this list deactivates the specified accounts
payto_uris: string[]; // (they will no longer be used for future contracts).
accounts: MerchantBankAccount[];
// Merchant name corresponding to this instance. // Merchant name corresponding to this instance.
name: string; name: string;
@ -491,6 +522,16 @@ export namespace MerchantBackend {
// salt used to compute h_wire // salt used to compute h_wire
salt: HashCode; 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, // true if this account is active,
// false if it is historic. // false if it is historic.
active: boolean; active: boolean;

View File

@ -47,7 +47,7 @@ interface Props {
function with_defaults(id?: string): Partial<Entity> { function with_defaults(id?: string): Partial<Entity> {
return { return {
id, id,
payto_uris: [], accounts: [],
user_type: "business", user_type: "business",
default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
default_wire_fee_amortization: 1, 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" : value.user_type !== "business" && value.user_type !== "individual"
? i18n.str`should be business or individual` ? i18n.str`should be business or individual`
: undefined, : undefined,
payto_uris: accounts:
!value.payto_uris || !value.payto_uris.length !value.accounts || !value.accounts.length
? i18n.str`required` ? i18n.str`required`
: undefinedIfEmpty( : undefinedIfEmpty(
value.payto_uris.map((p) => { value.accounts.map((p) => {
return !PAYTO_REGEX.test(p) ? i18n.str`is not valid` : undefined; return !PAYTO_REGEX.test(p.payto_uri)
? i18n.str`is not valid`
: undefined;
}), }),
), ),
default_max_deposit_fee: !value.default_max_deposit_fee default_max_deposit_fee: !value.default_max_deposit_fee

View File

@ -53,14 +53,23 @@ interface Props {
function convert( function convert(
from: MerchantBackend.Instances.QueryInstancesResponse, from: MerchantBackend.Instances.QueryInstancesResponse,
): Entity { ): Entity {
const { accounts, ...rest } = from; const { accounts: qAccounts, ...rest } = from;
const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri); 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 = { const defaults = {
default_wire_fee_amortization: 1, default_wire_fee_amortization: 1,
default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours 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 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 { function getTokenValuePart(t?: string): string | undefined {
@ -103,12 +112,14 @@ export function UpdatePage({
: value.user_type !== "business" && value.user_type !== "individual" : value.user_type !== "business" && value.user_type !== "individual"
? i18n.str`should be business or individual` ? i18n.str`should be business or individual`
: undefined, : undefined,
payto_uris: accounts:
!value.payto_uris || !value.payto_uris.length !value.accounts || !value.accounts.length
? i18n.str`required` ? i18n.str`required`
: undefinedIfEmpty( : undefinedIfEmpty(
value.payto_uris.map((p) => { value.accounts.map((p) => {
return !PAYTO_REGEX.test(p) ? i18n.str`is not valid` : undefined; return !PAYTO_REGEX.test(p.payto_uri)
? i18n.str`is not valid`
: undefined;
}), }),
), ),
default_max_deposit_fee: !value.default_max_deposit_fee default_max_deposit_fee: !value.default_max_deposit_fee

View File

@ -22,6 +22,7 @@ import { strings } from "./i18n/strings.js";
import * as admin from "./paths/admin/index.stories.js"; import * as admin from "./paths/admin/index.stories.js";
import * as instance from "./paths/instance/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"; import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
@ -33,7 +34,7 @@ function SortStories(a: any, b: any): number {
function main(): void { function main(): void {
renderStories( renderStories(
{ admin, instance }, { admin, instance, components },
{ {
strings, strings,
}, },