[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated (4f796dd -> 54723c9)
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated (4f796dd -> 54723c9) |
Date: |
Tue, 30 Nov 2021 20:21:47 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a change to branch master
in repository merchant-backoffice.
from 4f796dd storybook
new b56c499 format
new 54723c9 fixing issue taken from christian comments:
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../src/components/form/InputPaytoForm.tsx | 341 +++++++++----
.../instance/DefaultInstanceFormFields.tsx | 138 +++--
packages/merchant-backoffice/src/i18n/index.tsx | 60 ++-
.../src/paths/admin/create/CreatePage.tsx | 257 ++++++----
.../src/paths/admin/create/index.tsx | 56 +-
.../paths/instance/orders/create/CreatePage.tsx | 568 ++++++++++++++-------
.../src/paths/instance/update/UpdatePage.tsx | 288 ++++++-----
.../src/paths/instance/update/index.tsx | 100 +++-
packages/merchant-backoffice/src/schemas/index.ts | 8 +-
.../merchant-backoffice/src/utils/constants.ts | 147 ++++++
10 files changed, 1318 insertions(+), 645 deletions(-)
diff --git
a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
b/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
index c52dc33..73cf751 100644
--- a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
@@ -15,12 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode, Fragment } from "preact";
import { useCallback, useState } from "preact/hooks";
-import { Translate, useTranslator } from "../../i18n";
+import { Translate, Translator, useTranslator } from "../../i18n";
+import { COUNTRY_TABLE } from "../../utils/constants";
import { FormErrors, FormProvider } from "./FormProvider";
import { Input } from "./Input";
import { InputGroup } from "./InputGroup";
@@ -33,135 +34,287 @@ export interface Props<T> extends InputProps<T> {
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
- target: string,
- path: string,
- path1: string,
- path2: string,
- host: string,
- account: string,
+ // iban, bitcoin, x-taler-bank. it defined the format
+ target: string;
+ // path1 if the first field to be used
+ path1: string;
+ // path2 if the second field to be used, optional
+ path2?: string;
+ // options of the payto uri
options: {
- 'receiver-name'?: string,
- sender?: string,
- message?: string,
- amount?: string,
- instruction?: string,
- [name: string]: string | undefined,
- },
+ "receiver-name"?: string;
+ sender?: string;
+ message?: string;
+ amount?: string;
+ instruction?: string;
+ [name: string]: string | undefined;
+ };
+};
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not,
the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding
the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of
that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be
valid.
+ *
+ */
+function validateIBAN(iban: string, i18n: Translator): string | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+ // check supported country
+ const code = IBAN.substr(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substr(4) + iban.substr(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substr(0, 5);
+ const rest = str.substr(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+ }
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1) return i18n`IBAN number is not valid, checksum is wrong`;
+ return undefined;
}
// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void',
'x-taler-bank']
-const targets = ['iban', 'x-taler-bank']
-const defaultTarget = { target: 'iban', options: {} }
+const targets = ["iban", "x-taler-bank"];
+const defaultTarget = { target: "iban", options: {} };
function undefinedIfEmpty<T>(obj: T): T | undefined {
- return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj :
undefined
+ return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ ? obj
+ : undefined;
}
-export function InputPaytoForm<T>({ name, readonly, label, tooltip }:
Props<keyof T>): VNode {
- const { value: paytos, onChange, } = useField<T>(name);
+export function InputPaytoForm<T>({
+ name,
+ readonly,
+ label,
+ tooltip,
+}: Props<keyof T>): VNode {
+ const { value: paytos, onChange } = useField<T>(name);
- const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget)
+ const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
- if (value.path1) {
+ let payToPath;
+ if (value.target === "iban" && value.path1) {
+ payToPath = `/${value.path1.toUpperCase()}`;
+ } else if (value.path1) {
if (value.path2) {
- value.path = `/${value.path1}/${value.path2}`
+ payToPath = `/${value.path1}/${value.path2}`;
} else {
- value.path = `/${value.path1}`
+ payToPath = `/${value.path1}`;
}
}
- const i18n = useTranslator()
+ const i18n = useTranslator();
- const url = new URL(`payto://${value.target}${value.path}`)
- const ops = value.options!
- Object.keys(ops).forEach(opt_key => {
- const opt_value = ops[opt_key]
- if (opt_value) url.searchParams.set(opt_key, opt_value)
- })
- const paytoURL = url.toString()
+ const url = new URL(`payto://${value.target}${payToPath}`);
+ const ops = value.options!;
+ Object.keys(ops).forEach((opt_key) => {
+ const opt_value = ops[opt_key];
+ if (opt_value) url.searchParams.set(opt_key, opt_value);
+ });
+ const paytoURL = url.toString();
const errors: FormErrors<Entity> = {
target: !value.target ? i18n`required` : undefined,
- path1: !value.path1 ? i18n`required` : (
- value.target === 'iban' ? (
- value.path1.length < 4 ? i18n`IBAN numbers usually have more that 4
digits` : (
- value.path1.length > 34 ? i18n`IBAN numbers usually have less that
34 digits` :
- undefined
- )
- ): undefined
- ),
- path2: value.target === 'x-taler-bank' ? (!value.path2 ? i18n`required` :
undefined) : undefined,
+ path1: !value.path1
+ ? i18n`required`
+ : value.target === "iban"
+ ? validateIBAN(value.path1, i18n)
+ : undefined,
+ path2:
+ value.target === "x-taler-bank"
+ ? !value.path2
+ ? i18n`required`
+ : undefined
+ : undefined,
options: undefinedIfEmpty({
- 'receiver-name': !value.options?.["receiver-name"] ? i18n`required` :
undefined,
- })
- }
+ "receiver-name": !value.options?.["receiver-name"]
+ ? i18n`required`
+ : undefined,
+ }),
+ };
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !==
undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
const submit = useCallback((): void => {
- const alreadyExists = paytos.findIndex((x:string) => x === paytoURL) !==
-1;
+ const alreadyExists =
+ paytos.findIndex((x: string) => x === paytoURL) !== -1;
if (!alreadyExists) {
- onChange([paytoURL, ...paytos] as any)
+ onChange([paytoURL, ...paytos] as any);
}
- valueHandler(defaultTarget)
- }, [value])
-
+ valueHandler(defaultTarget);
+ }, [value]);
//FIXME: translating plural singular
return (
<InputGroup name="payto" label={label} fixed tooltip={tooltip}>
- <FormProvider<Entity> name="tax" errors={errors} object={value}
valueHandler={valueHandler} >
-
- <InputSelector<Entity> name="target" label={i18n`Target type`}
tooltip={i18n`Method to use for wire transfer`} values={targets} />
-
- {value.target === 'ach' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Routing`}
tooltip={i18n`Routing number.`} />
- <Input<Entity> name="path2" label={i18n`Account`}
tooltip={i18n`Account number.`} />
- </Fragment>}
- {value.target === 'bic' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Code`}
tooltip={i18n`Business Identifier Code.`} />
- </Fragment>}
- {value.target === 'iban' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Account`} tooltip={i18n`Bank
Account Number.`} />
- </Fragment>}
- {value.target === 'upi' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Account`}
tooltip={i18n`Unified Payment Interface.`} />
- </Fragment>}
- {value.target === 'bitcoin' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Address`}
tooltip={i18n`Bitcoin protocol.`} />
- </Fragment>}
- {value.target === 'ilp' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Address`}
tooltip={i18n`Interledger protocol.`} />
- </Fragment>}
- {value.target === 'void' && <Fragment>
- </Fragment>}
- {value.target === 'x-taler-bank' && <Fragment>
- <Input<Entity> name="path1" label={i18n`Host`} tooltip={i18n`Bank
host.`} />
- <Input<Entity> name="path2" label={i18n`Account`} tooltip={i18n`Bank
account.`} />
- </Fragment>}
-
- <Input name="options.receiver-name" label={i18n`Name`}
tooltip={i18n`Bank account owner's name.`} />
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <InputSelector<Entity>
+ name="target"
+ label={i18n`Target type`}
+ tooltip={i18n`Method to use for wire transfer`}
+ values={targets}
+ />
+
+ {value.target === "ach" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Routing`}
+ tooltip={i18n`Routing number.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n`Account`}
+ tooltip={i18n`Account number.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bic" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Code`}
+ tooltip={i18n`Business Identifier Code.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "iban" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Account`}
+ tooltip={i18n`Bank Account Number.`}
+ inputExtra={{ style: { textTransform: "uppercase" } }}
+ />
+ </Fragment>
+ )}
+ {value.target === "upi" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Account`}
+ tooltip={i18n`Unified Payment Interface.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bitcoin" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Address`}
+ tooltip={i18n`Bitcoin protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ilp" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Address`}
+ tooltip={i18n`Interledger protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "void" && <Fragment />}
+ {value.target === "x-taler-bank" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Host`}
+ tooltip={i18n`Bank host.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n`Account`}
+ tooltip={i18n`Bank account.`}
+ />
+ </Fragment>
+ )}
+
+ <Input
+ name="options.receiver-name"
+ label={i18n`Name`}
+ tooltip={i18n`Bank account owner's name.`}
+ />
<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%' }}>{v}</span>
- <a class="tag is-medium is-danger is-delete mb-0" onClick={() =>
{
- onChange(paytos.filter((f: any) => f !== v) as any);
- }} />
- </div>
- )}
+ <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%" }}
+ >
+ {v}
+ </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`No accounts yet.`}
</div>
</div>
<div class="buttons is-right mt-5">
- <button class="button is-info"
+ <button
+ class="button is-info"
data-tooltip={i18n`add tax to the tax list`}
disabled={hasErrors}
- onClick={submit}><Translate>Add</Translate></button>
+ onClick={submit}
+ >
+ <Translate>Add</Translate>
+ </button>
</div>
</FormProvider>
</InputGroup>
- )
+ );
}
diff --git
a/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
b/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
index fae8a35..8c59a6d 100644
---
a/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
+++
b/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
@@ -15,11 +15,11 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useBackendContext } from "../../context/backend";
import { useTranslator } from "../../i18n";
import { Entity } from "../../paths/admin/create/CreatePage";
@@ -31,56 +31,86 @@ import { InputLocation } from "../form/InputLocation";
import { InputPaytoForm } from "../form/InputPaytoForm";
import { InputWithAddon } from "../form/InputWithAddon";
-export function DefaultInstanceFormFields({ readonlyId, showId }: {
readonlyId?: boolean; showId: boolean }) {
+export function DefaultInstanceFormFields({
+ readonlyId,
+ showId,
+}: {
+ readonlyId?: boolean;
+ showId: boolean;
+}): VNode {
const i18n = useTranslator();
const backend = useBackendContext();
- return <Fragment>
- {showId && <InputWithAddon<Entity> name="id"
- addonBefore={`${backend.url}/instances/`} readonly={readonlyId}
- label={i18n`Identifier`}
- tooltip={i18n`Name of the instance in URLs. The 'default' instance is
special in that it is used to administer other instances.`} />
- }
-
- <Input<Entity> name="name"
- label={i18n`Business name`}
- tooltip={i18n`Legal name of the business represented by this instance.`}
/>
-
- <InputPaytoForm<Entity> name="payto_uris"
- label={i18n`Bank account`}
- tooltip={i18n`URI specifying bank account for crediting revenue.`} />
-
- <InputCurrency<Entity> name="default_max_deposit_fee"
- label={i18n`Default max deposit fee`}
- tooltip={i18n`Maximum deposit fees this merchant is willing to pay per
order by default.`} />
-
- <InputCurrency<Entity> name="default_max_wire_fee"
- label={i18n`Default max wire fee`}
- tooltip={i18n`Maximum wire fees this merchant is willing to pay per wire
transfer by default.`} />
-
- <Input<Entity> name="default_wire_fee_amortization"
- label={i18n`Default wire fee amortization`}
- tooltip={i18n`Number of orders excess wire transfer fees will be divided
by to compute per order surcharge.`} />
-
- <InputGroup name="address"
- label={i18n`Address`}
- tooltip={i18n`Physical location of the merchant.`}>
- <InputLocation name="address" />
- </InputGroup>
-
- <InputGroup name="jurisdiction"
- label={i18n`Jurisdiction`}
- tooltip={i18n`Jurisdiction for legal disputes with the merchant.`}>
- <InputLocation name="jurisdiction" />
- </InputGroup>
-
- <InputDuration<Entity> name="default_pay_delay"
- label={i18n`Default payment delay`}
- withForever
- tooltip={i18n`Time customers have to pay an order before the offer
expires by default.`} />
-
- <InputDuration<Entity> name="default_wire_transfer_delay"
- label={i18n`Default wire transfer delay`}
- tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds
to the merchant, enabling it to aggregate smaller payments into larger wire
transfers and reducing wire fees.`} />
-
- </Fragment>;
+ return (
+ <Fragment>
+ {showId && (
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`${backend.url}/instances/`}
+ readonly={readonlyId}
+ label={i18n`Identifier`}
+ tooltip={i18n`Name of the instance in URLs. The 'default' instance
is special in that it is used to administer other instances.`}
+ />
+ )}
+
+ <Input<Entity>
+ name="name"
+ label={i18n`Business name`}
+ tooltip={i18n`Legal name of the business represented by this
instance.`}
+ />
+
+ <InputPaytoForm<Entity>
+ name="payto_uris"
+ label={i18n`Bank account`}
+ tooltip={i18n`URI specifying bank account for crediting revenue.`}
+ />
+
+ <InputCurrency<Entity>
+ name="default_max_deposit_fee"
+ label={i18n`Default max deposit fee`}
+ tooltip={i18n`Maximum deposit fees this merchant is willing to pay per
order by default.`}
+ />
+
+ <InputCurrency<Entity>
+ name="default_max_wire_fee"
+ label={i18n`Default max wire fee`}
+ tooltip={i18n`Maximum wire fees this merchant is willing to pay per
wire transfer by default.`}
+ />
+
+ <Input<Entity>
+ name="default_wire_fee_amortization"
+ label={i18n`Default wire fee amortization`}
+ tooltip={i18n`Number of orders excess wire transfer fees will be
divided by to compute per order surcharge.`}
+ />
+
+ <InputGroup
+ name="address"
+ label={i18n`Address`}
+ tooltip={i18n`Physical location of the merchant.`}
+ >
+ <InputLocation name="address" />
+ </InputGroup>
+
+ <InputGroup
+ name="jurisdiction"
+ label={i18n`Jurisdiction`}
+ tooltip={i18n`Jurisdiction for legal disputes with the merchant.`}
+ >
+ <InputLocation name="jurisdiction" />
+ </InputGroup>
+
+ <InputDuration<Entity>
+ name="default_pay_delay"
+ label={i18n`Default payment delay`}
+ withForever
+ tooltip={i18n`Time customers have to pay an order before the offer
expires by default.`}
+ />
+
+ <InputDuration<Entity>
+ name="default_wire_transfer_delay"
+ label={i18n`Default wire transfer delay`}
+ tooltip={i18n`Maximum time an exchange is allowed to delay wiring
funds to the merchant, enabling it to aggregate smaller payments into larger
wire transfers and reducing wire fees.`}
+ withForever
+ />
+ </Fragment>
+ );
}
diff --git a/packages/merchant-backoffice/src/i18n/index.tsx
b/packages/merchant-backoffice/src/i18n/index.tsx
index 63c8e19..9403de1 100644
--- a/packages/merchant-backoffice/src/i18n/index.tsx
+++ b/packages/merchant-backoffice/src/i18n/index.tsx
@@ -25,25 +25,31 @@ import { ComponentChild, ComponentChildren, h, Fragment,
VNode } from "preact";
import { useTranslationContext } from "../context/translation";
-export function useTranslator() {
+export type Translator = (
+ stringSeq: TemplateStringsArray,
+ ...values: any[]
+) => string;
+export function useTranslator(): Translator {
const ctx = useTranslationContext();
- const jed = ctx.handler
- return function str(stringSeq: TemplateStringsArray, ...values: any[]):
string {
+ const jed = ctx.handler;
+ return function str(
+ stringSeq: TemplateStringsArray,
+ ...values: any[]
+ ): string {
const s = toI18nString(stringSeq);
- if (!s) return s
+ if (!s) return s;
const tr = jed
.translate(s)
.ifPlural(1, s)
.fetch(...values);
return tr;
- }
+ };
}
-
/**
* Convert template strings to a msgid
*/
- function toI18nString(stringSeq: ReadonlyArray<string>): string {
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@@ -54,7 +60,6 @@ export function useTranslator() {
return s;
}
-
interface TranslateSwitchProps {
target: number;
children: ComponentChildren;
@@ -88,7 +93,7 @@ interface TranslateProps {
function getTranslatedChildren(
translation: string,
- children: ComponentChildren,
+ children: ComponentChildren
): ComponentChild[] {
const tr = translation.split(/%(\d+)\$s/);
const childArray = children instanceof Array ? children : [children];
@@ -110,7 +115,7 @@ function getTranslatedChildren(
// Text
result.push(tr[i]);
} else {
- const childIdx = Number.parseInt(tr[i],10) - 1;
+ const childIdx = Number.parseInt(tr[i], 10) - 1;
result.push(placeholderChildren[childIdx]);
}
}
@@ -131,9 +136,9 @@ function getTranslatedChildren(
*/
export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children)
+ const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
@@ -154,14 +159,16 @@ export function TranslateSwitch({ children, target }:
TranslateSwitchProps) {
let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children;
if (children) {
- (children instanceof Array ? children : [children]).forEach((child: any)
=> {
- if (child.type === TranslatePlural) {
- plural = child;
+ (children instanceof Array ? children : [children]).forEach(
+ (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
}
- if (child.type === TranslateSingular) {
- singular = child;
- }
- });
+ );
}
if (!singular || !plural) {
console.error("translation not found");
@@ -182,9 +189,12 @@ interface TranslationPluralProps {
/**
* See [[TranslateSwitch]].
*/
-export function TranslatePlural({ children, target }: TranslationPluralProps):
VNode {
+export function TranslatePlural({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
@@ -193,11 +203,13 @@ export function TranslatePlural({ children, target }:
TranslationPluralProps): V
/**
* See [[TranslateSwitch]].
*/
-export function TranslateSingular({ children, target }:
TranslationPluralProps): VNode {
+export function TranslateSingular({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
-
}
diff --git a/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx
b/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx
index f5fa7c9..f1214c9 100644
--- a/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice/src/paths/admin/create/CreatePage.tsx
@@ -15,15 +15,18 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import * as yup from 'yup';
+import * as yup from "yup";
import { AsyncButton } from "../../../components/exception/AsyncButton";
-import { FormErrors, FormProvider } from
"../../../components/form/FormProvider";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider";
import { SetTokenNewInstanceModal } from "../../../components/modal";
import { MerchantBackend } from "../../../declaration";
import { Translate, useTranslator } from "../../../i18n";
@@ -31,10 +34,9 @@ import { DefaultInstanceFormFields } from
"../../../components/instance/DefaultI
import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants";
import { Amounts } from "@gnu-taler/taler-util";
-export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage &
{
- auth_token?: string
-}
-
+export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
+ auth_token?: string;
+};
interface Props {
onCreate: (d: Entity) => Promise<void>;
@@ -53,137 +55,180 @@ function with_defaults(id?: string): Partial<Entity> {
}
function undefinedIfEmpty<T>(obj: T): T | undefined {
- return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj :
undefined
+ return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ ? obj
+ : undefined;
}
export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
- const [value, valueHandler] = useState(with_defaults(forceId))
+ const [value, valueHandler] = useState(with_defaults(forceId));
const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
- const [isTokenDialogActive, updateIsTokenDialogActive] =
useState<boolean>(false);
+ const [isTokenDialogActive, updateIsTokenDialogActive] =
+ useState<boolean>(false);
- const i18n = useTranslator()
+ const i18n = useTranslator();
const errors: FormErrors<Entity> = {
- id: !value.id ? i18n`required` : (!INSTANCE_ID_REGEX.test(value.id) ?
i18n`is not valid` : undefined),
+ id: !value.id
+ ? i18n`required`
+ : !INSTANCE_ID_REGEX.test(value.id)
+ ? i18n`is not valid`
+ : undefined,
name: !value.name ? i18n`required` : undefined,
payto_uris:
- !value.payto_uris || !value.payto_uris.length ? i18n`required` : (
- undefinedIfEmpty(value.payto_uris.map(p => {
- return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined
- }))
- ),
- default_max_deposit_fee:
- !value.default_max_deposit_fee ? i18n`required` : (
- !Amounts.parse(value.default_max_deposit_fee) ? i18n`invalid format` :
- undefined
- ),
- default_max_wire_fee:
- !value.default_max_wire_fee ? i18n`required` : (
- !Amounts.parse(value.default_max_wire_fee) ? i18n`invalid format` :
- undefined
- ),
+ !value.payto_uris || !value.payto_uris.length
+ ? i18n`required`
+ : undefinedIfEmpty(
+ value.payto_uris.map((p) => {
+ return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined;
+ })
+ ),
+ default_max_deposit_fee: !value.default_max_deposit_fee
+ ? i18n`required`
+ : !Amounts.parse(value.default_max_deposit_fee)
+ ? i18n`invalid format`
+ : undefined,
+ default_max_wire_fee: !value.default_max_wire_fee
+ ? i18n`required`
+ : !Amounts.parse(value.default_max_wire_fee)
+ ? i18n`invalid format`
+ : undefined,
default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined ? i18n`required` : (
- isNaN(value.default_wire_fee_amortization) ? i18n`is not a number` : (
- value.default_wire_fee_amortization < 1 ? i18n`must be 1 or greater`
:
- undefined
- )
- ),
- default_pay_delay:
- !value.default_pay_delay ? i18n`required` : undefined,
- default_wire_transfer_delay:
- !value.default_wire_transfer_delay ? i18n`required` : undefined,
+ value.default_wire_fee_amortization === undefined
+ ? i18n`required`
+ : isNaN(value.default_wire_fee_amortization)
+ ? i18n`is not a number`
+ : value.default_wire_fee_amortization < 1
+ ? i18n`must be 1 or greater`
+ : undefined,
+ default_pay_delay: !value.default_pay_delay ? i18n`required` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n`required`
+ : undefined,
address: undefinedIfEmpty({
address_lines:
- value.address?.address_lines && value.address?.address_lines.length >
7 ? i18n`max 7 lines` :
- undefined
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n`max 7 lines`
+ : undefined,
}),
jurisdiction: undefinedIfEmpty({
- address_lines: value.address?.address_lines &&
value.address?.address_lines.length > 7 ? i18n`max 7 lines` :
- undefined
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n`max 7 lines`
+ : undefined,
}),
};
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !==
undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
const submit = (): Promise<void> => {
// use conversion instead of this
const newToken = value.auth_token;
value.auth_token = undefined;
- value.auth = newToken === null || newToken === undefined ? { method:
"external" } : { method: "token", token: `secret-token:${newToken}` };
- if (!value.address) value.address = {}
- if (!value.jurisdiction) value.jurisdiction = {}
+ value.auth =
+ newToken === null || newToken === undefined
+ ? { method: "external" }
+ : { method: "token", token: `secret-token:${newToken}` };
+ if (!value.address) value.address = {};
+ if (!value.jurisdiction) value.jurisdiction = {};
// remove above use conversion
// schema.validateSync(value, { abortEarly: false })
return onCreate(value as Entity);
- }
+ };
function updateToken(token: string | null) {
- valueHandler(old => ({ ...old, auth_token: token === null ? undefined :
token }))
+ valueHandler((old) => ({
+ ...old,
+ auth_token: token === null ? undefined : token,
+ }));
}
- return <div>
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- {isTokenDialogActive && <SetTokenNewInstanceModal
- onCancel={() => {
- updateIsTokenDialogActive(false);
- updateIsTokenSet(false);
- }}
- onClear={() => {
- updateToken(null);
- updateIsTokenDialogActive(false);
- updateIsTokenSet(true);
- }}
- onConfirm={(newToken) => {
- updateToken(newToken); updateIsTokenDialogActive(false);
- updateIsTokenSet(true);
- }}
- />}
- </div>
- <div class="column" />
- </div>
-
- <section class="hero is-hero-bar">
- <div class="hero-body">
- <div class="level">
- <div class="level-item has-text-centered">
- <h1 class="title">
- <button class="button is-danger has-tooltip-bottom"
- data-tooltip={i18n`change authorization configuration`}
- onClick={() => updateIsTokenDialogActive(true)} >
- <div class="icon is-centered"><i class="mdi mdi-lock-reset"
/></div>
- <span><Translate>Set access token</Translate></span>
- </button>
- </h1>
- </div>
- </div>
- </div></section>
-
-
- <section class="section is-main-section">
+ return (
+ <div>
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
-
- <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
-
- <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} />
-
- </FormProvider>
-
- <div class="buttons is-right mt-5">
- {onBack && <button class="button"
onClick={onBack}><Translate>Cancel</Translate></button>}
- <AsyncButton onClick={submit} disabled={!isTokenSet || hasErrors}
data-tooltip={
- hasErrors ? i18n`Need to complete marked fields and choose
authorization method` : 'confirm operation'
- }><Translate>Confirm</Translate></AsyncButton>
- </div>
-
+ {isTokenDialogActive && (
+ <SetTokenNewInstanceModal
+ onCancel={() => {
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(false);
+ }}
+ onClear={() => {
+ updateToken(null);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ onConfirm={(newToken) => {
+ updateToken(newToken);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ />
+ )}
</div>
<div class="column" />
</div>
- </section>
- </div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <h1 class="title">
+ <button
+ class="button is-danger has-tooltip-bottom"
+ data-tooltip={i18n`change authorization configuration`}
+ onClick={() => updateIsTokenDialogActive(true)}
+ >
+ <div class="icon is-centered">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <Translate>Set access token</Translate>
+ </span>
+ </button>
+ </h1>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields readonlyId={!!forceId} showId={true}
/>
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <Translate>Cancel</Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submit}
+ disabled={!isTokenSet || hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n`Need to complete marked fields and choose
authorization method`
+ : "confirm operation"
+ }
+ >
+ <Translate>Confirm</Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
}
diff --git a/packages/merchant-backoffice/src/paths/admin/create/index.tsx
b/packages/merchant-backoffice/src/paths/admin/create/index.tsx
index e240bb6..3f31b3d 100644
--- a/packages/merchant-backoffice/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice/src/paths/admin/create/index.tsx
@@ -14,9 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../components/menu";
@@ -36,31 +36,39 @@ export type Entity =
MerchantBackend.Instances.InstanceConfigurationMessage;
export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
const { createInstance } = useAdminAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined)
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined);
- const i18n = useTranslator()
+ const i18n = useTranslator();
if (createdOk) {
- return <InstanceCreatedSuccessfully entity={createdOk}
onConfirm={onConfirm} />
+ return (
+ <InstanceCreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />
+ );
}
- return <Fragment>
- <NotificationCard notification={notif} />
-
- <CreatePage
- onBack={onBack}
- forceId={forceId}
- onCreate={(d: MerchantBackend.Instances.InstanceConfigurationMessage) =>
{
- return createInstance(d).then(() => {
- setCreatedOk(d)
- }).catch((error) => {
- setNotif({
- message: i18n`Failed to create instance`,
- type: "ERROR",
- description: error.message
- })
- })
- }} />
- </Fragment>
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ forceId={forceId}
+ onCreate={(
+ d: MerchantBackend.Instances.InstanceConfigurationMessage
+ ) => {
+ return createInstance(d)
+ .then(() => {
+ setCreatedOk(d);
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n`Failed to create instance`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
}
diff --git
a/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
b/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
index e0e9970..580ead1 100644
---
a/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
+++
b/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
@@ -15,15 +15,18 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { add, isAfter, isBefore, isFuture } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { FormProvider, FormErrors } from
"../../../../components/form/FormProvider";
+import {
+ FormProvider,
+ FormErrors,
+} from "../../../../components/form/FormProvider";
import { Input } from "../../../../components/form/Input";
import { InputCurrency } from "../../../../components/form/InputCurrency";
import { InputDate } from "../../../../components/form/InputDate";
@@ -33,17 +36,18 @@ import { ProductList } from
"../../../../components/product/ProductList";
import { useConfigContext } from "../../../../context/config";
import { Duration, MerchantBackend, WithId } from "../../../../declaration";
import { Translate, useTranslator } from "../../../../i18n";
-import { OrderCreateSchema as schema } from '../../../../schemas/index';
+import { OrderCreateSchema as schema } from "../../../../schemas/index";
import { rate } from "../../../../utils/amount";
import { InventoryProductForm } from
"../../../../components/product/InventoryProductForm";
import { NonInventoryProductFrom } from
"../../../../components/product/NonInventoryProductForm";
import { InputNumber } from "../../../../components/form/InputNumber";
+import { InputBoolean } from "../../../../components/form/InputBoolean";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
onBack?: () => void;
instanceConfig: InstanceConfig;
- instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[],
+ instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
}
interface InstanceConfig {
default_max_wire_fee: string;
@@ -53,9 +57,10 @@ interface InstanceConfig {
}
function with_defaults(config: InstanceConfig): Partial<Entity> {
- const defaultPayDeadline = !config.default_pay_delay ||
config.default_pay_delay.d_ms === "forever" ?
- undefined :
- add(new Date(), { seconds: config.default_pay_delay.d_ms / 1000 })
+ const defaultPayDeadline =
+ !config.default_pay_delay || config.default_pay_delay.d_ms === "forever"
+ ? undefined
+ : add(new Date(), { seconds: config.default_pay_delay.d_ms / 1000 });
return {
inventoryProducts: {},
@@ -67,14 +72,15 @@ function with_defaults(config: InstanceConfig):
Partial<Entity> {
wire_fee_amortization: config.default_wire_fee_amortization,
pay_deadline: defaultPayDeadline,
refund_deadline: defaultPayDeadline,
+ createToken: true,
},
shipping: {},
- extra: ''
+ extra: "",
};
}
interface ProductAndQuantity {
- product: MerchantBackend.Products.ProductDetail & WithId,
+ product: MerchantBackend.Products.ProductDetail & WithId;
quantity: number;
}
export interface ProductMap {
@@ -94,14 +100,16 @@ interface Shipping {
interface Payments {
refund_deadline?: Date;
pay_deadline?: Date;
+ wire_transfer_deadline?: Date;
auto_refund_deadline?: Date;
max_fee?: string;
max_wire_fee?: string;
wire_fee_amortization?: number;
+ createToken: boolean;
}
interface Entity {
- inventoryProducts: ProductMap,
- products: MerchantBackend.Product[],
+ inventoryProducts: ProductMap;
+ products: MerchantBackend.Product[];
pricing: Partial<Pricing>;
payments: Partial<Payments>;
shipping: Partial<Shipping>;
@@ -110,65 +118,100 @@ interface Entity {
const stringIsValidJSON = (value: string) => {
try {
- JSON.parse(value.trim())
- return true
+ JSON.parse(value.trim());
+ return true;
} catch {
- return false
+ return false;
}
-}
+};
function undefinedIfEmpty<T>(obj: T): T | undefined {
- return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj :
undefined
+ return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ ? obj
+ : undefined;
}
-export function CreatePage({ onCreate, onBack, instanceConfig,
instanceInventory }: Props): VNode {
- const [value, valueHandler] = useState(with_defaults(instanceConfig))
- const config = useConfigContext()
- const zero = Amounts.getZero(config.currency)
+export function CreatePage({
+ onCreate,
+ onBack,
+ instanceConfig,
+ instanceInventory,
+}: Props): VNode {
+ const [value, valueHandler] = useState(with_defaults(instanceConfig));
+ const config = useConfigContext();
+ const zero = Amounts.getZero(config.currency);
const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
const i18n = useTranslator();
-
+
const errors: FormErrors<Entity> = {
pricing: undefinedIfEmpty({
summary: !value.pricing?.summary ? i18n`required` : undefined,
- order_price: !value.pricing?.order_price ? i18n`required` : (
- (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ?
- i18n`must be greater than 0` : undefined
- )
+ order_price: !value.pricing?.order_price
+ ? i18n`required`
+ : (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0
+ ? i18n`must be greater than 0`
+ : undefined,
}),
- extra: value.extra && !stringIsValidJSON(value.extra) ? i18n`not a valid
json` : undefined,
+ extra:
+ value.extra && !stringIsValidJSON(value.extra)
+ ? i18n`not a valid json`
+ : undefined,
payments: undefinedIfEmpty({
- refund_deadline: !value.payments?.refund_deadline ? i18n`required` : (
- !isFuture(value.payments.refund_deadline) ? i18n`should be in the
future` : (
- value.payments.pay_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ?
- i18n`pay deadline cannot be before refund deadline` : undefined
- )
- ),
- pay_deadline: !value.payments?.pay_deadline ? i18n`required` : (
- !isFuture(value.payments.pay_deadline) ? i18n`should be in the future`
: undefined
- ),
- auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined
: (
- !isFuture(value.payments.auto_refund_deadline) ? i18n`should be in the
future` : (
- !value.payments?.refund_deadline ? i18n`should have a refund
deadline` : (
- !isAfter(value.payments.refund_deadline,
value.payments.auto_refund_deadline) ?
- i18n`auto refund cannot be after refund deadline` : undefined
+ refund_deadline: !value.payments?.refund_deadline
+ ? undefined
+ : !isFuture(value.payments.refund_deadline)
+ ? i18n`should be in the future`
+ : value.payments.pay_deadline &&
+ isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
+ ? i18n`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ isBefore(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline
)
- )
- ),
+ ? i18n`wire transfer deadline cannot be before refund deadline`
+ : undefined,
+ pay_deadline: !value.payments?.pay_deadline
+ ? undefined
+ : !isFuture(value.payments.pay_deadline)
+ ? i18n`should be in the future`
+ : value.payments.wire_transfer_deadline &&
+ isBefore(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline
+ )
+ ? i18n`wire transfer deadline cannot be before pay deadline`
+ : undefined,
+ auto_refund_deadline: !value.payments?.auto_refund_deadline
+ ? undefined
+ : !isFuture(value.payments.auto_refund_deadline)
+ ? i18n`should be in the future`
+ : !value.payments?.refund_deadline
+ ? i18n`should have a refund deadline`
+ : !isAfter(
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline
+ )
+ ? i18n`auto refund cannot be after refund deadline`
+ : undefined,
}),
shipping: undefinedIfEmpty({
- delivery_date: !value.shipping?.delivery_date ? undefined : (
- !isFuture(value.shipping.delivery_date) ? i18n`should be in the
future` : undefined
- ),
+ delivery_date: !value.shipping?.delivery_date
+ ? undefined
+ : !isFuture(value.shipping.delivery_date)
+ ? i18n`should be in the future`
+ : undefined,
}),
- }
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !==
undefined)
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
const submit = (): void => {
- const order = schema.cast(value)
+ const order = schema.cast(value);
if (!value.payments) return;
if (!value.shipping) return;
@@ -178,198 +221,335 @@ export function CreatePage({ onCreate, onBack,
instanceConfig, instanceInventory
summary: order.pricing.summary,
products: productList,
extra: value.extra,
- pay_deadline: value.payments.pay_deadline ? { t_ms:
Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined,
- wire_transfer_deadline: value.payments.pay_deadline ? { t_ms:
Math.floor(value.payments.pay_deadline.getTime() / 1000) * 1000 } : undefined,
- refund_deadline: value.payments.refund_deadline ? { t_ms:
Math.floor(value.payments.refund_deadline.getTime() / 1000) * 1000 } :
undefined,
+ pay_deadline: value.payments.pay_deadline
+ ? {
+ t_ms:
+ Math.floor(value.payments.pay_deadline.getTime() / 1000) *
1000,
+ }
+ : undefined,
+ wire_transfer_deadline: value.payments.wire_transfer_deadline
+ ? {
+ t_ms:
+ Math.floor(
+ value.payments.wire_transfer_deadline.getTime() / 1000
+ ) * 1000,
+ }
+ : undefined,
+ refund_deadline: value.payments.refund_deadline
+ ? {
+ t_ms:
+ Math.floor(value.payments.refund_deadline.getTime() / 1000) *
+ 1000,
+ }
+ : undefined,
wire_fee_amortization: value.payments.wire_fee_amortization,
max_fee: value.payments.max_fee,
max_wire_fee: value.payments.max_wire_fee,
- delivery_date: value.shipping.delivery_date ? { t_ms:
value.shipping.delivery_date.getTime() } : undefined,
+ delivery_date: value.shipping.delivery_date
+ ? { t_ms: value.shipping.delivery_date.getTime() }
+ : undefined,
delivery_location: value.shipping.delivery_location,
fulfillment_url: value.shipping.fullfilment_url,
},
- inventory_products: inventoryList.map(p => ({
+ inventory_products: inventoryList.map((p) => ({
product_id: p.product.id,
- quantity: p.quantity
+ quantity: p.quantity,
})),
- }
+ create_token: value.payments.createToken,
+ };
onCreate(request);
- }
+ };
- const addProductToTheInventoryList = (product:
MerchantBackend.Products.ProductDetail & WithId, quantity: number) => {
- valueHandler(v => {
- const inventoryProducts = { ...v.inventoryProducts }
- inventoryProducts[product.id] = { product, quantity }
- return ({ ...v, inventoryProducts })
- })
- }
+ const addProductToTheInventoryList = (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number
+ ) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ inventoryProducts[product.id] = { product, quantity };
+ return { ...v, inventoryProducts };
+ });
+ };
const removeProductFromTheInventoryList = (id: string) => {
- valueHandler(v => {
- const inventoryProducts = { ...v.inventoryProducts }
- delete inventoryProducts[id]
- return ({ ...v, inventoryProducts })
- })
- }
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ delete inventoryProducts[id];
+ return { ...v, inventoryProducts };
+ });
+ };
const addNewProduct = async (product: MerchantBackend.Product) => {
- return valueHandler(v => {
- const products = v.products ? [...v.products, product] : []
- return ({ ...v, products })
- })
- }
+ return valueHandler((v) => {
+ const products = v.products ? [...v.products, product] : [];
+ return { ...v, products };
+ });
+ };
const removeFromNewProduct = (index: number) => {
- valueHandler(v => {
- const products = v.products ? [...v.products] : []
- products.splice(index, 1)
- return ({ ...v, products })
- })
- }
+ valueHandler((v) => {
+ const products = v.products ? [...v.products] : [];
+ products.splice(index, 1);
+ return { ...v, products };
+ });
+ };
- const [editingProduct, setEditingProduct] = useState<MerchantBackend.Product
| undefined>(undefined)
+ const [editingProduct, setEditingProduct] = useState<
+ MerchantBackend.Product | undefined
+ >(undefined);
const totalPriceInventory = inventoryList.reduce((prev, cur) => {
- const p = Amounts.parseOrThrow(cur.product.price)
- return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
- }, zero)
+ const p = Amounts.parseOrThrow(cur.product.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
const totalPriceProducts = productList.reduce((prev, cur) => {
- if (!cur.price) return zero
- const p = Amounts.parseOrThrow(cur.price)
- return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
- }, zero)
+ if (!cur.price) return zero;
+ const p = Amounts.parseOrThrow(cur.price);
+ return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
+ }, zero);
- const hasProducts = inventoryList.length > 0 || productList.length > 0
- const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts)
+ const hasProducts = inventoryList.length > 0 || productList.length > 0;
+ const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts);
const totalAsString = Amounts.stringify(totalPrice.amount);
- const allProducts = productList.concat(inventoryList.map(asProduct))
+ const allProducts = productList.concat(inventoryList.map(asProduct));
useEffect(() => {
- valueHandler(v => {
- return ({
- ...v, pricing: {
+ valueHandler((v) => {
+ return {
+ ...v,
+ pricing: {
...v.pricing,
products_price: hasProducts ? totalAsString : undefined,
order_price: hasProducts ? totalAsString : undefined,
- }
- })
- })
- }, [hasProducts, totalAsString])
-
- const discountOrRise = rate(value.pricing?.order_price ||
`${config.currency}:0`, totalAsString)
-
- return <div>
-
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
-
- {/* // FIXME: translating plural singular */}
- <InputGroup name="inventory_products" label={i18n`Manage products in
order`} alternative={
- allProducts.length > 0 && <p>
- {allProducts.length} products
- with a total price of {totalAsString}.
- </p>
- } tooltip={i18n`Manage list of products in the order.`}>
-
- <InventoryProductForm
- currentProducts={value.inventoryProducts || {}}
- onAddProduct={addProductToTheInventoryList}
- inventory={instanceInventory}
- />
-
- <NonInventoryProductFrom productToEdit={editingProduct}
onAddProduct={(p) => {
- setEditingProduct(undefined)
- return addNewProduct(p)
- }} />
-
- {allProducts.length > 0 &&
- <ProductList list={allProducts}
- actions={[{
- name: i18n`Remove`,
- tooltip: i18n`Remove this product from the order.`,
- handler: (e, index) => {
- if (e.product_id) {
- removeProductFromTheInventoryList(e.product_id)
- } else {
- removeFromNewProduct(index);
- setEditingProduct(e);
- }
- }
- }]}
+ },
+ };
+ });
+ }, [hasProducts, totalAsString]);
+
+ const discountOrRise = rate(
+ value.pricing?.order_price || `${config.currency}:0`,
+ totalAsString
+ );
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {/* // FIXME: translating plural singular */}
+ <InputGroup
+ name="inventory_products"
+ label={i18n`Manage products in order`}
+ alternative={
+ allProducts.length > 0 && (
+ <p>
+ {allProducts.length} products with a total price of{" "}
+ {totalAsString}.
+ </p>
+ )
+ }
+ tooltip={i18n`Manage list of products in the order.`}
+ >
+ <InventoryProductForm
+ currentProducts={value.inventoryProducts || {}}
+ onAddProduct={addProductToTheInventoryList}
+ inventory={instanceInventory}
/>
- }
- </InputGroup>
-
- <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler as any}>
- {hasProducts ?
- <Fragment>
- <InputCurrency name="pricing.products_price" label={i18n`Total
price`} readonly tooltip={i18n`total product price added up`} />
- <InputCurrency name="pricing.order_price"
- label={i18n`Total price`}
- addonAfter={discountOrRise > 0 && (discountOrRise < 1 ?
- `discount of %${Math.round((1 - discountOrRise) * 100)}` :
- `rise of %${Math.round((discountOrRise - 1) * 100)}`)
- }
- tooltip={i18n`Amount to be paid by the customer`}
- />
- </Fragment> :
- <InputCurrency name="pricing.order_price" label={i18n`Order
price`} tooltip={i18n`final order price`} />
- }
- <Input name="pricing.summary" inputType="multiline"
label={i18n`Summary`} tooltip={i18n`Title of the order to be shown to the
customer`} />
+ <NonInventoryProductFrom
+ productToEdit={editingProduct}
+ onAddProduct={(p) => {
+ setEditingProduct(undefined);
+ return addNewProduct(p);
+ }}
+ />
- <InputGroup name="shipping" label={i18n`Shipping and Fulfillment`}
initialActive >
- <InputDate name="shipping.delivery_date" label={i18n`Delivery
date`} tooltip={i18n`Deadline for physical delivery assured by the merchant.`}
/>
- {value.shipping?.delivery_date &&
- <InputGroup name="shipping.delivery_location"
label={i18n`Location`} tooltip={i18n`address where the products will be
delivered`} >
- <InputLocation name="shipping.delivery_location" />
- </InputGroup>
- }
- <Input name="shipping.fullfilment_url" label={i18n`Fulfillment
URL`} tooltip={i18n`URL to which the user will be redirected after successful
payment.`} />
+ {allProducts.length > 0 && (
+ <ProductList
+ list={allProducts}
+ actions={[
+ {
+ name: i18n`Remove`,
+ tooltip: i18n`Remove this product from the order.`,
+ handler: (e, index) => {
+ if (e.product_id) {
+ removeProductFromTheInventoryList(e.product_id);
+ } else {
+ removeFromNewProduct(index);
+ setEditingProduct(e);
+ }
+ },
+ },
+ ]}
+ />
+ )}
</InputGroup>
- <InputGroup name="payments" label={i18n`Taler payment options`}
tooltip={i18n`Override default Taler payment settings for this order`}>
- <InputDate name="payments.pay_deadline" label={i18n`Payment
deadline`} tooltip={i18n`Deadline for the customer to pay for the offer before
it expires. Inventory products will be reserved until this deadline.`} />
- <InputDate name="payments.refund_deadline" label={i18n`Refund
deadline`} tooltip={i18n`Time until which the order can be refunded by the
merchant.`} />
- <InputDate name="payments.auto_refund_deadline"
label={i18n`Auto-refund deadline`} tooltip={i18n`Time until which the wallet
will automatically check for refunds without user interaction.`} />
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ {hasProducts ? (
+ <Fragment>
+ <InputCurrency
+ name="pricing.products_price"
+ label={i18n`Total price`}
+ readonly
+ tooltip={i18n`total product price added up`}
+ />
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n`Total price`}
+ addonAfter={
+ discountOrRise > 0 &&
+ (discountOrRise < 1
+ ? `discount of %${Math.round(
+ (1 - discountOrRise) * 100
+ )}`
+ : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
+ }
+ tooltip={i18n`Amount to be paid by the customer`}
+ />
+ </Fragment>
+ ) : (
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n`Order price`}
+ tooltip={i18n`final order price`}
+ />
+ )}
- <InputCurrency name="payments.max_fee" label={i18n`Maximum
deposit fee`} tooltip={i18n`Maximum deposit fees the merchant is willing to
cover for this order. Higher deposit fees must be covered in full by the
consumer.`} />
- <InputCurrency name="payments.max_wire_fee" label={i18n`Maximum
wire fee`} tooltip={i18n`Maximum aggregate wire fees the merchant is willing to
cover for this order. Wire fees exceeding this amount are to be covered by the
customers.`} />
- <InputNumber name="payments.wire_fee_amortization"
label={i18n`Wire fee amortization`} tooltip={i18n`Factor by which wire fees
exceeding the above threshold are divided to determine the share of excess wire
fees to be paid explicitly by the consumer.`} />
- </InputGroup>
+ <Input
+ name="pricing.summary"
+ inputType="multiline"
+ label={i18n`Summary`}
+ tooltip={i18n`Title of the order to be shown to the customer`}
+ />
- <InputGroup name="extra" label={i18n`Additional information`}
tooltip={i18n`Custom information to be included in the contract for this
order.`}>
- <Input name="extra" inputType="multiline" label={`Value`}
tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} />
- </InputGroup>
- </FormProvider>
+ <InputGroup
+ name="shipping"
+ label={i18n`Shipping and Fulfillment`}
+ initialActive
+ >
+ <InputDate
+ name="shipping.delivery_date"
+ label={i18n`Delivery date`}
+ tooltip={i18n`Deadline for physical delivery assured by the
merchant.`}
+ />
+ {value.shipping?.delivery_date && (
+ <InputGroup
+ name="shipping.delivery_location"
+ label={i18n`Location`}
+ tooltip={i18n`address where the products will be
delivered`}
+ >
+ <InputLocation name="shipping.delivery_location" />
+ </InputGroup>
+ )}
+ <Input
+ name="shipping.fullfilment_url"
+ label={i18n`Fulfillment URL`}
+ tooltip={i18n`URL to which the user will be redirected after
successful payment.`}
+ />
+ </InputGroup>
+
+ <InputGroup
+ name="payments"
+ label={i18n`Taler payment options`}
+ tooltip={i18n`Override default Taler payment settings for this
order`}
+ >
+ <InputDate
+ name="payments.pay_deadline"
+ label={i18n`Payment deadline`}
+ tooltip={i18n`Deadline for the customer to pay for the offer
before it expires. Inventory products will be reserved until this deadline.`}
+ />
+ <InputDate
+ name="payments.refund_deadline"
+ label={i18n`Refund deadline`}
+ tooltip={i18n`Time until which the order can be refunded by
the merchant.`}
+ />
+ <InputDate
+ name="payments.wire_transfer_deadline"
+ label={i18n`Wire transfer deadline`}
+ tooltip={i18n`Deadline for the exchange to make the wire
transfer.`}
+ />
+ <InputDate
+ name="payments.auto_refund_deadline"
+ label={i18n`Auto-refund deadline`}
+ tooltip={i18n`Time until which the wallet will automatically
check for refunds without user interaction.`}
+ />
- <div class="buttons is-right mt-5">
- {onBack && <button class="button" onClick={onBack}
><Translate>Cancel</Translate></button>}
- <button class="button is-success" onClick={submit}
disabled={hasErrors} ><Translate>Confirm</Translate></button>
+ <InputCurrency
+ name="payments.max_fee"
+ label={i18n`Maximum deposit fee`}
+ tooltip={i18n`Maximum deposit fees the merchant is willing
to cover for this order. Higher deposit fees must be covered in full by the
consumer.`}
+ />
+ <InputCurrency
+ name="payments.max_wire_fee"
+ label={i18n`Maximum wire fee`}
+ tooltip={i18n`Maximum aggregate wire fees the merchant is
willing to cover for this order. Wire fees exceeding this amount are to be
covered by the customers.`}
+ />
+ <InputNumber
+ name="payments.wire_fee_amortization"
+ label={i18n`Wire fee amortization`}
+ tooltip={i18n`Factor by which wire fees exceeding the above
threshold are divided to determine the share of excess wire fees to be paid
explicitly by the consumer.`}
+ />
+ <InputBoolean
+ name="payments.createToken"
+ label={i18n`Create token`}
+ tooltip={i18n`Uncheck this option if the merchant backend
generated an order ID with enough entropy to prevent adversarial claims.`}
+ />
+ </InputGroup>
+
+ <InputGroup
+ name="extra"
+ label={i18n`Additional information`}
+ tooltip={i18n`Custom information to be included in the
contract for this order.`}
+ >
+ <Input
+ name="extra"
+ inputType="multiline"
+ label={`Value`}
+ tooltip={i18n`You must enter a value in JavaScript Object
Notation (JSON).`}
+ />
+ </InputGroup>
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <Translate>Cancel</Translate>
+ </button>
+ )}
+ <button
+ class="button is-success"
+ onClick={submit}
+ disabled={hasErrors}
+ >
+ <Translate>Confirm</Translate>
+ </button>
+ </div>
</div>
-
+ <div class="column" />
</div>
- <div class="column" />
- </div>
- </section>
-
- </div>
+ </section>
+ </div>
+ );
}
function asProduct(p: ProductAndQuantity) {
- return ({
+ return {
product_id: p.product.id,
image: p.product.image,
price: p.product.price,
unit: p.product.unit,
quantity: p.quantity,
description: p.product.description,
- taxes: p.product.taxes
- })
+ taxes: p.product.taxes,
+ };
}
diff --git
a/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
b/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
index 0fa96ed..741edc6 100644
--- a/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
@@ -15,191 +15,245 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import * as yup from 'yup';
+import * as yup from "yup";
import { AsyncButton } from "../../../components/exception/AsyncButton";
-import { FormProvider, FormErrors } from
"../../../components/form/FormProvider";
+import {
+ FormProvider,
+ FormErrors,
+} from "../../../components/form/FormProvider";
import { UpdateTokenModal } from "../../../components/modal";
import { useInstanceContext } from "../../../context/instance";
import { MerchantBackend } from "../../../declaration";
import { Translate, useTranslator } from "../../../i18n";
-import { InstanceUpdateSchema as schema } from '../../../schemas';
+import { InstanceUpdateSchema as schema } from "../../../schemas";
import { DefaultInstanceFormFields } from
"../../../components/instance/DefaultInstanceFormFields";
import { PAYTO_REGEX } from "../../../utils/constants";
import { Amounts } from "@gnu-taler/taler-util";
-
-type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
- auth_token?: string
-}
+type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
+ auth_token?: string;
+};
//MerchantBackend.Instances.InstanceAuthConfigurationMessage
interface Props {
onUpdate: (d: Entity) => void;
- onChangeAuth: (d:
MerchantBackend.Instances.InstanceAuthConfigurationMessage) => Promise<void>;
+ onChangeAuth: (
+ d: MerchantBackend.Instances.InstanceAuthConfigurationMessage
+ ) => Promise<void>;
selected: MerchantBackend.Instances.QueryInstancesResponse;
isLoading: boolean;
onBack: () => void;
}
-function convert(from: MerchantBackend.Instances.QueryInstancesResponse):
Entity {
- const { accounts, ...rest } = from
- const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri)
+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 defaults = {
default_wire_fee_amortization: 1,
default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour
default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours
- }
+ };
return { ...defaults, ...rest, payto_uris };
}
function getTokenValuePart(t?: string): string | undefined {
- if (!t) return t
+ if (!t) return t;
const match = /secret-token:(.*)/.exec(t);
if (!match || !match[1]) return undefined;
- return match[1]
+ return match[1];
}
function undefinedIfEmpty<T>(obj: T): T | undefined {
- return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj :
undefined
+ return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ ? obj
+ : undefined;
}
-export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }:
Props): VNode {
- const { id, token } = useInstanceContext()
- const currentTokenValue = getTokenValuePart(token)
+export function UpdatePage({
+ onUpdate,
+ onChangeAuth,
+ selected,
+ onBack,
+}: Props): VNode {
+ const { id, token } = useInstanceContext();
+ const currentTokenValue = getTokenValuePart(token);
function updateToken(token: string | undefined | null) {
- const value = token && token.startsWith('secret-token:') ?
- token.substring('secret-token:'.length) : token
+ const value =
+ token && token.startsWith("secret-token:")
+ ? token.substring("secret-token:".length)
+ : token;
if (!token) {
- onChangeAuth({ method: 'external' })
+ onChangeAuth({ method: "external" });
} else {
- onChangeAuth({ method: 'token', token: `secret-token:${value}` })
+ onChangeAuth({ method: "token", token: `secret-token:${value}` });
}
}
- const [value, valueHandler] = useState<Partial<Entity>>(convert(selected))
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
- const i18n = useTranslator()
+ const i18n = useTranslator();
const errors: FormErrors<Entity> = {
name: !value.name ? i18n`required` : undefined,
payto_uris:
- !value.payto_uris || !value.payto_uris.length ? i18n`required` : (
- undefinedIfEmpty(value.payto_uris.map(p => {
- return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined
- }))
- ),
- default_max_deposit_fee:
- !value.default_max_deposit_fee ? i18n`required` : (
- !Amounts.parse(value.default_max_deposit_fee) ? i18n`invalid format` :
- undefined
- ),
- default_max_wire_fee:
- !value.default_max_wire_fee ? i18n`required` : (
- !Amounts.parse(value.default_max_wire_fee) ? i18n`invalid format` :
- undefined
- ),
+ !value.payto_uris || !value.payto_uris.length
+ ? i18n`required`
+ : undefinedIfEmpty(
+ value.payto_uris.map((p) => {
+ return !PAYTO_REGEX.test(p) ? i18n`is not valid` : undefined;
+ })
+ ),
+ default_max_deposit_fee: !value.default_max_deposit_fee
+ ? i18n`required`
+ : !Amounts.parse(value.default_max_deposit_fee)
+ ? i18n`invalid format`
+ : undefined,
+ default_max_wire_fee: !value.default_max_wire_fee
+ ? i18n`required`
+ : !Amounts.parse(value.default_max_wire_fee)
+ ? i18n`invalid format`
+ : undefined,
default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined ? i18n`required` : (
- isNaN(value.default_wire_fee_amortization) ? i18n`is not a number` : (
- value.default_wire_fee_amortization < 1 ? i18n`must be 1 or greater`
:
- undefined
- )
- ),
- default_pay_delay:
- !value.default_pay_delay ? i18n`required` : undefined,
- default_wire_transfer_delay:
- !value.default_wire_transfer_delay ? i18n`required` : undefined,
+ value.default_wire_fee_amortization === undefined
+ ? i18n`required`
+ : isNaN(value.default_wire_fee_amortization)
+ ? i18n`is not a number`
+ : value.default_wire_fee_amortization < 1
+ ? i18n`must be 1 or greater`
+ : undefined,
+ default_pay_delay: !value.default_pay_delay ? i18n`required` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n`required`
+ : undefined,
address: undefinedIfEmpty({
address_lines:
- value.address?.address_lines && value.address?.address_lines.length >
7 ? i18n`max 7 lines` :
- undefined
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n`max 7 lines`
+ : undefined,
}),
jurisdiction: undefinedIfEmpty({
- address_lines: value.address?.address_lines &&
value.address?.address_lines.length > 7 ? i18n`max 7 lines` :
- undefined
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n`max 7 lines`
+ : undefined,
}),
};
- const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !==
undefined)
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
const submit = async (): Promise<void> => {
- await onUpdate(schema.cast(value));
- await onBack()
- return Promise.resolve()
- }
+ await onUpdate(value as Entity);
+ };
const [active, setActive] = useState(false);
- return <div>
- <section class="section">
-
- <section class="hero is-hero-bar">
- <div class="hero-body">
-
- <div class="level">
- <div class="level-left">
- <div class="level-item">
- <span class="is-size-4"><Translate>Instance id</Translate>:
<b>{id}</b></span>
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ <Translate>Instance id</Translate>: <b>{id}</b>
+ </span>
+ </div>
</div>
- </div>
- <div class="level-right">
- <div class="level-item">
- <h1 class="title">
- <button class="button is-danger"
- data-tooltip={i18n`Change the authorization method use for
this instance.`}
- onClick={(): void => { setActive(!active); }} >
- <div class="icon is-left"><i class="mdi mdi-lock-reset"
/></div>
- <span><Translate>Manage access token</Translate></span>
- </button>
- </h1>
+ <div class="level-right">
+ <div class="level-item">
+ <h1 class="title">
+ <button
+ class="button is-danger"
+ data-tooltip={i18n`Change the authorization method use
for this instance.`}
+ onClick={(): void => {
+ setActive(!active);
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <Translate>Manage access token</Translate>
+ </span>
+ </button>
+ </h1>
+ </div>
</div>
</div>
</div>
- </div></section>
-
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- {active && <UpdateTokenModal oldToken={currentTokenValue}
- onCancel={() => { setActive(false); }}
- onClear={() => { updateToken(null); setActive(false); }}
- onConfirm={(newToken) => {
- updateToken(newToken); setActive(false)
- }}
- />}
- </div>
- <div class="column" />
- </div>
- <hr />
-
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <FormProvider<Entity> errors={errors} object={value}
valueHandler={valueHandler} >
-
- <DefaultInstanceFormFields showId={false} />
-
- </FormProvider>
+ </section>
- <div class="buttons is-right mt-4">
- <button class="button" onClick={onBack} data-tooltip="cancel
operation"><Translate>Cancel</Translate></button>
-
- <AsyncButton onClick={submit} data-tooltip={
- hasErrors ? i18n`Need to complete marked fields` : 'confirm
operation'
- } disabled={hasErrors}
><Translate>Confirm</Translate></AsyncButton>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {active && (
+ <UpdateTokenModal
+ oldToken={currentTokenValue}
+ onCancel={() => {
+ setActive(false);
+ }}
+ onClear={() => {
+ updateToken(null);
+ setActive(false);
+ }}
+ onConfirm={(newToken) => {
+ updateToken(newToken);
+ setActive(false);
+ }}
+ />
+ )}
</div>
+ <div class="column" />
</div>
- <div class="column" />
- </div>
+ <hr />
- </section>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields showId={false} />
+ </FormProvider>
- </div >
+ <div class="buttons is-right mt-4">
+ <button
+ class="button"
+ onClick={onBack}
+ data-tooltip="cancel operation"
+ >
+ <Translate>Cancel</Translate>
+ </button>
+ <AsyncButton
+ onClick={submit}
+ data-tooltip={
+ hasErrors
+ ? i18n`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <Translate>Confirm</Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
}
diff --git a/packages/merchant-backoffice/src/paths/instance/update/index.tsx
b/packages/merchant-backoffice/src/paths/instance/update/index.tsx
index 33dc476..bd5f4c7 100644
--- a/packages/merchant-backoffice/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice/src/paths/instance/update/index.tsx
@@ -14,11 +14,20 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
import { Loading } from "../../../components/exception/loading";
+import { NotificationCard } from "../../../components/menu";
import { useInstanceContext } from "../../../context/instance";
import { MerchantBackend } from "../../../declaration";
import { HttpError, HttpResponse } from "../../../hooks/backend";
-import { useInstanceAPI, useInstanceDetails, useManagedInstanceDetails,
useManagementAPI } from "../../../hooks/instance";
+import {
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagedInstanceDetails,
+ useManagementAPI,
+} from "../../../hooks/instance";
+import { useTranslator } from "../../../i18n";
+import { Notification } from "../../../utils/types";
import { UpdatePage } from "./UpdatePage";
export interface Props {
@@ -29,41 +38,76 @@ export interface Props {
onNotFound: () => VNode;
onLoadError: (e: HttpError) => VNode;
onUpdateError: (e: HttpError) => void;
-
}
export default function Update(props: Props): VNode {
const { updateInstance, clearToken, setNewToken } = useInstanceAPI();
- const result = useInstanceDetails()
- return CommonUpdate(props, result, updateInstance, clearToken, setNewToken)
+ const result = useInstanceDetails();
+ return CommonUpdate(props, result, updateInstance, clearToken, setNewToken);
}
-export function AdminUpdate(props:Props & {instanceId:string}): VNode {
- const { updateInstance, clearToken, setNewToken } =
useManagementAPI(props.instanceId);
- const result = useManagedInstanceDetails(props.instanceId)
- return CommonUpdate(props, result, updateInstance, clearToken, setNewToken)
+export function AdminUpdate(props: Props & { instanceId: string }): VNode {
+ const { updateInstance, clearToken, setNewToken } = useManagementAPI(
+ props.instanceId
+ );
+ const result = useManagedInstanceDetails(props.instanceId);
+ return CommonUpdate(props, result, updateInstance, clearToken, setNewToken);
}
-function CommonUpdate({ onBack, onConfirm, onLoadError, onNotFound,
onUpdateError, onUnauthorized }: Props, result:
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse>, updateInstance:
any, clearToken: any, setNewToken: any): VNode {
- const { changeToken } = useInstanceContext()
+function CommonUpdate(
+ {
+ onBack,
+ onConfirm,
+ onLoadError,
+ onNotFound,
+ onUpdateError,
+ onUnauthorized,
+ }: Props,
+ result: HttpResponse<MerchantBackend.Instances.QueryInstancesResponse>,
+ updateInstance: any,
+ clearToken: any,
+ setNewToken: any
+): VNode {
+ const { changeToken } = useInstanceContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const i18n = useTranslator();
- if (result.clientError && result.isUnauthorized) return onUnauthorized()
- if (result.clientError && result.isNotfound) return onNotFound()
- if (result.loading) return <Loading />
- if (!result.ok) return onLoadError(result)
+ if (result.clientError && result.isUnauthorized) return onUnauthorized();
+ if (result.clientError && result.isNotfound) return onNotFound();
+ if (result.loading) return <Loading />;
+ if (!result.ok) return onLoadError(result);
- return <Fragment>
- <UpdatePage
- onBack={onBack}
- isLoading={false}
- selected={result.data}
- onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage):
Promise<void> => {
- return updateInstance(d).then(onConfirm).catch(onUpdateError)
- }}
- onChangeAuth={(d:
MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => {
- const apiCall = d.method === 'external' ? clearToken() :
setNewToken(d.token!);
- return apiCall.then(() =>
changeToken(d.token)).then(onConfirm).catch(onUpdateError)
- }}
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ onBack={onBack}
+ isLoading={false}
+ selected={result.data}
+ onUpdate={(
+ d: MerchantBackend.Instances.InstanceReconfigurationMessage
+ ): Promise<void> => {
+ return updateInstance(d)
+ .then(onConfirm)
+ .catch((error: Error) =>
+ setNotif({
+ message: i18n`Failed to create instance`,
+ type: "ERROR",
+ description: error.message,
+ })
+ );
+ }}
+ onChangeAuth={(
+ d: MerchantBackend.Instances.InstanceAuthConfigurationMessage
+ ): Promise<void> => {
+ const apiCall =
+ d.method === "external" ? clearToken() : setNewToken(d.token!);
+ return apiCall
+ .then(() => changeToken(d.token))
+ .then(onConfirm)
+ .catch(onUpdateError);
+ }}
/>
- </Fragment>
-}
\ No newline at end of file
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice/src/schemas/index.ts
b/packages/merchant-backoffice/src/schemas/index.ts
index 1c58016..9d64a67 100644
--- a/packages/merchant-backoffice/src/schemas/index.ts
+++ b/packages/merchant-backoffice/src/schemas/index.ts
@@ -98,10 +98,10 @@ export const InstanceSchema = yup.object().shape({
district: yup.string().optional(),
country_subdivision: yup.string().optional(),
}).meta({ type: 'group' }),
- default_pay_delay: yup.object()
- .shape({ d_ms: yup.number() })
- .required()
- .meta({ type: 'duration' }),
+ // default_pay_delay: yup.object()
+ // .shape({ d_ms: yup.number() })
+ // .required()
+ // .meta({ type: 'duration' }),
// .transform(numberToDuration),
default_wire_transfer_delay: yup.object()
.shape({ d_ms: yup.number() })
diff --git a/packages/merchant-backoffice/src/utils/constants.ts
b/packages/merchant-backoffice/src/utils/constants.ts
index 6f76a71..5356a1a 100644
--- a/packages/merchant-backoffice/src/utils/constants.ts
+++ b/packages/merchant-backoffice/src/utils/constants.ts
@@ -45,3 +45,150 @@ export const DEFAULT_REQUEST_TIMEOUT = 10;
export const MAX_IMAGE_SIZE = 1024 * 1024;
export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe"
+}
+
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-merchant-backoffice] branch master updated (4f796dd -> 54723c9),
gnunet <=