validate IBAN, removing internal iban from account form, add missing logo, do not save backend URL in login state
This commit is contained in:
parent
906eddd48a
commit
4de014927e
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
|
[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
|
||||||
|
|
||||||
for file in index.html index.js index.css; do
|
for file in index.html index.js index.css logo-white-U55BSKA2.svg; do
|
||||||
cp packages/demobank-ui/dist/$file prebuilt/demobank/
|
cp packages/demobank-ui/dist/$file prebuilt/demobank/
|
||||||
done
|
done
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ By default, the demobank-ui points to `https://bank.demo.taler.net/demobanks/def
|
|||||||
as the bank access API base URL.
|
as the bank access API base URL.
|
||||||
|
|
||||||
This can be changed for testing by setting the URL via local storage (via your browser's devtools):
|
This can be changed for testing by setting the URL via local storage (via your browser's devtools):
|
||||||
|
|
||||||
```
|
```
|
||||||
localStorage.setItem("bank-base-url", OTHER_URL);
|
localStorage.setItem("bank-base-url", OTHER_URL);
|
||||||
```
|
```
|
||||||
@ -35,6 +36,7 @@ to the default settings:
|
|||||||
|
|
||||||
```
|
```
|
||||||
globalThis.talerDemobankSettings = {
|
globalThis.talerDemobankSettings = {
|
||||||
|
backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
|
||||||
allowRegistrations: true,
|
allowRegistrations: true,
|
||||||
bankName: "Taler Bank",
|
bankName: "Taler Bank",
|
||||||
// Show explainer text and navbar to other demo sites
|
// Show explainer text and navbar to other demo sites
|
||||||
|
@ -42,26 +42,23 @@ export interface BackendCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface LoggedIn extends BackendCredentials {
|
interface LoggedIn extends BackendCredentials {
|
||||||
url: string;
|
|
||||||
status: "loggedIn";
|
status: "loggedIn";
|
||||||
isUserAdministrator: boolean;
|
isUserAdministrator: boolean;
|
||||||
}
|
}
|
||||||
interface LoggedOut {
|
interface LoggedOut {
|
||||||
url: string;
|
|
||||||
status: "loggedOut";
|
status: "loggedOut";
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeRootPath = bankUiSettings.backendBaseURL;
|
|
||||||
|
|
||||||
export function getInitialBackendBaseURL(): string {
|
export function getInitialBackendBaseURL(): string {
|
||||||
const overrideUrl = localStorage.getItem("bank-base-url");
|
const overrideUrl = localStorage.getItem("bank-base-url");
|
||||||
|
|
||||||
return canonicalizeBaseUrl(overrideUrl ? overrideUrl : maybeRootPath);
|
return canonicalizeBaseUrl(
|
||||||
|
overrideUrl ? overrideUrl : bankUiSettings.backendBaseURL,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultState: BackendState = {
|
export const defaultState: BackendState = {
|
||||||
status: "loggedOut",
|
status: "loggedOut",
|
||||||
url: getInitialBackendBaseURL(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BackendStateHandler {
|
export interface BackendStateHandler {
|
||||||
@ -91,13 +88,12 @@ export function useBackendState(): BackendStateHandler {
|
|||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
logOut() {
|
logOut() {
|
||||||
update(JSON.stringify({ ...defaultState, url: state.url }));
|
update(JSON.stringify({ ...defaultState }));
|
||||||
},
|
},
|
||||||
logIn(info) {
|
logIn(info) {
|
||||||
//admin is defined by the username
|
//admin is defined by the username
|
||||||
const nextState: BackendState = {
|
const nextState: BackendState = {
|
||||||
status: "loggedIn",
|
status: "loggedIn",
|
||||||
url: state.url,
|
|
||||||
...info,
|
...info,
|
||||||
isUserAdministrator: info.username === "admin",
|
isUserAdministrator: info.username === "admin",
|
||||||
};
|
};
|
||||||
@ -125,7 +121,7 @@ export function usePublicBackend(): useBackendType {
|
|||||||
const { state } = useBackendContext();
|
const { state } = useBackendContext();
|
||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
|
||||||
const baseUrl = state.url;
|
const baseUrl = getInitialBackendBaseURL();
|
||||||
|
|
||||||
const request = useCallback(
|
const request = useCallback(
|
||||||
function requestImpl<T>(
|
function requestImpl<T>(
|
||||||
@ -201,7 +197,7 @@ export function useAuthenticatedBackend(): useBackendType {
|
|||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
|
||||||
const creds = state.status === "loggedIn" ? state : undefined;
|
const creds = state.status === "loggedIn" ? state : undefined;
|
||||||
const baseUrl = state.url;
|
const baseUrl = getInitialBackendBaseURL();
|
||||||
|
|
||||||
const request = useCallback(
|
const request = useCallback(
|
||||||
function requestImpl<T>(
|
function requestImpl<T>(
|
||||||
|
@ -24,7 +24,11 @@ import {
|
|||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
|
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
|
||||||
import { useAuthenticatedBackend, useMatchMutate } from "./backend.js";
|
import {
|
||||||
|
getInitialBackendBaseURL,
|
||||||
|
useAuthenticatedBackend,
|
||||||
|
useMatchMutate,
|
||||||
|
} from "./backend.js";
|
||||||
|
|
||||||
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
import _useSWR, { SWRHook } from "swr";
|
import _useSWR, { SWRHook } from "swr";
|
||||||
@ -210,10 +214,10 @@ export interface CircuitAccountAPI {
|
|||||||
|
|
||||||
async function getBusinessStatus(
|
async function getBusinessStatus(
|
||||||
request: ReturnType<typeof useApiContext>["request"],
|
request: ReturnType<typeof useApiContext>["request"],
|
||||||
url: string,
|
|
||||||
basicAuth: { username: string; password: string },
|
basicAuth: { username: string; password: string },
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
const url = getInitialBackendBaseURL();
|
||||||
const result = await request<
|
const result = await request<
|
||||||
HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>
|
HttpResponseOk<SandboxBackend.Circuit.CircuitAccountData>
|
||||||
>(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth });
|
>(url, `circuit-api/accounts/${basicAuth.username}`, { basicAuth });
|
||||||
@ -234,7 +238,7 @@ export function useBusinessAccountFlag(): boolean | undefined {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!creds) return;
|
if (!creds) return;
|
||||||
getBusinessStatus(request, state.url, creds)
|
getBusinessStatus(request, creds)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setIsBusiness(result);
|
setIsBusiness(result);
|
||||||
})
|
})
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
PartialButDefined,
|
PartialButDefined,
|
||||||
RecursivePartial,
|
RecursivePartial,
|
||||||
undefinedIfEmpty,
|
undefinedIfEmpty,
|
||||||
|
validateIBAN,
|
||||||
WithIntermediate,
|
WithIntermediate,
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
import { ErrorBannerFloat } from "./BankFrame.js";
|
import { ErrorBannerFloat } from "./BankFrame.js";
|
||||||
@ -230,74 +231,78 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section id="main">
|
<section id="main">
|
||||||
<article>
|
{!customers.length ? (
|
||||||
<h2>{i18n.str`Accounts:`}</h2>
|
<div></div>
|
||||||
<div class="results">
|
) : (
|
||||||
<table class="pure-table pure-table-striped">
|
<article>
|
||||||
<thead>
|
<h2>{i18n.str`Accounts:`}</h2>
|
||||||
<tr>
|
<div class="results">
|
||||||
<th>{i18n.str`Username`}</th>
|
<table class="pure-table pure-table-striped">
|
||||||
<th>{i18n.str`Name`}</th>
|
<thead>
|
||||||
<th></th>
|
<tr>
|
||||||
<th></th>
|
<th>{i18n.str`Username`}</th>
|
||||||
</tr>
|
<th>{i18n.str`Name`}</th>
|
||||||
</thead>
|
<th></th>
|
||||||
<tbody>
|
<th></th>
|
||||||
{customers.map((item, idx) => {
|
</tr>
|
||||||
return (
|
</thead>
|
||||||
<tr key={idx}>
|
<tbody>
|
||||||
<td>
|
{customers.map((item, idx) => {
|
||||||
<a
|
return (
|
||||||
href="#"
|
<tr key={idx}>
|
||||||
onClick={(e) => {
|
<td>
|
||||||
e.preventDefault();
|
<a
|
||||||
setShowDetails(item.username);
|
href="#"
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
{item.username}
|
setShowDetails(item.username);
|
||||||
</a>
|
}}
|
||||||
</td>
|
>
|
||||||
<td>{item.name}</td>
|
{item.username}
|
||||||
<td>
|
</a>
|
||||||
<a
|
</td>
|
||||||
href="#"
|
<td>{item.name}</td>
|
||||||
onClick={(e) => {
|
<td>
|
||||||
e.preventDefault();
|
<a
|
||||||
setUpdatePassword(item.username);
|
href="#"
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
change password
|
setUpdatePassword(item.username);
|
||||||
</a>
|
}}
|
||||||
</td>
|
>
|
||||||
<td>
|
change password
|
||||||
<a
|
</a>
|
||||||
href="#"
|
</td>
|
||||||
onClick={(e) => {
|
<td>
|
||||||
e.preventDefault();
|
<a
|
||||||
setShowCashouts(item.username);
|
href="#"
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
cashouts
|
setShowCashouts(item.username);
|
||||||
</a>
|
}}
|
||||||
</td>
|
>
|
||||||
<td>
|
cashouts
|
||||||
<a
|
</a>
|
||||||
href="#"
|
</td>
|
||||||
onClick={(e) => {
|
<td>
|
||||||
e.preventDefault();
|
<a
|
||||||
setRemoveAccount(item.username);
|
href="#"
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
remove
|
setRemoveAccount(item.username);
|
||||||
</a>
|
}}
|
||||||
</td>
|
>
|
||||||
</tr>
|
remove
|
||||||
);
|
</a>
|
||||||
})}
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
);
|
||||||
</div>
|
})}
|
||||||
</article>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -835,15 +840,15 @@ function AccountForm({
|
|||||||
? i18n.str`only "IBAN" target are supported`
|
? i18n.str`only "IBAN" target are supported`
|
||||||
: !IBAN_REGEX.test(parsed.iban)
|
: !IBAN_REGEX.test(parsed.iban)
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
: undefined,
|
: validateIBAN(parsed.iban, i18n),
|
||||||
contact_data: undefinedIfEmpty({
|
contact_data: undefinedIfEmpty({
|
||||||
email: !newForm.contact_data?.email
|
email: !newForm.contact_data?.email
|
||||||
? undefined
|
? i18n.str`required`
|
||||||
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
||||||
? i18n.str`it should be an email`
|
? i18n.str`it should be an email`
|
||||||
: undefined,
|
: undefined,
|
||||||
phone: !newForm.contact_data?.phone
|
phone: !newForm.contact_data?.phone
|
||||||
? undefined
|
? i18n.str`required`
|
||||||
: !newForm.contact_data.phone.startsWith("+")
|
: !newForm.contact_data.phone.startsWith("+")
|
||||||
? i18n.str`should start with +`
|
? i18n.str`should start with +`
|
||||||
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
||||||
@ -851,10 +856,10 @@ function AccountForm({
|
|||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
iban: !newForm.iban
|
iban: !newForm.iban
|
||||||
? i18n.str`required`
|
? undefined //optional field
|
||||||
: !IBAN_REGEX.test(newForm.iban)
|
: !IBAN_REGEX.test(newForm.iban)
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
: undefined,
|
: validateIBAN(newForm.iban, i18n),
|
||||||
name: !newForm.name ? i18n.str`required` : undefined,
|
name: !newForm.name ? i18n.str`required` : undefined,
|
||||||
username: !newForm.username ? i18n.str`required` : undefined,
|
username: !newForm.username ? i18n.str`required` : undefined,
|
||||||
});
|
});
|
||||||
@ -866,7 +871,10 @@ function AccountForm({
|
|||||||
return (
|
return (
|
||||||
<form class="pure-form">
|
<form class="pure-form">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="username">{i18n.str`Username`}</label>
|
<label for="username">
|
||||||
|
{i18n.str`Username`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
@ -876,14 +884,17 @@ function AccountForm({
|
|||||||
form.username = e.currentTarget.value;
|
form.username = e.currentTarget.value;
|
||||||
updateForm(structuredClone(form));
|
updateForm(structuredClone(form));
|
||||||
}}
|
}}
|
||||||
/>
|
/>{" "}
|
||||||
<ShowInputErrorLabel
|
<ShowInputErrorLabel
|
||||||
message={errors?.username}
|
message={errors?.username}
|
||||||
isDirty={form.username !== undefined}
|
isDirty={form.username !== undefined}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Name`}</label>
|
<label>
|
||||||
|
{i18n.str`Name`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
disabled={purpose !== "create"}
|
disabled={purpose !== "create"}
|
||||||
value={form.name ?? ""}
|
value={form.name ?? ""}
|
||||||
@ -897,23 +908,28 @@ function AccountForm({
|
|||||||
isDirty={form.name !== undefined}
|
isDirty={form.name !== undefined}
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{purpose !== "create" && (
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Internal IBAN`}</label>
|
||||||
|
<input
|
||||||
|
disabled={true}
|
||||||
|
value={form.iban ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.iban = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.iban}
|
||||||
|
isDirty={form.iban !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Internal IBAN`}</label>
|
<label>
|
||||||
<input
|
{i18n.str`Email`}
|
||||||
disabled={purpose !== "create"}
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
value={form.iban ?? ""}
|
</label>
|
||||||
onChange={(e) => {
|
|
||||||
form.iban = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.iban}
|
|
||||||
isDirty={form.iban !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>{i18n.str`Email`}</label>
|
|
||||||
<input
|
<input
|
||||||
disabled={purpose === "show"}
|
disabled={purpose === "show"}
|
||||||
value={form.contact_data.email ?? ""}
|
value={form.contact_data.email ?? ""}
|
||||||
@ -928,7 +944,10 @@ function AccountForm({
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Phone`}</label>
|
<label>
|
||||||
|
{i18n.str`Phone`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
disabled={purpose === "show"}
|
disabled={purpose === "show"}
|
||||||
value={form.contact_data.phone ?? ""}
|
value={form.contact_data.phone ?? ""}
|
||||||
@ -943,12 +962,15 @@ function AccountForm({
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>{i18n.str`Cashout address`}</label>
|
<label>
|
||||||
|
{i18n.str`Cashout address`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
disabled={purpose === "show"}
|
disabled={purpose === "show"}
|
||||||
value={form.cashout_address ?? ""}
|
value={(form.cashout_address ?? "").substring("payto://iban/".length)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
form.cashout_address = e.currentTarget.value;
|
form.cashout_address = "payto://iban/" + e.currentTarget.value;
|
||||||
updateForm(structuredClone(form));
|
updateForm(structuredClone(form));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -161,3 +161,208 @@ export function buildRequestErrorMessage(
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function validateIBAN(
|
||||||
|
iban: string,
|
||||||
|
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
||||||
|
): string | undefined {
|
||||||
|
// Check total length
|
||||||
|
if (iban.length < 4)
|
||||||
|
return i18n.str`IBAN numbers usually have more that 4 digits`;
|
||||||
|
if (iban.length > 34)
|
||||||
|
return i18n.str`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.substring(0, 2);
|
||||||
|
const found = code in COUNTRY_TABLE;
|
||||||
|
if (!found) return i18n.str`IBAN country code not found`;
|
||||||
|
|
||||||
|
// 2.- Move the four initial characters to the end of the string
|
||||||
|
const step2 = IBAN.substring(4) + iban.substring(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("");
|
||||||
|
|
||||||
|
const checksum = calculate_iban_checksum(step3);
|
||||||
|
if (checksum !== 1)
|
||||||
|
return i18n.str`IBAN number is not valid, checksum is wrong`;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate_iban_checksum(str: string): number {
|
||||||
|
const numberStr = str.substring(0, 5);
|
||||||
|
const rest = str.substring(5);
|
||||||
|
const number = parseInt(numberStr, 10);
|
||||||
|
const result = number % 97;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
return calculate_iban_checksum(`${result}${rest}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user