[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 02/02: fixing issue taken from christian com
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 02/02: fixing issue taken from christian comments: |
Date: |
Tue, 30 Nov 2021 20:21:49 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
commit 54723c9d6cec1b46e577222a2f66b37c85ef10ac
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Nov 30 16:19:38 2021 -0300
fixing issue taken from christian comments:
* iban validaton client side
* iban account should be CAP
* added claim token option when creating an order
* added wire transfer deadline, was missing
* default wire transfer delay is now optional
* refund deadline is now optional
* fix update instance: do not redirect when there is a server error, stay
in the same view and display the server error
---
.../src/components/form/InputPaytoForm.tsx | 87 +++++++++---
.../instance/DefaultInstanceFormFields.tsx | 5 +-
packages/merchant-backoffice/src/i18n/index.tsx | 6 +-
.../paths/instance/orders/create/CreatePage.tsx | 39 +++++-
.../src/paths/instance/update/UpdatePage.tsx | 4 +-
.../src/paths/instance/update/index.tsx | 17 ++-
packages/merchant-backoffice/src/schemas/index.ts | 8 +-
.../merchant-backoffice/src/utils/constants.ts | 147 +++++++++++++++++++++
8 files changed, 282 insertions(+), 31 deletions(-)
diff --git
a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
b/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
index 02436a3..73cf751 100644
--- a/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice/src/components/form/InputPaytoForm.tsx
@@ -20,7 +20,8 @@
*/
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,12 +34,13 @@ export interface Props<T> extends InputProps<T> {
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
+ // iban, bitcoin, x-taler-bank. it defined the format
target: string;
- path: string;
+ // path1 if the first field to be used
path1: string;
- path2: string;
- host: string;
- account: string;
+ // path2 if the second field to be used, optional
+ path2?: string;
+ // options of the payto uri
options: {
"receiver-name"?: string;
sender?: string;
@@ -49,6 +51,61 @@ type Entity = {
};
};
+/**
+ * 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: {} };
@@ -69,16 +126,19 @@ export function InputPaytoForm<T>({
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 url = new URL(`payto://${value.target}${value.path}`);
+ const url = new URL(`payto://${value.target}${payToPath}`);
const ops = value.options!;
Object.keys(ops).forEach((opt_key) => {
const opt_value = ops[opt_key];
@@ -91,11 +151,7 @@ export function InputPaytoForm<T>({
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
+ ? validateIBAN(value.path1, i18n)
: undefined,
path2:
value.target === "x-taler-bank"
@@ -168,6 +224,7 @@ export function InputPaytoForm<T>({
name="path1"
label={i18n`Account`}
tooltip={i18n`Bank Account Number.`}
+ inputExtra={{ style: { textTransform: "uppercase" } }}
/>
</Fragment>
)}
@@ -198,7 +255,7 @@ export function InputPaytoForm<T>({
/>
</Fragment>
)}
- {value.target === "void" && <Fragment></Fragment>}
+ {value.target === "void" && <Fragment />}
{value.target === "x-taler-bank" && (
<Fragment>
<Input<Entity>
diff --git
a/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
b/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
index 5a8e1d1..8c59a6d 100644
---
a/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
+++
b/packages/merchant-backoffice/src/components/instance/DefaultInstanceFormFields.tsx
@@ -19,7 +19,7 @@
* @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";
@@ -37,7 +37,7 @@ export function DefaultInstanceFormFields({
}: {
readonlyId?: boolean;
showId: boolean;
-}) {
+}): VNode {
const i18n = useTranslator();
const backend = useBackendContext();
return (
@@ -109,6 +109,7 @@ export function DefaultInstanceFormFields({
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 f89ab31..9403de1 100644
--- a/packages/merchant-backoffice/src/i18n/index.tsx
+++ b/packages/merchant-backoffice/src/i18n/index.tsx
@@ -25,7 +25,11 @@ 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(
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 4a5af6b..580ead1 100644
---
a/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
+++
b/packages/merchant-backoffice/src/paths/instance/orders/create/CreatePage.tsx
@@ -41,6 +41,7 @@ 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;
@@ -71,6 +72,7 @@ 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: "",
@@ -98,10 +100,12 @@ 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;
@@ -157,17 +161,29 @@ export function CreatePage({
: undefined,
payments: undefinedIfEmpty({
refund_deadline: !value.payments?.refund_deadline
- ? i18n`required`
+ ? 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`pay deadline cannot be before refund 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
- ? i18n`required`
+ ? 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
@@ -211,10 +227,12 @@ export function CreatePage({
Math.floor(value.payments.pay_deadline.getTime() / 1000) *
1000,
}
: undefined,
- wire_transfer_deadline: value.payments.pay_deadline
+ wire_transfer_deadline: value.payments.wire_transfer_deadline
? {
t_ms:
- Math.floor(value.payments.pay_deadline.getTime() / 1000) *
1000,
+ Math.floor(
+ value.payments.wire_transfer_deadline.getTime() / 1000
+ ) * 1000,
}
: undefined,
refund_deadline: value.payments.refund_deadline
@@ -238,6 +256,7 @@ export function CreatePage({
product_id: p.product.id,
quantity: p.quantity,
})),
+ create_token: value.payments.createToken,
};
onCreate(request);
@@ -454,6 +473,11 @@ export function CreatePage({
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`}
@@ -475,6 +499,11 @@ export function CreatePage({
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
diff --git
a/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
b/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
index 863f977..741edc6 100644
--- a/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice/src/paths/instance/update/UpdatePage.tsx
@@ -153,9 +153,7 @@ export function UpdatePage({
(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);
diff --git a/packages/merchant-backoffice/src/paths/instance/update/index.tsx
b/packages/merchant-backoffice/src/paths/instance/update/index.tsx
index 38a652d..bd5f4c7 100644
--- a/packages/merchant-backoffice/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice/src/paths/instance/update/index.tsx
@@ -14,7 +14,9 @@
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";
@@ -24,6 +26,8 @@ import {
useManagedInstanceDetails,
useManagementAPI,
} from "../../../hooks/instance";
+import { useTranslator } from "../../../i18n";
+import { Notification } from "../../../utils/types";
import { UpdatePage } from "./UpdatePage";
export interface Props {
@@ -65,6 +69,8 @@ function CommonUpdate(
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();
@@ -73,6 +79,7 @@ function CommonUpdate(
return (
<Fragment>
+ <NotificationCard notification={notif} />
<UpdatePage
onBack={onBack}
isLoading={false}
@@ -80,7 +87,15 @@ function CommonUpdate(
onUpdate={(
d: MerchantBackend.Instances.InstanceReconfigurationMessage
): Promise<void> => {
- return updateInstance(d).then(onConfirm).catch(onUpdateError);
+ 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
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.