cases, account details and new-form screen
This commit is contained in:
parent
dad7d48ed2
commit
64e3705669
@ -23,39 +23,14 @@ import {
|
|||||||
useMemoryStorage,
|
useMemoryStorage,
|
||||||
useNotifications,
|
useNotifications,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import {
|
||||||
/**
|
AbsoluteTime,
|
||||||
* references between forms
|
Codec,
|
||||||
*
|
buildCodecForObject,
|
||||||
* 902.1e
|
codecForAbsoluteTime,
|
||||||
* --> 902.11 (operational legal entity or partnership)
|
codecForString,
|
||||||
* --> 902.12 (a foundation)
|
} from "@gnu-taler/taler-util";
|
||||||
* --> 902.13 (a trust)
|
import logo from "./assets/logo-2021.svg";
|
||||||
* --> 902.15 (life insurance policy)
|
|
||||||
* --> 902.9 (all other cases)
|
|
||||||
* --> 902.5 (cash transaction with no customer profile)
|
|
||||||
* --> 902.4 (risk profile)
|
|
||||||
*
|
|
||||||
* 902.11
|
|
||||||
* --> 902.9 (beneficial owner in fiduciary holding assets)
|
|
||||||
*
|
|
||||||
* 902.12
|
|
||||||
*
|
|
||||||
* 902.13
|
|
||||||
*
|
|
||||||
* 902.15
|
|
||||||
*
|
|
||||||
* 902.9
|
|
||||||
*
|
|
||||||
* 902.5
|
|
||||||
*
|
|
||||||
* 902.4
|
|
||||||
*/
|
|
||||||
|
|
||||||
const userNavigation = [
|
|
||||||
{ name: "Your profile", href: "#" },
|
|
||||||
{ name: "Sign out", href: "#" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function classNames(...classes: string[]) {
|
function classNames(...classes: string[]) {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
@ -153,7 +128,7 @@ function LeftMenu() {
|
|||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Info
|
Cases
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -175,7 +150,7 @@ function LeftMenu() {
|
|||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Officer
|
Account
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -203,7 +178,7 @@ function LeftMenu() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li> */}
|
</li> */}
|
||||||
<li class="mt-auto">
|
{/* <li class="mt-auto">
|
||||||
<a
|
<a
|
||||||
href={Pages.settings.url}
|
href={Pages.settings.url}
|
||||||
class={classNames(
|
class={classNames(
|
||||||
@ -224,7 +199,7 @@ function LeftMenu() {
|
|||||||
/>
|
/>
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> */}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
@ -237,26 +212,18 @@ export function Dashboard({
|
|||||||
}): VNode {
|
}): VNode {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const logRef = useRef<HTMLPreElement>(null);
|
|
||||||
function showFormOnSidebar(v: any) {
|
|
||||||
if (!logRef.current) return;
|
|
||||||
logRef.current.innerHTML = JSON.stringify(v, undefined, 1);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NavigationBar isOpen={sidebarOpen} setOpen={setSidebarOpen}>
|
<NavigationBar isOpen={sidebarOpen} setOpen={setSidebarOpen}>
|
||||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6 pb-4">
|
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-indigo-600 px-6 pb-4">
|
||||||
<div class="flex h-16 shrink-0 items-center">
|
<div class="flex h-16 shrink-0 items-center">
|
||||||
<img
|
<header class="flex items-center justify-between border-b border-white/5 ">
|
||||||
class="h-8 w-auto"
|
<h1 class="text-base font-semibold leading-7 text-white">
|
||||||
src="https://tailwindui.com/img/logos/mark.svg?color=white"
|
Exchange AML Backoffice
|
||||||
alt="Taler"
|
</h1>
|
||||||
/>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
<div class="text-white text-sm">
|
|
||||||
<pre ref={logRef}></pre>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</NavigationBar>
|
</NavigationBar>
|
||||||
@ -362,123 +329,193 @@ function NavigationBar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Officer {
|
||||||
|
salt: string;
|
||||||
|
when: AbsoluteTime;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForOfficer = (): Codec<Officer> =>
|
||||||
|
buildCodecForObject<Officer>()
|
||||||
|
.property("salt", codecForString()) // FIXME
|
||||||
|
.property("when", codecForAbsoluteTime) // FIXME
|
||||||
|
.property("key", codecForString())
|
||||||
|
.build("Officer");
|
||||||
|
|
||||||
function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
|
function TopBar({ onOpenSidebar }: { onOpenSidebar: () => void }) {
|
||||||
const password = useMemoryStorage("password");
|
const password = useMemoryStorage("password");
|
||||||
const officer = useLocalStorage("officer");
|
const officer = useLocalStorage("officer", {
|
||||||
|
codec: codecForOfficer(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
<div class="relative flex h-16 justify-between">
|
||||||
<button
|
<div class="relative z-10 flex p-2 lg:hidden">
|
||||||
type="button"
|
<button
|
||||||
class="-m-2.5 p-2.5 text-gray-700 lg:hidden"
|
type="button"
|
||||||
onClick={onOpenSidebar}
|
onClick={() => {
|
||||||
>
|
onOpenSidebar();
|
||||||
<span class="sr-only">Open sidebar</span>
|
}}
|
||||||
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-900"
|
||||||
</button>
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
{/* Separator */}
|
>
|
||||||
<div class="h-6 w-px bg-gray-900/10 lg:hidden" aria-hidden="true" />
|
<span class="sr-only">Open menu</span>
|
||||||
|
<svg
|
||||||
<div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
class="block h-6 w-6"
|
||||||
<div class="relative flex flex-1" />
|
fill="none"
|
||||||
{/* <form class="relative flex flex-1" action="#" method="GET">
|
viewBox="0 0 24 24"
|
||||||
<label htmlFor="search-field" class="sr-only">
|
stroke-width="1.5"
|
||||||
Search
|
stroke="currentColor"
|
||||||
</label>
|
|
||||||
<MagnifyingGlassIcon
|
|
||||||
class="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
|
||||||
<input
|
|
||||||
id="search-field"
|
|
||||||
class="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
|
|
||||||
placeholder="Search..."
|
|
||||||
type="search"
|
|
||||||
name="search"
|
|
||||||
/>
|
|
||||||
</form> */}
|
|
||||||
<div class="flex items-center gap-x-4 lg:gap-x-6">
|
|
||||||
{/* <button
|
|
||||||
type="button"
|
|
||||||
class="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
|
|
||||||
>
|
>
|
||||||
<span class="sr-only">View notifications</span>
|
<path
|
||||||
<BellIcon class="h-6 w-6" aria-hidden="true" />
|
stroke-linecap="round"
|
||||||
</button> */}
|
stroke-linejoin="round"
|
||||||
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||||
{/* Separator */}
|
/>
|
||||||
<div
|
</svg>
|
||||||
class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10"
|
<svg
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="relative z-0 flex flex-1 items-center justify-center px-2 sm:absolute sm:inset-0">
|
||||||
|
<div class="w-full sm:max-w-xs flex flex-1 items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="h-8 w-auto"
|
||||||
|
src={logo}
|
||||||
|
alt="Taler"
|
||||||
|
style={{ height: 35, margin: 10 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{officer.value === undefined ? (
|
|
||||||
<div />
|
|
||||||
) : (
|
|
||||||
<Menu
|
|
||||||
as="div"
|
|
||||||
/* @ts-ignore */
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<Menu.Button class="-m-1.5 flex items-center p-1.5">
|
|
||||||
<span class="sr-only">Open user menu</span>
|
|
||||||
<img
|
|
||||||
class="h-8 w-8 rounded-full bg-gray-50"
|
|
||||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="hidden lg:flex lg:items-center">
|
|
||||||
<span
|
|
||||||
class="ml-4 text-sm font-semibold leading-6 text-gray-900"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{/* Tom Cook */}
|
|
||||||
{officer.value?.substring(0, 6)}
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon
|
|
||||||
class="ml-2 h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Menu.Button>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items class="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }: { active: boolean }) => (
|
|
||||||
<a
|
|
||||||
// href={item.href}
|
|
||||||
onClick={() => {
|
|
||||||
officer.reset();
|
|
||||||
password.reset();
|
|
||||||
}}
|
|
||||||
class={classNames(
|
|
||||||
active ? "bg-gray-50" : "",
|
|
||||||
"block px-3 py-1 text-sm leading-6 text-gray-900",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Forget account
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div class="relative z-10 flex items-center lg:hidden">dd</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
||||||
|
// <button
|
||||||
|
// type="button"
|
||||||
|
// class="-m-2.5 p-2.5 text-gray-700 lg:hidden"
|
||||||
|
// onClick={onOpenSidebar}
|
||||||
|
// >
|
||||||
|
// <span class="sr-only">Open sidebar</span>
|
||||||
|
// <Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
// </button>
|
||||||
|
|
||||||
|
// {/* Separator */}
|
||||||
|
// <div class="h-6 w-px bg-gray-900/10 lg:hidden" aria-hidden="true" />
|
||||||
|
|
||||||
|
// <div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||||
|
// <div class="relative flex flex-1" />
|
||||||
|
// {/* <form class="relative flex flex-1" action="#" method="GET">
|
||||||
|
// <label htmlFor="search-field" class="sr-only">
|
||||||
|
// Search
|
||||||
|
// </label>
|
||||||
|
// <MagnifyingGlassIcon
|
||||||
|
// class="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// />
|
||||||
|
// <input
|
||||||
|
// id="search-field"
|
||||||
|
// class="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
|
||||||
|
// placeholder="Search..."
|
||||||
|
// type="search"
|
||||||
|
// name="search"
|
||||||
|
// />
|
||||||
|
// </form> */}
|
||||||
|
// <div class="flex items-center gap-x-4 lg:gap-x-6">
|
||||||
|
// {/* <button
|
||||||
|
// type="button"
|
||||||
|
// class="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
|
||||||
|
// >
|
||||||
|
// <span class="sr-only">View notifications</span>
|
||||||
|
// <BellIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
// </button> */}
|
||||||
|
|
||||||
|
// {/* Separator */}
|
||||||
|
// <div
|
||||||
|
// class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// />
|
||||||
|
|
||||||
|
// {/* {officerName === undefined ? (
|
||||||
|
// <div />
|
||||||
|
// ) : (
|
||||||
|
// <Menu
|
||||||
|
// as="div"
|
||||||
|
// class="relative"
|
||||||
|
// >
|
||||||
|
// <Menu.Button class="-m-1.5 flex items-center p-1.5">
|
||||||
|
// <span class="sr-only">Open user menu</span>
|
||||||
|
// <img
|
||||||
|
// class="h-8 w-8 rounded-full bg-gray-50"
|
||||||
|
// src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||||
|
// alt=""
|
||||||
|
// />
|
||||||
|
// <span class="hidden lg:flex lg:items-center">
|
||||||
|
// <span
|
||||||
|
// class="ml-4 text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// >
|
||||||
|
// {officerName}
|
||||||
|
// </span>
|
||||||
|
// <ChevronDownIcon
|
||||||
|
// class="ml-2 h-5 w-5 text-gray-400"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// />
|
||||||
|
// </span>
|
||||||
|
// </Menu.Button>
|
||||||
|
// <Transition
|
||||||
|
// as={Fragment}
|
||||||
|
// enter="transition ease-out duration-100"
|
||||||
|
// enterFrom="transform opacity-0 scale-95"
|
||||||
|
// enterTo="transform opacity-100 scale-100"
|
||||||
|
// leave="transition ease-in duration-75"
|
||||||
|
// leaveFrom="transform opacity-100 scale-100"
|
||||||
|
// leaveTo="transform opacity-0 scale-95"
|
||||||
|
// >
|
||||||
|
// <Menu.Items class="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||||
|
// <Menu.Item>
|
||||||
|
// {({ active }: { active: boolean }) => (
|
||||||
|
// <a
|
||||||
|
// onClick={() => {
|
||||||
|
// officer.reset();
|
||||||
|
// password.reset();
|
||||||
|
// }}
|
||||||
|
// class={classNames(
|
||||||
|
// active ? "bg-gray-50" : "",
|
||||||
|
// "block px-3 py-1 text-sm leading-6 text-gray-900",
|
||||||
|
// )}
|
||||||
|
// >
|
||||||
|
// Forget account
|
||||||
|
// </a>
|
||||||
|
// )}
|
||||||
|
// </Menu.Item>
|
||||||
|
// </Menu.Items>
|
||||||
|
// </Transition>
|
||||||
|
// </Menu>
|
||||||
|
// )} */}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer class="absolute bottom-4">
|
<footer class="absolute bottom-4">
|
||||||
@ -502,7 +539,6 @@ function Notifications() {
|
|||||||
{
|
{
|
||||||
/* <!-- Global notification live region, render this permanently at the end of the document --> */
|
/* <!-- Global notification live region, render this permanently at the end of the document --> */
|
||||||
}
|
}
|
||||||
console.log("render", ns.length);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h } from "preact";
|
import { ComponentChildren, Fragment, h } from "preact";
|
||||||
import { FlexibleForm } from "./forms/index.js";
|
import { FlexibleForm } from "./forms/index.js";
|
||||||
import { FormProvider } from "./handlers/FormProvider.js";
|
import { FormProvider } from "./handlers/FormProvider.js";
|
||||||
import { RenderAllFieldsByUiConfig } from "./handlers/forms.js";
|
import { RenderAllFieldsByUiConfig } from "./handlers/forms.js";
|
||||||
@ -8,21 +8,25 @@ export function NiceForm<T extends object>({
|
|||||||
initial,
|
initial,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
form,
|
form,
|
||||||
|
onSubmit,
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
children?: ComponentChildren;
|
||||||
initial: Partial<T>;
|
initial: Partial<T>;
|
||||||
|
onSubmit?: (v: T) => void;
|
||||||
form: FlexibleForm<T>;
|
form: FlexibleForm<T>;
|
||||||
onUpdate: (d: Partial<T>) => void;
|
onUpdate?: (d: Partial<T>) => void;
|
||||||
}) {
|
}) {
|
||||||
const { i18n } = useTranslationContext();
|
|
||||||
return (
|
return (
|
||||||
<FormProvider
|
<FormProvider
|
||||||
initialValue={initial}
|
initialValue={initial}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
onSubmit={() => {}}
|
onSubmit={onSubmit}
|
||||||
computeFormState={form.behavior}
|
computeFormState={form.behavior}
|
||||||
>
|
>
|
||||||
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
|
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
|
||||||
{form.design.map((section, i) => {
|
{form.design.map((section, i) => {
|
||||||
|
if (!section) return <Fragment />;
|
||||||
return (
|
return (
|
||||||
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
|
||||||
<div class="px-4 sm:px-0">
|
<div class="px-4 sm:px-0">
|
||||||
@ -49,6 +53,7 @@ export function NiceForm<T extends object>({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,28 +7,33 @@ import { decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
|
|||||||
*
|
*
|
||||||
* @returns session id as string
|
* @returns session id as string
|
||||||
*/
|
*/
|
||||||
export function createNewSessionId(): string {
|
export function createSalt(): string {
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(8));
|
const salt = crypto.getRandomValues(new Uint8Array(8));
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
return encodeCrock(salt.buffer) + "-" + encodeCrock(iv.buffer);
|
return encodeCrock(salt.buffer) + "-" + encodeCrock(iv.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
accountId: string;
|
||||||
|
secret: CryptoKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore previous session and unlock account
|
* Restore previous session and unlock account
|
||||||
*
|
*
|
||||||
* @param sessionId string from which crypto params will be derived
|
* @param salt string from which crypto params will be derived
|
||||||
* @param accountId secured private key
|
* @param key secured private key
|
||||||
* @param password password for the private key
|
* @param password password for the private key
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function unlockAccount(
|
export async function unlockAccount(
|
||||||
sessionId: string,
|
salt: string,
|
||||||
accountId: string,
|
key: string,
|
||||||
password: string,
|
password: string,
|
||||||
) {
|
): Promise<Account> {
|
||||||
const key = str2ab(window.atob(accountId));
|
const rawKey = str2ab(window.atob(key));
|
||||||
|
|
||||||
const privateKey = await recoverWithPassword(key, sessionId, password);
|
const privateKey = await recoverWithPassword(rawKey, salt, password);
|
||||||
|
|
||||||
const publicKey = await getPublicFromPrivate(privateKey);
|
const publicKey = await getPublicFromPrivate(privateKey);
|
||||||
|
|
||||||
@ -36,9 +41,9 @@ export async function unlockAccount(
|
|||||||
throw new Error(String(e));
|
throw new Error(String(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
const pub = btoa(ab2str(pubRaw));
|
const accountId = btoa(ab2str(pubRaw));
|
||||||
|
|
||||||
return { accountId, pub };
|
return { accountId, secret: privateKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,12 +54,13 @@ export async function unlockAccount(
|
|||||||
* @param password
|
* @param password
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function createNewAccount(sessionId: string, password: string) {
|
export async function createNewAccount(password: string) {
|
||||||
const { privateKey, publicKey } = await createPair();
|
const { privateKey } = await createPair();
|
||||||
|
const salt = createSalt();
|
||||||
|
|
||||||
const protectedPrivKey = await protectWithPassword(
|
const protectedPrivKey = await protectWithPassword(
|
||||||
privateKey,
|
privateKey,
|
||||||
sessionId,
|
salt,
|
||||||
password,
|
password,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -64,14 +70,14 @@ export async function createNewAccount(sessionId: string, password: string) {
|
|||||||
// throw new Error(String(e));
|
// throw new Error(String(e));
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => {
|
// const pubRaw = await crypto.subtle.exportKey("spki", publicKey).catch((e) => {
|
||||||
throw new Error(String(e));
|
// throw new Error(String(e));
|
||||||
});
|
// });
|
||||||
|
|
||||||
const pub = btoa(ab2str(pubRaw));
|
// const pub = btoa(ab2str(pubRaw));
|
||||||
const protectedPriv = btoa(ab2str(protectedPrivKey));
|
const protectedPriv = btoa(ab2str(protectedPrivKey));
|
||||||
|
|
||||||
return { accountId: protectedPriv, pub };
|
return { accountId: protectedPriv, salt };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rsaAlgorithm: RsaHashedKeyGenParams = {
|
const rsaAlgorithm: RsaHashedKeyGenParams = {
|
||||||
@ -97,7 +103,7 @@ async function protectWithPassword(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<ArrayBuffer> {
|
): Promise<ArrayBuffer> {
|
||||||
const { salt, initVector: iv } = getCryptoPArameters(sessionId);
|
const { salt, initVector: iv } = getCryptoParameters(sessionId);
|
||||||
const passwordAsKey = await crypto.subtle
|
const passwordAsKey = await crypto.subtle
|
||||||
.importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [
|
.importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [
|
||||||
"deriveBits",
|
"deriveBits",
|
||||||
@ -139,7 +145,7 @@ async function recoverWithPassword(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<CryptoKey> {
|
): Promise<CryptoKey> {
|
||||||
const { salt, initVector: iv } = getCryptoPArameters(sessionId);
|
const { salt, initVector: iv } = getCryptoParameters(sessionId);
|
||||||
|
|
||||||
const master = await crypto.subtle
|
const master = await crypto.subtle
|
||||||
.importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [
|
.importKey("raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [
|
||||||
@ -231,7 +237,7 @@ function str2ab(str: string) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCryptoPArameters(sessionId: string): {
|
function getCryptoParameters(sessionId: string): {
|
||||||
salt: Uint8Array;
|
salt: Uint8Array;
|
||||||
initVector: Uint8Array;
|
initVector: Uint8Array;
|
||||||
} {
|
} {
|
||||||
|
9
packages/exchange-backoffice-ui/src/assets/logo-2021.svg
Normal file
9
packages/exchange-backoffice-ui/src/assets/logo-2021.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
|
||||||
|
<g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
|
||||||
|
<path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
|
||||||
|
<path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
|
||||||
|
<path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
|
||||||
|
</g>
|
||||||
|
<path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,2 +1,30 @@
|
|||||||
declare const __VERSION__: string;
|
declare const __VERSION__: string;
|
||||||
declare const __GIT_HASH__: string;
|
declare const __GIT_HASH__: string;
|
||||||
|
|
||||||
|
declare module "*.po" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
declare module "jed" {
|
||||||
|
const x: any;
|
||||||
|
export = x;
|
||||||
|
}
|
||||||
|
declare module "*.jpeg" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
declare module "*.png" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
declare module "*.svg" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: Record<string, string>;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
declare const __VERSION__: string;
|
||||||
|
declare const __GIT_HASH__: string;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_11e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_11.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -115,8 +116,8 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_11e.Form>,
|
v: Partial<Form902_11.Form>,
|
||||||
): FormState<Form902_11e.Form> {
|
): FormState<Form902_11.Form> {
|
||||||
return {
|
return {
|
||||||
person: {
|
person: {
|
||||||
hidden:
|
hidden:
|
||||||
@ -128,9 +129,9 @@ export const v1: FlexibleForm<Form902_11e.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_11e {
|
namespace Form902_11 {
|
||||||
interface Person {
|
interface Person {
|
||||||
lastName: string;
|
lastName: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_12e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_12.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -364,8 +365,8 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_12e.Form>,
|
v: Partial<Form902_12.Form>,
|
||||||
): FormState<Form902_12e.Form> {
|
): FormState<Form902_12.Form> {
|
||||||
return {
|
return {
|
||||||
founders: {
|
founders: {
|
||||||
elements: (v.founders ?? []).map((f) => {
|
elements: (v.founders ?? []).map((f) => {
|
||||||
@ -390,9 +391,9 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_12e {
|
namespace Form902_12 {
|
||||||
interface Foundation {
|
interface Foundation {
|
||||||
name: string;
|
name: string;
|
||||||
type: "discretionary" | "non-discretionary";
|
type: "discretionary" | "non-discretionary";
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_13e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_13.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -441,8 +442,8 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_13e.Form>,
|
v: Partial<Form902_13.Form>,
|
||||||
): FormState<Form902_13e.Form> {
|
): FormState<Form902_13.Form> {
|
||||||
return {
|
return {
|
||||||
settlors: {
|
settlors: {
|
||||||
elements: (v.settlors ?? []).map((f) => {
|
elements: (v.settlors ?? []).map((f) => {
|
||||||
@ -476,9 +477,9 @@ export const v1: FlexibleForm<Form902_13e.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_13e {
|
namespace Form902_13 {
|
||||||
interface Foundation {
|
interface Foundation {
|
||||||
name: string;
|
name: string;
|
||||||
type: "discretionary" | "non-discretionary";
|
type: "discretionary" | "non-discretionary";
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_15e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_15.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -160,17 +161,17 @@ export const v1: FlexibleForm<Form902_15e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_15e.Form>,
|
v: Partial<Form902_15.Form>,
|
||||||
): FormState<Form902_15e.Form> {
|
): FormState<Form902_15.Form> {
|
||||||
return {
|
return {
|
||||||
when: {
|
when: {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_15e {
|
namespace Form902_15 {
|
||||||
interface Person {
|
interface Person {
|
||||||
fullName: string;
|
fullName: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FlexibleForm, languageList } from "./index.js";
|
import { FlexibleForm, languageList } from "./index.js";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_1e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -512,8 +513,8 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_1e.Form>,
|
v: Partial<Form902_1.Form>,
|
||||||
): FormState<Form902_1e.Form> {
|
): FormState<Form902_1.Form> {
|
||||||
return {
|
return {
|
||||||
fullName: {
|
fullName: {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@ -606,9 +607,9 @@ export const v1: FlexibleForm<Form902_1e.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_1e {
|
namespace Form902_1 {
|
||||||
interface LegalEntityCustomer {
|
interface LegalEntityCustomer {
|
||||||
companyName: string;
|
companyName: string;
|
||||||
domicile: string;
|
domicile: string;
|
||||||
|
@ -4,8 +4,9 @@ import { FlexibleForm } from "./index.js";
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { h as create } from "preact";
|
import { h as create } from "preact";
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_4.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -745,7 +746,7 @@ export const v1: FlexibleForm<Form902_4.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_4 {
|
namespace Form902_4 {
|
||||||
export interface Form {
|
export interface Form {
|
||||||
|
@ -5,8 +5,9 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm, currencyList } from "./index.js";
|
import { FlexibleForm, currencyList } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_12e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_5.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -230,8 +231,8 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_12e.Form>,
|
v: Partial<Form902_5.Form>,
|
||||||
): FormState<Form902_12e.Form> {
|
): FormState<Form902_5.Form> {
|
||||||
return {
|
return {
|
||||||
when: {
|
when: {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@ -243,9 +244,9 @@ export const v1: FlexibleForm<Form902_12e.Form> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_12e {
|
namespace Form902_5 {
|
||||||
export interface Form {
|
export interface Form {
|
||||||
customer: string;
|
customer: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { FormState } from "../handlers/FormProvider.js";
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
import { FlexibleForm } from "./index.js";
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
export const v1: FlexibleForm<Form902_9e.Form> = {
|
export const v1 = (current: State): FlexibleForm<Form902_9.Form> => ({
|
||||||
versionId: "2023-05-15",
|
versionId: "2023-05-15",
|
||||||
design: [
|
design: [
|
||||||
{
|
{
|
||||||
@ -104,17 +105,17 @@ export const v1: FlexibleForm<Form902_9e.Form> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
behavior: function formBehavior(
|
behavior: function formBehavior(
|
||||||
v: Partial<Form902_9e.Form>,
|
v: Partial<Form902_9.Form>,
|
||||||
): FormState<Form902_9e.Form> {
|
): FormState<Form902_9.Form> {
|
||||||
return {
|
return {
|
||||||
when: {
|
when: {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
namespace Form902_9e {
|
namespace Form902_9 {
|
||||||
interface Person {
|
interface Person {
|
||||||
surname: string;
|
surname: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
96
packages/exchange-backoffice-ui/src/forms/simplest.ts
Normal file
96
packages/exchange-backoffice-ui/src/forms/simplest.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
TranslatedString,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { FormState } from "../handlers/FormProvider.js";
|
||||||
|
import { FlexibleForm } from "./index.js";
|
||||||
|
import { AmlState } from "../types.js";
|
||||||
|
import { amlStateConverter } from "../pages/AccountDetails.js";
|
||||||
|
import { State } from "../pages/AntiMoneyLaunderingForm.js";
|
||||||
|
|
||||||
|
export const v1 = (current: State): FlexibleForm<Simplest.Form> => ({
|
||||||
|
versionId: "2023-05-25",
|
||||||
|
design: [
|
||||||
|
{
|
||||||
|
title: "Simple form" as TranslatedString,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "textArea",
|
||||||
|
props: {
|
||||||
|
name: "comment",
|
||||||
|
label: "Comments" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Resolution" as TranslatedString,
|
||||||
|
description: `Current state is ${amlStateConverter.toStringUI(
|
||||||
|
current.state,
|
||||||
|
)} and threshold at ${Amounts.stringifyValue(
|
||||||
|
current.threshold,
|
||||||
|
)}` as TranslatedString,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "date",
|
||||||
|
props: {
|
||||||
|
name: "when",
|
||||||
|
label: "Decision Time" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "choiceHorizontal",
|
||||||
|
props: {
|
||||||
|
name: "state",
|
||||||
|
label: "New state" as TranslatedString,
|
||||||
|
converter: amlStateConverter,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: AmlState.frozen,
|
||||||
|
label: "Frozen" as TranslatedString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AmlState.pending,
|
||||||
|
label: "Pending" as TranslatedString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AmlState.normal,
|
||||||
|
label: "Normal" as TranslatedString,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "amount",
|
||||||
|
props: {
|
||||||
|
name: "threshold",
|
||||||
|
label: "New threshold" as TranslatedString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
behavior: function formBehavior(
|
||||||
|
v: Partial<Simplest.Form>,
|
||||||
|
): FormState<Simplest.Form> {
|
||||||
|
return {
|
||||||
|
when: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
threshold: {
|
||||||
|
disabled: v.state === AmlState.frozen,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
namespace Simplest {
|
||||||
|
export interface Form {
|
||||||
|
when: AbsoluteTime;
|
||||||
|
threshold: AmountJson;
|
||||||
|
state: AmlState;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,16 @@
|
|||||||
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AmountJson,
|
||||||
|
TranslatedString,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
import { ComponentChildren, VNode, createContext, h } from "preact";
|
import { ComponentChildren, VNode, createContext, h } from "preact";
|
||||||
import { MutableRef, StateUpdater, useEffect, useRef } from "preact/hooks";
|
import {
|
||||||
|
MutableRef,
|
||||||
|
StateUpdater,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "preact/hooks";
|
||||||
|
|
||||||
export interface FormType<T> {
|
export interface FormType<T> {
|
||||||
value: MutableRef<Partial<T>>;
|
value: MutableRef<Partial<T>>;
|
||||||
@ -14,6 +24,8 @@ export const FormContext = createContext<FormType<any>>({});
|
|||||||
|
|
||||||
export type FormState<T> = {
|
export type FormState<T> = {
|
||||||
[field in keyof T]?: T[field] extends AbsoluteTime
|
[field in keyof T]?: T[field] extends AbsoluteTime
|
||||||
|
? Partial<InputFieldState>
|
||||||
|
: T[field] extends AmountJson
|
||||||
? Partial<InputFieldState>
|
? Partial<InputFieldState>
|
||||||
: T[field] extends Array<infer P>
|
: T[field] extends Array<infer P>
|
||||||
? Partial<InputArrayFieldState<P>>
|
? Partial<InputArrayFieldState<P>>
|
||||||
@ -40,22 +52,31 @@ export interface InputArrayFieldState<T> extends InputFieldState {
|
|||||||
export function FormProvider<T>({
|
export function FormProvider<T>({
|
||||||
children,
|
children,
|
||||||
initialValue,
|
initialValue,
|
||||||
onUpdate,
|
onUpdate: notify,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
computeFormState,
|
computeFormState,
|
||||||
}: {
|
}: {
|
||||||
initialValue?: Partial<T>;
|
initialValue?: Partial<T>;
|
||||||
onUpdate?: (v: Partial<T>) => void;
|
onUpdate?: (v: Partial<T>) => void;
|
||||||
onSubmit: (v: T) => void;
|
onSubmit?: (v: T) => void;
|
||||||
computeFormState?: (v: T) => FormState<T>;
|
computeFormState?: (v: T) => FormState<T>;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const value = useRef(initialValue ?? {});
|
// const value = useRef(initialValue ?? {});
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
return function onUnload() {
|
// return function onUnload() {
|
||||||
value.current = initialValue ?? {};
|
// value.current = initialValue ?? {};
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
|
// const onUpdate = notify
|
||||||
|
const [state, setState] = useState<Partial<T>>(initialValue ?? {});
|
||||||
|
const value = { current: state };
|
||||||
|
// console.log("RENDER", initialValue, value);
|
||||||
|
const onUpdate = (v: typeof state) => {
|
||||||
|
// console.log("updated");
|
||||||
|
setState(v);
|
||||||
|
if (notify) notify(v);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<FormContext.Provider
|
<FormContext.Provider
|
||||||
value={{ initialValue, value, onUpdate, computeFormState }}
|
value={{ initialValue, value, onUpdate, computeFormState }}
|
||||||
@ -64,7 +85,7 @@ export function FormProvider<T>({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
onSubmit(value.current);
|
if (onSubmit) onSubmit(value.current);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
34
packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx
Normal file
34
packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { VNode, h } from "preact";
|
||||||
|
import { InputLine, UIFormProps } from "./InputLine.js";
|
||||||
|
import { useField } from "./useField.js";
|
||||||
|
|
||||||
|
export function InputAmount<T extends object, K extends keyof T>(
|
||||||
|
props: { currency?: string } & UIFormProps<T, K>,
|
||||||
|
): VNode {
|
||||||
|
const { value } = useField<T, K>(props.name);
|
||||||
|
const currency =
|
||||||
|
!value || !(value as any).currency
|
||||||
|
? props.currency
|
||||||
|
: (value as any).currency;
|
||||||
|
return (
|
||||||
|
<InputLine<T, K>
|
||||||
|
type="text"
|
||||||
|
before={{
|
||||||
|
type: "text",
|
||||||
|
text: currency as TranslatedString,
|
||||||
|
}}
|
||||||
|
converter={{
|
||||||
|
//@ts-ignore
|
||||||
|
fromStringUI: (v): AmountJson => {
|
||||||
|
return Amounts.parseOrThrow(`${currency}:${v}`);
|
||||||
|
},
|
||||||
|
//@ts-ignore
|
||||||
|
toStringUI: (v: AmountJson) => {
|
||||||
|
return v === undefined ? "" : Amounts.stringifyValue(v);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
|
||||||
|
import { useField } from "./useField.js";
|
||||||
|
|
||||||
|
export interface Choice<V> {
|
||||||
|
label: TranslatedString;
|
||||||
|
value: V;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputChoiceHorizontal<T extends object, K extends keyof T>(
|
||||||
|
props: {
|
||||||
|
choices: Choice<T[K]>[];
|
||||||
|
} & UIFormProps<T, K>,
|
||||||
|
): VNode {
|
||||||
|
const {
|
||||||
|
choices,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
help,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
converter,
|
||||||
|
} = props;
|
||||||
|
const { value, onChange, state, isDirty } = useField<T, K>(name);
|
||||||
|
if (state.hidden) {
|
||||||
|
return <Fragment />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="sm:col-span-6">
|
||||||
|
<LabelWithTooltipMaybeRequired
|
||||||
|
label={label}
|
||||||
|
required={required}
|
||||||
|
tooltip={tooltip}
|
||||||
|
/>
|
||||||
|
<fieldset class="mt-2">
|
||||||
|
<div class="isolate inline-flex rounded-md shadow-sm">
|
||||||
|
{choices.map((choice, idx) => {
|
||||||
|
const isFirst = idx === 0;
|
||||||
|
const isLast = idx === choices.length - 1;
|
||||||
|
let clazz =
|
||||||
|
"relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10";
|
||||||
|
if (choice.value === value) {
|
||||||
|
clazz +=
|
||||||
|
" text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
|
||||||
|
} else {
|
||||||
|
clazz += " hover:bg-gray-100 border-gray-300";
|
||||||
|
}
|
||||||
|
if (isFirst) {
|
||||||
|
clazz += " rounded-l-md";
|
||||||
|
} else {
|
||||||
|
clazz += " -ml-px";
|
||||||
|
}
|
||||||
|
if (isLast) {
|
||||||
|
clazz += " rounded-r-md";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={clazz}
|
||||||
|
onClick={(e) => {
|
||||||
|
onChange(
|
||||||
|
(value === choice.value ? undefined : choice.value) as T[K],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(!converter
|
||||||
|
? (choice.value as string)
|
||||||
|
: converter?.toStringUI(choice.value)) ?? ""}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{help && (
|
||||||
|
<p class="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
{help}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -3,15 +3,15 @@ import { Fragment, VNode, h } from "preact";
|
|||||||
import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
|
import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
|
||||||
import { useField } from "./useField.js";
|
import { useField } from "./useField.js";
|
||||||
|
|
||||||
export interface Choice {
|
export interface Choice<V> {
|
||||||
label: TranslatedString;
|
label: TranslatedString;
|
||||||
description?: TranslatedString;
|
description?: TranslatedString;
|
||||||
value: string;
|
value: V;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputChoiceStacked<T extends object, K extends keyof T>(
|
export function InputChoiceStacked<T extends object, K extends keyof T>(
|
||||||
props: {
|
props: {
|
||||||
choices: Choice[];
|
choices: Choice<T[K]>[];
|
||||||
} & UIFormProps<T, K>,
|
} & UIFormProps<T, K>,
|
||||||
): VNode {
|
): VNode {
|
||||||
const {
|
const {
|
||||||
@ -41,6 +41,10 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
|
|||||||
<fieldset class="mt-2">
|
<fieldset class="mt-2">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{choices.map((choice) => {
|
{choices.map((choice) => {
|
||||||
|
// const currentValue = !converter
|
||||||
|
// ? choice.value
|
||||||
|
// : converter.fromStringUI(choice.value) ?? "";
|
||||||
|
|
||||||
let clazz =
|
let clazz =
|
||||||
"border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between";
|
"border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between";
|
||||||
if (choice.value === value) {
|
if (choice.value === value) {
|
||||||
@ -49,12 +53,18 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
|
|||||||
} else {
|
} else {
|
||||||
clazz += " border-gray-300";
|
clazz += " border-gray-300";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label class={clazz}>
|
<label class={clazz}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="server-size"
|
name="server-size"
|
||||||
defaultValue={choice.value}
|
// defaultValue={choice.value}
|
||||||
|
value={
|
||||||
|
(!converter
|
||||||
|
? (choice.value as string)
|
||||||
|
: converter?.toStringUI(choice.value)) ?? ""
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onChange(
|
onChange(
|
||||||
(value === choice.value
|
(value === choice.value
|
||||||
|
@ -250,7 +250,8 @@ export function InputLine<T extends object, K extends keyof T>(
|
|||||||
onChange(fromString(e.currentTarget.value));
|
onChange(fromString(e.currentTarget.value));
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder ? placeholder : undefined}
|
placeholder={placeholder ? placeholder : undefined}
|
||||||
defaultValue={toString(value)}
|
value={toString(value) ?? ""}
|
||||||
|
// defaultValue={toString(value)}
|
||||||
disabled={state.disabled}
|
disabled={state.disabled}
|
||||||
aria-invalid={showError}
|
aria-invalid={showError}
|
||||||
// aria-describedby="email-error"
|
// aria-describedby="email-error"
|
||||||
@ -269,7 +270,8 @@ export function InputLine<T extends object, K extends keyof T>(
|
|||||||
onChange(fromString(e.currentTarget.value));
|
onChange(fromString(e.currentTarget.value));
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder ? placeholder : undefined}
|
placeholder={placeholder ? placeholder : undefined}
|
||||||
defaultValue={toString(value)}
|
value={toString(value) ?? ""}
|
||||||
|
// defaultValue={toString(value)}
|
||||||
disabled={state.disabled}
|
disabled={state.disabled}
|
||||||
aria-invalid={showError}
|
aria-invalid={showError}
|
||||||
// aria-describedby="email-error"
|
// aria-describedby="email-error"
|
||||||
|
@ -13,8 +13,10 @@ import { Group } from "./Group.js";
|
|||||||
import { InputSelectOne } from "./InputSelectOne.js";
|
import { InputSelectOne } from "./InputSelectOne.js";
|
||||||
import { FormProvider } from "./FormProvider.js";
|
import { FormProvider } from "./FormProvider.js";
|
||||||
import { InputLine } from "./InputLine.js";
|
import { InputLine } from "./InputLine.js";
|
||||||
|
import { InputAmount } from "./InputAmount.js";
|
||||||
|
import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
|
||||||
|
|
||||||
export type DoubleColumnForm = DoubleColumnFormSection[];
|
export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
|
||||||
|
|
||||||
type DoubleColumnFormSection = {
|
type DoubleColumnFormSection = {
|
||||||
title: TranslatedString;
|
title: TranslatedString;
|
||||||
@ -35,8 +37,10 @@ type FieldType<T extends object = any, K extends keyof T = any> = {
|
|||||||
text: Parameters<typeof InputText<T, K>>[0];
|
text: Parameters<typeof InputText<T, K>>[0];
|
||||||
textArea: Parameters<typeof InputTextArea<T, K>>[0];
|
textArea: Parameters<typeof InputTextArea<T, K>>[0];
|
||||||
choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
|
choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
|
||||||
|
choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
|
||||||
date: Parameters<typeof InputDate<T, K>>[0];
|
date: Parameters<typeof InputDate<T, K>>[0];
|
||||||
integer: Parameters<typeof InputInteger<T, K>>[0];
|
integer: Parameters<typeof InputInteger<T, K>>[0];
|
||||||
|
amount: Parameters<typeof InputAmount<T, K>>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,11 +51,13 @@ export type UIFormField =
|
|||||||
| { type: "caption"; props: FieldType["caption"] }
|
| { type: "caption"; props: FieldType["caption"] }
|
||||||
| { type: "array"; props: FieldType["array"] }
|
| { type: "array"; props: FieldType["array"] }
|
||||||
| { type: "file"; props: FieldType["file"] }
|
| { type: "file"; props: FieldType["file"] }
|
||||||
|
| { type: "amount"; props: FieldType["amount"] }
|
||||||
| { type: "selectOne"; props: FieldType["selectOne"] }
|
| { type: "selectOne"; props: FieldType["selectOne"] }
|
||||||
| { type: "selectMultiple"; props: FieldType["selectMultiple"] }
|
| { type: "selectMultiple"; props: FieldType["selectMultiple"] }
|
||||||
| { type: "text"; props: FieldType["text"] }
|
| { type: "text"; props: FieldType["text"] }
|
||||||
| { type: "textArea"; props: FieldType["textArea"] }
|
| { type: "textArea"; props: FieldType["textArea"] }
|
||||||
| { type: "choiceStacked"; props: FieldType["choiceStacked"] }
|
| { type: "choiceStacked"; props: FieldType["choiceStacked"] }
|
||||||
|
| { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
|
||||||
| { type: "integer"; props: FieldType["integer"] }
|
| { type: "integer"; props: FieldType["integer"] }
|
||||||
| { type: "date"; props: FieldType["date"] };
|
| { type: "date"; props: FieldType["date"] };
|
||||||
|
|
||||||
@ -79,11 +85,15 @@ const UIFormConfiguration: UIFormFieldMap = {
|
|||||||
date: InputDate,
|
date: InputDate,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
choiceStacked: InputChoiceStacked,
|
choiceStacked: InputChoiceStacked,
|
||||||
|
//@ts-ignore
|
||||||
|
choiceHorizontal: InputChoiceHorizontal,
|
||||||
integer: InputInteger,
|
integer: InputInteger,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
selectOne: InputSelectOne,
|
selectOne: InputSelectOne,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
selectMultiple: InputSelectMultiple,
|
selectMultiple: InputSelectMultiple,
|
||||||
|
//@ts-ignore
|
||||||
|
amount: InputAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RenderAllFieldsByUiConfig({
|
export function RenderAllFieldsByUiConfig({
|
||||||
@ -103,13 +113,23 @@ export function RenderAllFieldsByUiConfig({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormSet<T extends object, K extends keyof T = any> = {
|
type FormSet<T extends object> = {
|
||||||
Provider: typeof FormProvider<T>;
|
Provider: typeof FormProvider<T>;
|
||||||
InputLine: typeof InputLine<T, K>;
|
InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
|
||||||
|
InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<
|
||||||
|
T,
|
||||||
|
K
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
export function createNewForm<T extends object>(): FormSet<T> {
|
export function createNewForm<T extends object>() {
|
||||||
return {
|
const res: FormSet<T> = {
|
||||||
Provider: FormProvider,
|
Provider: FormProvider,
|
||||||
InputLine: InputLine,
|
InputLine: () => InputLine,
|
||||||
|
InputChoiceHorizontal: () => InputChoiceHorizontal,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
Provider: res.Provider,
|
||||||
|
InputLine: res.InputLine(),
|
||||||
|
InputChoiceHorizontal: res.InputChoiceHorizontal(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import { TargetedEvent, useContext, useState } from "preact/compat";
|
import { useContext, useState } from "preact/compat";
|
||||||
import {
|
import { FormContext, InputFieldState } from "./FormProvider.js";
|
||||||
FormContext,
|
|
||||||
InputArrayFieldState,
|
|
||||||
InputFieldState,
|
|
||||||
} from "./FormProvider.js";
|
|
||||||
|
|
||||||
export interface InputFieldHandler<Type> {
|
export interface InputFieldHandler<Type> {
|
||||||
value: Type;
|
value: Type;
|
||||||
@ -21,11 +17,13 @@ export function useField<T extends object, K extends keyof T>(
|
|||||||
computeFormState,
|
computeFormState,
|
||||||
onUpdate: notifyUpdate,
|
onUpdate: notifyUpdate,
|
||||||
} = useContext(FormContext);
|
} = useContext(FormContext);
|
||||||
|
|
||||||
type P = typeof name;
|
type P = typeof name;
|
||||||
type V = T[P];
|
type V = T[P];
|
||||||
const formState = computeFormState ? computeFormState(formValue.current) : {};
|
const formState = computeFormState ? computeFormState(formValue.current) : {};
|
||||||
|
|
||||||
const fieldValue = readField(formValue.current, String(name)) as V;
|
const fieldValue = readField(formValue.current, String(name)) as V;
|
||||||
|
// console.log("USE FIELD", String(name), formValue.current, fieldValue);
|
||||||
const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
|
const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
|
||||||
const fieldState =
|
const fieldState =
|
||||||
readField<Partial<InputFieldState>>(formState, String(name)) ?? {};
|
readField<Partial<InputFieldState>>(formState, String(name)) ?? {};
|
||||||
@ -66,10 +64,23 @@ export function useField<T extends object, K extends keyof T>(
|
|||||||
* @param name
|
* @param name
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function readField<T>(object: any, name: string): T | undefined {
|
function readField<T>(
|
||||||
return name
|
object: any,
|
||||||
.split(".")
|
name: string,
|
||||||
.reduce((prev, current) => prev && prev[current], object);
|
debug?: boolean,
|
||||||
|
): T | undefined {
|
||||||
|
return name.split(".").reduce((prev, current) => {
|
||||||
|
if (debug) {
|
||||||
|
console.log(
|
||||||
|
"READ",
|
||||||
|
name,
|
||||||
|
prev,
|
||||||
|
current,
|
||||||
|
prev ? prev[current] : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return prev ? prev[current] : undefined;
|
||||||
|
}, object);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setValueDeeper(object: any, names: string[], value: any): any {
|
function setValueDeeper(object: any, names: string[], value: any): any {
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
|
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
|
||||||
<title>Exchange Backoffice</title>
|
<title>Exchange Backoffice</title>
|
||||||
|
<!-- Optional customization script. -->
|
||||||
|
<script src="exchange-backofice-ui-settings.js"></script>
|
||||||
<!-- Entry point for the SPA. -->
|
<!-- Entry point for the SPA. -->
|
||||||
<script type="module" src="index.js"></script>
|
<script type="module" src="index.js"></script>
|
||||||
<link rel="stylesheet" href="index.css" />
|
<link rel="stylesheet" href="index.css" />
|
||||||
|
@ -4,15 +4,26 @@ import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js";
|
|||||||
import { Welcome } from "./pages/Welcome.js";
|
import { Welcome } from "./pages/Welcome.js";
|
||||||
import { PageEntry, pageDefinition } from "./route.js";
|
import { PageEntry, pageDefinition } from "./route.js";
|
||||||
import { Officer } from "./pages/Officer.js";
|
import { Officer } from "./pages/Officer.js";
|
||||||
import { Info } from "./pages/Info.js";
|
import { Cases } from "./pages/Cases.js";
|
||||||
|
import { AccountDetails } from "./pages/AccountDetails.js";
|
||||||
|
import { NewFormEntry } from "./pages/NewFormEntry.js";
|
||||||
|
|
||||||
const home: PageEntry = {
|
const home: PageEntry = {
|
||||||
url: "#/",
|
url: "#/",
|
||||||
view: Home,
|
view: Home,
|
||||||
};
|
};
|
||||||
const info: PageEntry = {
|
const cases: PageEntry = {
|
||||||
url: "#/info",
|
url: "#/cases",
|
||||||
view: Info,
|
view: Cases,
|
||||||
|
};
|
||||||
|
const account: PageEntry<{ account?: string }> = {
|
||||||
|
url: pageDefinition("#/account/:account"),
|
||||||
|
view: AccountDetails,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
|
||||||
|
url: pageDefinition("#/account/:account/new/:type?"),
|
||||||
|
view: NewFormEntry,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings: PageEntry = {
|
const settings: PageEntry = {
|
||||||
@ -32,4 +43,13 @@ const form: PageEntry<{ number?: string }> = {
|
|||||||
view: AntiMoneyLaunderingForm,
|
view: AntiMoneyLaunderingForm,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Pages = { home, info, officer, settings, welcome, form };
|
export const Pages = {
|
||||||
|
home,
|
||||||
|
info: cases,
|
||||||
|
officer,
|
||||||
|
details: account,
|
||||||
|
settings,
|
||||||
|
welcome,
|
||||||
|
form,
|
||||||
|
newFormEntry,
|
||||||
|
};
|
||||||
|
457
packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
Normal file
457
packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import {
|
||||||
|
AmlDecisionDetail,
|
||||||
|
AmlDecisionDetails,
|
||||||
|
AmlState,
|
||||||
|
KycDetail,
|
||||||
|
} from "../types.js";
|
||||||
|
import {
|
||||||
|
AbsoluteTime,
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
TranslatedString,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ArrowDownCircleIcon, ClockIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { NiceForm } from "../NiceForm.js";
|
||||||
|
import { FlexibleForm } from "../forms/index.js";
|
||||||
|
import { UIFormField } from "../handlers/forms.js";
|
||||||
|
import { Pages } from "../pages.js";
|
||||||
|
|
||||||
|
const response: AmlDecisionDetails = {
|
||||||
|
aml_history: [
|
||||||
|
{
|
||||||
|
justification: "Lack of documentation",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
new_state: 2,
|
||||||
|
new_threshold: "USD:0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
justification: "Doing a transfer of high amount",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
|
||||||
|
},
|
||||||
|
new_state: 1,
|
||||||
|
new_threshold: "USD:2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
justification: "Account is known to the system",
|
||||||
|
decider_pub: "ASDASDASD",
|
||||||
|
decision_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
|
||||||
|
},
|
||||||
|
new_state: 0,
|
||||||
|
new_threshold: "USD:100",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
kyc_attributes: [
|
||||||
|
{
|
||||||
|
collection_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
|
||||||
|
},
|
||||||
|
expiration_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
|
||||||
|
},
|
||||||
|
provider_section: "asdasd",
|
||||||
|
attributes: {
|
||||||
|
name: "Sebastian",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collection_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
|
||||||
|
},
|
||||||
|
expiration_time: {
|
||||||
|
t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
|
||||||
|
},
|
||||||
|
provider_section: "asdasd",
|
||||||
|
attributes: {
|
||||||
|
creditCard: "12312312312",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
type AmlEvent = AmlFormEvent | KycCollectionEvent | KycExpirationEvent;
|
||||||
|
type AmlFormEvent = {
|
||||||
|
type: "aml-form";
|
||||||
|
when: AbsoluteTime;
|
||||||
|
title: TranslatedString;
|
||||||
|
state: AmlState;
|
||||||
|
threshold: AmountJson;
|
||||||
|
};
|
||||||
|
type KycCollectionEvent = {
|
||||||
|
type: "kyc-collection";
|
||||||
|
when: AbsoluteTime;
|
||||||
|
title: TranslatedString;
|
||||||
|
values: object;
|
||||||
|
provider: string;
|
||||||
|
};
|
||||||
|
type KycExpirationEvent = {
|
||||||
|
type: "kyc-expiration";
|
||||||
|
when: AbsoluteTime;
|
||||||
|
title: TranslatedString;
|
||||||
|
fields: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WithTime = { when: AbsoluteTime };
|
||||||
|
|
||||||
|
function selectSooner(a: WithTime, b: WithTime) {
|
||||||
|
return AbsoluteTime.cmp(a.when, b.when);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventsFromAmlHistory(
|
||||||
|
aml: AmlDecisionDetail[],
|
||||||
|
kyc: KycDetail[],
|
||||||
|
): AmlEvent[] {
|
||||||
|
const ae: AmlEvent[] = aml.map((a) => {
|
||||||
|
return {
|
||||||
|
type: "aml-form",
|
||||||
|
state: a.new_state,
|
||||||
|
threshold: Amounts.parseOrThrow(a.new_threshold),
|
||||||
|
title: a.justification as TranslatedString,
|
||||||
|
when: {
|
||||||
|
t_ms:
|
||||||
|
a.decision_time.t_s === "never"
|
||||||
|
? "never"
|
||||||
|
: a.decision_time.t_s * 1000,
|
||||||
|
},
|
||||||
|
} as AmlEvent;
|
||||||
|
});
|
||||||
|
const ke = kyc.reduce((prev, k) => {
|
||||||
|
prev.push({
|
||||||
|
type: "kyc-collection",
|
||||||
|
title: "collection" as TranslatedString,
|
||||||
|
when: {
|
||||||
|
t_ms:
|
||||||
|
k.collection_time.t_s === "never"
|
||||||
|
? "never"
|
||||||
|
: k.collection_time.t_s * 1000,
|
||||||
|
},
|
||||||
|
values: !k.attributes ? {} : k.attributes,
|
||||||
|
provider: k.provider_section,
|
||||||
|
});
|
||||||
|
prev.push({
|
||||||
|
type: "kyc-expiration",
|
||||||
|
title: "expired" as TranslatedString,
|
||||||
|
when: {
|
||||||
|
t_ms:
|
||||||
|
k.expiration_time.t_s === "never"
|
||||||
|
? "never"
|
||||||
|
: k.expiration_time.t_s * 1000,
|
||||||
|
},
|
||||||
|
fields: !k.attributes ? [] : Object.keys(k.attributes),
|
||||||
|
});
|
||||||
|
return prev;
|
||||||
|
}, [] as AmlEvent[]);
|
||||||
|
return ae.concat(ke).sort(selectSooner);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountDetails({ account }: { account?: string }) {
|
||||||
|
const events = getEventsFromAmlHistory(
|
||||||
|
response.aml_history,
|
||||||
|
response.kyc_attributes,
|
||||||
|
);
|
||||||
|
console.log("DETAILS", events, events[events.length - 1 - 2]);
|
||||||
|
const [selected, setSelected] = useState<AmlEvent>(
|
||||||
|
events[events.length - 1 - 2],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href={Pages.newFormEntry.url({ account })}
|
||||||
|
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
New AML form
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
|
||||||
|
<h1 class="text-base font-semibold leading-7 text-black">
|
||||||
|
Case history
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<div class="flow-root">
|
||||||
|
<ul role="list">
|
||||||
|
{events.map((e, idx) => {
|
||||||
|
const isLast = events.length - 1 === idx;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
class="hover:bg-gray-200 p-2 rounded cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="relative pb-6">
|
||||||
|
{!isLast ? (
|
||||||
|
<span
|
||||||
|
class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
) : undefined}
|
||||||
|
<div class="relative flex space-x-3">
|
||||||
|
{(() => {
|
||||||
|
switch (e.type) {
|
||||||
|
case "aml-form": {
|
||||||
|
switch (e.state) {
|
||||||
|
case AmlState.normal: {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Normal
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
|
||||||
|
{e.threshold.currency}{" "}
|
||||||
|
{Amounts.stringifyValue(e.threshold)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case AmlState.pending: {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
|
||||||
|
{e.threshold.currency}{" "}
|
||||||
|
{Amounts.stringifyValue(e.threshold)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case AmlState.frozen: {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Frozen
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
|
||||||
|
{e.threshold.currency}{" "}
|
||||||
|
{Amounts.stringifyValue(e.threshold)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "kyc-collection": {
|
||||||
|
return (
|
||||||
|
<ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "kyc-expiration": {
|
||||||
|
return <ClockIcon class="h-8 w-8 text-gray-700" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-900">{e.title}</p>
|
||||||
|
</div>
|
||||||
|
<div class="whitespace-nowrap text-right text-sm text-gray-500">
|
||||||
|
{e.when.t_ms === "never" ? (
|
||||||
|
"never"
|
||||||
|
) : (
|
||||||
|
<time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
|
||||||
|
{format(e.when.t_ms, "dd MMM yyyy")}
|
||||||
|
</time>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{selected && <ShowEventDetails event={selected} />}
|
||||||
|
{selected && <ShowConsolidated history={events} until={selected} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
|
||||||
|
return <div>type {event.type}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowConsolidated({
|
||||||
|
history,
|
||||||
|
until,
|
||||||
|
}: {
|
||||||
|
history: AmlEvent[];
|
||||||
|
until: AmlEvent;
|
||||||
|
}): VNode {
|
||||||
|
console.log("UNTIL", until);
|
||||||
|
const cons = getConsolidated(history, until.when);
|
||||||
|
|
||||||
|
const form: FlexibleForm<Consolidated> = {
|
||||||
|
versionId: "1",
|
||||||
|
behavior: (form) => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
design: [
|
||||||
|
{
|
||||||
|
title: "AML" as TranslatedString,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "amount",
|
||||||
|
props: {
|
||||||
|
label: "Threshold" as TranslatedString,
|
||||||
|
name: "aml.threshold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "choiceHorizontal",
|
||||||
|
props: {
|
||||||
|
label: "State" as TranslatedString,
|
||||||
|
name: "aml.state",
|
||||||
|
converter: amlStateConverter,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
label: "Frozen" as TranslatedString,
|
||||||
|
value: AmlState.frozen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pending" as TranslatedString,
|
||||||
|
value: AmlState.pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Normal" as TranslatedString,
|
||||||
|
value: AmlState.normal,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object.entries(cons.kyc).length > 0
|
||||||
|
? {
|
||||||
|
title: "KYC" as TranslatedString,
|
||||||
|
fields: Object.entries(cons.kyc).map(([key, field]) => {
|
||||||
|
const result: UIFormField = {
|
||||||
|
type: "text",
|
||||||
|
props: {
|
||||||
|
label: key as TranslatedString,
|
||||||
|
name: `kyc.${key}.value`,
|
||||||
|
help: `${field.provider} since ${
|
||||||
|
field.since.t_ms === "never"
|
||||||
|
? "never"
|
||||||
|
: format(field.since.t_ms, "dd/MM/yyyy")
|
||||||
|
}` as TranslatedString,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<h1 class="text-base font-semibold leading-7 text-black">
|
||||||
|
Consolidated information after{" "}
|
||||||
|
{until.when.t_ms === "never"
|
||||||
|
? "never"
|
||||||
|
: format(until.when.t_ms, "dd MMMM yyyy")}
|
||||||
|
</h1>
|
||||||
|
<NiceForm
|
||||||
|
key={`${String(Date.now())}`}
|
||||||
|
form={form}
|
||||||
|
initial={cons}
|
||||||
|
onUpdate={() => {}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Consolidated {
|
||||||
|
aml: {
|
||||||
|
state?: AmlState;
|
||||||
|
threshold?: AmountJson;
|
||||||
|
since: AbsoluteTime;
|
||||||
|
};
|
||||||
|
kyc: {
|
||||||
|
[field: string]: {
|
||||||
|
value: any;
|
||||||
|
provider: string;
|
||||||
|
since: AbsoluteTime;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConsolidated(
|
||||||
|
history: AmlEvent[],
|
||||||
|
when: AbsoluteTime,
|
||||||
|
): Consolidated {
|
||||||
|
const initial: Consolidated = {
|
||||||
|
aml: {
|
||||||
|
since: AbsoluteTime.never(),
|
||||||
|
},
|
||||||
|
kyc: {},
|
||||||
|
};
|
||||||
|
return history.reduce((prev, cur) => {
|
||||||
|
if (AbsoluteTime.cmp(when, cur.when) < 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
switch (cur.type) {
|
||||||
|
case "kyc-expiration": {
|
||||||
|
cur.fields.forEach((field) => {
|
||||||
|
delete prev.kyc[field];
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "aml-form": {
|
||||||
|
prev.aml.threshold = cur.threshold;
|
||||||
|
prev.aml.state = cur.state;
|
||||||
|
prev.aml.since = cur.when;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "kyc-collection": {
|
||||||
|
Object.keys(cur.values).forEach((field) => {
|
||||||
|
prev.kyc[field] = {
|
||||||
|
value: (cur.values as any)[field],
|
||||||
|
provider: cur.provider,
|
||||||
|
since: cur.when,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const amlStateConverter = {
|
||||||
|
toStringUI: stringifyAmlState,
|
||||||
|
fromStringUI: parseAmlState,
|
||||||
|
};
|
||||||
|
|
||||||
|
function stringifyAmlState(s: AmlState | undefined): string {
|
||||||
|
if (s === undefined) return "";
|
||||||
|
switch (s) {
|
||||||
|
case AmlState.normal:
|
||||||
|
return "normal";
|
||||||
|
case AmlState.pending:
|
||||||
|
return "pending";
|
||||||
|
case AmlState.frozen:
|
||||||
|
return "frozen";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAmlState(s: string | undefined): AmlState {
|
||||||
|
switch (s) {
|
||||||
|
case "normal":
|
||||||
|
return AmlState.normal;
|
||||||
|
case "pending":
|
||||||
|
return AmlState.pending;
|
||||||
|
case "frozen":
|
||||||
|
return AmlState.frozen;
|
||||||
|
default:
|
||||||
|
throw Error(`unknown AML state: ${s}`);
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,11 @@ import { v1 as form_902_1e_v1 } from "../forms/902_1e.js";
|
|||||||
import { v1 as form_902_4e_v1 } from "../forms/902_4e.js";
|
import { v1 as form_902_4e_v1 } from "../forms/902_4e.js";
|
||||||
import { v1 as form_902_5e_v1 } from "../forms/902_5e.js";
|
import { v1 as form_902_5e_v1 } from "../forms/902_5e.js";
|
||||||
import { v1 as form_902_9e_v1 } from "../forms/902_9e.js";
|
import { v1 as form_902_9e_v1 } from "../forms/902_9e.js";
|
||||||
|
import { v1 as simplest } from "../forms/simplest.js";
|
||||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||||
import { AbsoluteTime } from "@gnu-taler/taler-util";
|
import { AbsoluteTime } from "@gnu-taler/taler-util";
|
||||||
|
import { AmlState } from "../types.js";
|
||||||
|
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
||||||
const selectedForm = Number.parseInt(number ?? "0", 10);
|
const selectedForm = Number.parseInt(number ?? "0", 10);
|
||||||
@ -22,11 +25,28 @@ export function AntiMoneyLaunderingForm({ number }: { number?: string }) {
|
|||||||
when: AbsoluteTime.now(),
|
when: AbsoluteTime.now(),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<NiceForm initial={storedValue} form={showingFrom} onUpdate={() => {}} />
|
<NiceForm
|
||||||
|
initial={storedValue}
|
||||||
|
form={showingFrom({
|
||||||
|
state: AmlState.pending,
|
||||||
|
threshold: Amounts.parseOrThrow("USD:10"),
|
||||||
|
})}
|
||||||
|
onUpdate={() => {}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
state: AmlState;
|
||||||
|
threshold: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
export const allForms = [
|
export const allForms = [
|
||||||
|
{
|
||||||
|
name: "Simple comment",
|
||||||
|
icon: DocumentDuplicateIcon,
|
||||||
|
impl: simplest,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Identification form (902.1e)",
|
name: "Identification form (902.1e)",
|
||||||
icon: DocumentDuplicateIcon,
|
icon: DocumentDuplicateIcon,
|
||||||
|
282
packages/exchange-backoffice-ui/src/pages/Cases.tsx
Normal file
282
packages/exchange-backoffice-ui/src/pages/Cases.tsx
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import { VNode, h } from "preact";
|
||||||
|
import { Pages } from "../pages.js";
|
||||||
|
import { AmlRecords, AmlState } from "../types.js";
|
||||||
|
import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js";
|
||||||
|
import { createNewForm } from "../handlers/forms.js";
|
||||||
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { amlStateConverter as amlStateConverter } from "./AccountDetails.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
const response: AmlRecords = {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
current_state: 0,
|
||||||
|
h_payto: "QWEQWEQWEQWEWQE",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 1,
|
||||||
|
h_payto: "ASDASDASD",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 2,
|
||||||
|
h_payto: "ZXCZXCZXCXZC",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 0,
|
||||||
|
h_payto: "QWEQWEQWEQWEWQE",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 1,
|
||||||
|
h_payto: "ASDASDASD",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
current_state: 2,
|
||||||
|
h_payto: "ZXCZXCZXCXZC",
|
||||||
|
rowid: 1,
|
||||||
|
threshold: "USD 1000",
|
||||||
|
},
|
||||||
|
].map((e, idx) => {
|
||||||
|
e.rowid = idx;
|
||||||
|
e.threshold = `${e.threshold}${idx}`;
|
||||||
|
return e;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
function doFilter(
|
||||||
|
list: typeof response.records,
|
||||||
|
filter: AmlState | undefined,
|
||||||
|
): typeof response.records {
|
||||||
|
if (filter === undefined) return list;
|
||||||
|
return list.filter((r) => r.current_state === filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Cases() {
|
||||||
|
const form = createNewForm<{
|
||||||
|
state: AmlState;
|
||||||
|
}>();
|
||||||
|
const initial = { state: AmlState.pending };
|
||||||
|
const [list, setList] = useState(doFilter(response.records, initial.state));
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
Cases
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
|
A list of all the account with the status
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form.Provider
|
||||||
|
initialValue={initial}
|
||||||
|
onUpdate={(v) => {
|
||||||
|
setList(doFilter(response.records, v.state));
|
||||||
|
}}
|
||||||
|
onSubmit={(v) => {}}
|
||||||
|
>
|
||||||
|
<form.InputChoiceHorizontal
|
||||||
|
name="state"
|
||||||
|
label={"Filter" as TranslatedString}
|
||||||
|
converter={amlStateConverter}
|
||||||
|
choices={[
|
||||||
|
{
|
||||||
|
label: "Pending" as TranslatedString,
|
||||||
|
value: AmlState.pending,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Frozen" as TranslatedString,
|
||||||
|
value: AmlState.frozen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Normal" as TranslatedString,
|
||||||
|
value: AmlState.normal,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 flow-root">
|
||||||
|
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
|
<Pagination />
|
||||||
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Account Id
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Threshold
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
|
{list.map((r) => {
|
||||||
|
return (
|
||||||
|
<tr class="hover:bg-gray-100 ">
|
||||||
|
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
|
||||||
|
<div class="text-gray-900">
|
||||||
|
<a
|
||||||
|
href={Pages.details.url({ account: r.h_payto })}
|
||||||
|
class="text-indigo-600 hover:text-indigo-900"
|
||||||
|
>
|
||||||
|
{r.h_payto}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
|
||||||
|
{((state: AmlState): VNode => {
|
||||||
|
switch (state) {
|
||||||
|
case AmlState.normal: {
|
||||||
|
return (
|
||||||
|
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Normal
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case AmlState.pending: {
|
||||||
|
return (
|
||||||
|
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case AmlState.frozen: {
|
||||||
|
return (
|
||||||
|
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
|
||||||
|
Frozen
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(r.current_state)}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
|
||||||
|
{r.threshold}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<Pagination />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Pagination() {
|
||||||
|
return (
|
||||||
|
<nav class="flex items-center justify-between px-4 sm:px-0">
|
||||||
|
<div class="-mt-px flex w-0 flex-1">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent pr-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="mr-3 h-5 w-5 text-gray-400"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M18 10a.75.75 0 01-.75.75H4.66l2.1 1.95a.75.75 0 11-1.02 1.1l-3.5-3.25a.75.75 0 010-1.1l3.5-3.25a.75.75 0 111.02 1.1l-2.1 1.95h12.59A.75.75 0 0118 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:-mt-px md:flex">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
{/* <!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" --> */}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500"
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</a>
|
||||||
|
<span class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500">
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
8
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
9
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
10
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="-mt-px flex w-0 flex-1 justify-end">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="inline-flex items-center border-t-2 border-transparent pl-1 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<svg
|
||||||
|
class="ml-3 h-5 w-5 text-gray-400"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
import { h } from "preact";
|
|
||||||
|
|
||||||
export function Info() {
|
|
||||||
return <div>Show key and wire info</div>;
|
|
||||||
}
|
|
78
packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx
Normal file
78
packages/exchange-backoffice-ui/src/pages/NewFormEntry.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { VNode, h } from "preact";
|
||||||
|
import { allForms } from "./AntiMoneyLaunderingForm.js";
|
||||||
|
import { Pages } from "../pages.js";
|
||||||
|
import { NiceForm } from "../NiceForm.js";
|
||||||
|
import { AmlState } from "../types.js";
|
||||||
|
import { Amounts } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
|
export function NewFormEntry({
|
||||||
|
account,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
account?: string;
|
||||||
|
type?: string;
|
||||||
|
}): VNode {
|
||||||
|
if (!account) {
|
||||||
|
return <div>no account</div>;
|
||||||
|
}
|
||||||
|
if (!type) {
|
||||||
|
return <SelectForm account={account} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedForm = Number.parseInt(type ?? "0", 10);
|
||||||
|
if (Number.isNaN(selectedForm)) {
|
||||||
|
return <div>WHAT! {type}</div>;
|
||||||
|
}
|
||||||
|
const showingFrom = allForms[selectedForm].impl;
|
||||||
|
const initial = {
|
||||||
|
fullName: "loggedIn_user_fullname",
|
||||||
|
when: {
|
||||||
|
t_ms: new Date().getTime(),
|
||||||
|
},
|
||||||
|
state: AmlState.pending,
|
||||||
|
threshold: Amounts.parseOrThrow("USD:10"),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<NiceForm
|
||||||
|
initial={initial}
|
||||||
|
form={showingFrom(initial)}
|
||||||
|
onSubmit={(v) => {
|
||||||
|
alert(JSON.stringify(v));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="mt-6 flex items-center justify-end gap-x-6">
|
||||||
|
<a
|
||||||
|
// type="button"
|
||||||
|
href={Pages.details.url({ account })}
|
||||||
|
class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</NiceForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectForm({ account }: { account: string }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<pre>New form for account: {account}</pre>
|
||||||
|
{allForms.map((form, idx) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={Pages.newFormEntry.url({ account, type: String(idx) })}
|
||||||
|
class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600"
|
||||||
|
>
|
||||||
|
{form.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,69 +4,60 @@ import {
|
|||||||
notifyInfo,
|
notifyInfo,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
useMemoryStorage,
|
useMemoryStorage,
|
||||||
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { VNode, h } from "preact";
|
import { VNode, h } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
|
Account,
|
||||||
UnwrapKeyError,
|
UnwrapKeyError,
|
||||||
createNewAccount,
|
createNewAccount,
|
||||||
createNewSessionId,
|
|
||||||
unlockAccount,
|
unlockAccount,
|
||||||
} from "../account.js";
|
} from "../account.js";
|
||||||
import { createNewForm } from "../handlers/forms.js";
|
import { createNewForm } from "../handlers/forms.js";
|
||||||
|
import { Officer, codecForOfficer } from "../Dashboard.js";
|
||||||
|
|
||||||
export function Officer() {
|
export function Officer() {
|
||||||
const password = useMemoryStorage("password");
|
const password = useMemoryStorage("password");
|
||||||
const session = useLocalStorage("session");
|
const officer = useLocalStorage("officer", {
|
||||||
const officer = useLocalStorage("officer");
|
codec: codecForOfficer(),
|
||||||
const [keys, setKeys] = useState({ accountId: "", pub: "" });
|
});
|
||||||
|
const [keys, setKeys] = useState<Account>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (officer.value === undefined || password.value === undefined) {
|
||||||
officer.value === undefined ||
|
|
||||||
session.value === undefined ||
|
|
||||||
password.value === undefined
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unlockAccount(session.value, officer.value, password.value)
|
|
||||||
|
unlockAccount(officer.value.salt, officer.value.key, password.value)
|
||||||
.then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
|
.then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e instanceof UnwrapKeyError) {
|
if (e instanceof UnwrapKeyError) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [officer.value, session.value, password.value]);
|
}, [officer.value, password.value]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (
|
||||||
if (!session.value) {
|
officer.value === undefined ||
|
||||||
session.update(createNewSessionId());
|
!officer.value.key ||
|
||||||
}
|
!officer.value.salt
|
||||||
}, []);
|
) {
|
||||||
|
|
||||||
const { value: sessionId } = session;
|
|
||||||
if (!sessionId) {
|
|
||||||
return <div>loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (officer.value === undefined) {
|
|
||||||
return (
|
return (
|
||||||
<CreateAccount
|
<CreateAccount
|
||||||
sessionId={sessionId}
|
onNewAccount={(salt, key, pwd) => {
|
||||||
onNewAccount={(id) => {
|
password.update(pwd);
|
||||||
password.reset();
|
officer.update({ salt, when: { t_ms: Date.now() }, key });
|
||||||
officer.update(id);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("pwd", password.value);
|
|
||||||
if (password.value === undefined) {
|
if (password.value === undefined) {
|
||||||
return (
|
return (
|
||||||
<UnlockAccount
|
<UnlockAccount
|
||||||
sessionId={sessionId}
|
salt={officer.value.salt}
|
||||||
accountId={officer.value}
|
sealedKey={officer.value.key}
|
||||||
onAccountUnlocked={(pwd) => {
|
onAccountUnlocked={(pwd) => {
|
||||||
password.update(pwd);
|
password.update(pwd);
|
||||||
}}
|
}}
|
||||||
@ -76,42 +67,59 @@ export function Officer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Officer</div>
|
|
||||||
<h1>{sessionId}</h1>
|
|
||||||
<h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
|
<h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
|
||||||
Public key
|
Public key
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
|
||||||
<p class="mt-6 leading-8 text-gray-700 break-all">
|
<p class="mt-6 font-mono break-all">{keys?.accountId}</p>
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
<div>{keys.pub}</div>
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
|
|
||||||
Private key
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<p class="mt-6 leading-8 text-gray-700 break-all">
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
<div>{keys.accountId}</div>
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
|
||||||
|
`I want my AML account\n\n\nPubKey: ${keys?.accountId}`,
|
||||||
|
)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
Request account activation
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
password.reset();
|
||||||
|
}}
|
||||||
|
class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm "
|
||||||
|
>
|
||||||
|
Lock account
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
officer.reset();
|
||||||
|
}}
|
||||||
|
class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
|
||||||
|
>
|
||||||
|
Remove account
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateAccount({
|
function CreateAccount({
|
||||||
sessionId,
|
|
||||||
onNewAccount,
|
onNewAccount,
|
||||||
}: {
|
}: {
|
||||||
sessionId: string;
|
onNewAccount: (salt: string, accountId: string, password: string) => void;
|
||||||
onNewAccount: (accountId: string) => void;
|
|
||||||
}): VNode {
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
const Form = createNewForm<{
|
const Form = createNewForm<{
|
||||||
email: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
|
repeat: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -125,24 +133,50 @@ function CreateAccount({
|
|||||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
||||||
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
||||||
<Form.Provider
|
<Form.Provider
|
||||||
|
computeFormState={(v) => {
|
||||||
|
return {
|
||||||
|
password: {
|
||||||
|
error: !v.password
|
||||||
|
? i18n.str`required`
|
||||||
|
: v.password.length < 8
|
||||||
|
? i18n.str`should have at least 8 characters`
|
||||||
|
: !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
|
||||||
|
? i18n.str`should have lowercase and uppercase characters`
|
||||||
|
: !v.password.match(/\d/)
|
||||||
|
? i18n.str`should have numbers`
|
||||||
|
: !v.password.match(/[^a-zA-Z\d]/)
|
||||||
|
? i18n.str`should have at least one character which is not a number or letter`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
repeat: {
|
||||||
|
// error: !v.repeat
|
||||||
|
// ? i18n.str`required`
|
||||||
|
// // : v.repeat !== v.password
|
||||||
|
// // ? i18n.str`doesn't match`
|
||||||
|
// : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
onSubmit={async (v) => {
|
onSubmit={async (v) => {
|
||||||
const keys = await createNewAccount(sessionId, v.password);
|
const keys = await createNewAccount(v.password);
|
||||||
onNewAccount(keys.accountId);
|
onNewAccount(keys.salt, keys.accountId, v.password);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Form.InputLine
|
<Form.InputLine
|
||||||
label={"Email" as TranslatedString}
|
label={"Password" as TranslatedString}
|
||||||
name="email"
|
name="password"
|
||||||
type="email"
|
type="password"
|
||||||
|
help={
|
||||||
|
"lower and upper case letters, number and special character" as TranslatedString
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Form.InputLine
|
<Form.InputLine
|
||||||
label={"Password" as TranslatedString}
|
label={"Repeat password" as TranslatedString}
|
||||||
name="password"
|
name="repeat"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -164,17 +198,15 @@ function CreateAccount({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UnlockAccount({
|
function UnlockAccount({
|
||||||
sessionId,
|
salt,
|
||||||
accountId,
|
sealedKey,
|
||||||
onAccountUnlocked,
|
onAccountUnlocked,
|
||||||
}: {
|
}: {
|
||||||
sessionId: string;
|
salt: string;
|
||||||
accountId: string;
|
sealedKey: string;
|
||||||
onAccountUnlocked: (password: string) => void;
|
onAccountUnlocked: (password: string) => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const Form = createNewForm<{
|
const Form = createNewForm<{
|
||||||
sessionId: string;
|
|
||||||
accountId: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -182,34 +214,21 @@ function UnlockAccount({
|
|||||||
<div class="flex min-h-full flex-col ">
|
<div class="flex min-h-full flex-col ">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||||
Unlock account
|
Account locked
|
||||||
</h2>
|
</h2>
|
||||||
|
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||||
|
Your account is normally locked anytime you reload. To unlock type
|
||||||
|
your password again.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
|
||||||
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
|
||||||
<Form.Provider
|
<Form.Provider
|
||||||
initialValue={{
|
|
||||||
sessionId,
|
|
||||||
accountId:
|
|
||||||
accountId.substring(0, 6) +
|
|
||||||
"..." +
|
|
||||||
accountId.substring(accountId.length - 6),
|
|
||||||
}}
|
|
||||||
computeFormState={(v) => {
|
|
||||||
return {
|
|
||||||
accountId: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
sessionId: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
onSubmit={async (v) => {
|
onSubmit={async (v) => {
|
||||||
try {
|
try {
|
||||||
// test login
|
// test login
|
||||||
await unlockAccount(sessionId, accountId, v.password);
|
await unlockAccount(salt, sealedKey, v.password);
|
||||||
|
|
||||||
onAccountUnlocked(v.password ?? "");
|
onAccountUnlocked(v.password ?? "");
|
||||||
notifyInfo("Account unlocked" as TranslatedString);
|
notifyInfo("Account unlocked" as TranslatedString);
|
||||||
@ -225,21 +244,6 @@ function UnlockAccount({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mb-4">
|
|
||||||
<Form.InputLine
|
|
||||||
label={"Session" as TranslatedString}
|
|
||||||
name="sessionId"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<Form.InputLine
|
|
||||||
label={"AccountId" as TranslatedString}
|
|
||||||
name="accountId"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Form.InputLine
|
<Form.InputLine
|
||||||
label={"Password" as TranslatedString}
|
label={"Password" as TranslatedString}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createHashHistory } from "history";
|
import { createHashHistory } from "history";
|
||||||
import { VNode } from "preact";
|
import { h as create, VNode } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
const history = createHashHistory();
|
const history = createHashHistory();
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ export function Router({
|
|||||||
}): VNode {
|
}): VNode {
|
||||||
const current = useCurrentLocation(pageList);
|
const current = useCurrentLocation(pageList);
|
||||||
if (current !== undefined) {
|
if (current !== undefined) {
|
||||||
return current.page.view(current.values ?? {});
|
return create(current.page.view, current.values);
|
||||||
}
|
}
|
||||||
return onNotFound();
|
return onNotFound();
|
||||||
}
|
}
|
||||||
|
81
packages/exchange-backoffice-ui/src/types.ts
Normal file
81
packages/exchange-backoffice-ui/src/types.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
export interface AmlDecisionDetails {
|
||||||
|
// Array of AML decisions made for this account. Possibly
|
||||||
|
// contains only the most recent decision if "history" was
|
||||||
|
// not set to 'true'.
|
||||||
|
aml_history: AmlDecisionDetail[];
|
||||||
|
|
||||||
|
// Array of KYC attributes obtained for this account.
|
||||||
|
kyc_attributes: KycDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmlOfficerPublicKeyP = string;
|
||||||
|
|
||||||
|
export interface AmlDecisionDetail {
|
||||||
|
// What was the justification given?
|
||||||
|
justification: string;
|
||||||
|
|
||||||
|
// What is the new AML state.
|
||||||
|
new_state: Integer;
|
||||||
|
|
||||||
|
// When was this decision made?
|
||||||
|
decision_time: Timestamp;
|
||||||
|
|
||||||
|
// What is the new AML decision threshold (in monthly transaction volume)?
|
||||||
|
new_threshold: Amount;
|
||||||
|
|
||||||
|
// Who made the decision?
|
||||||
|
decider_pub: AmlOfficerPublicKeyP;
|
||||||
|
}
|
||||||
|
export interface KycDetail {
|
||||||
|
// Name of the configuration section that specifies the provider
|
||||||
|
// which was used to collect the KYC details
|
||||||
|
provider_section: string;
|
||||||
|
|
||||||
|
// The collected KYC data. NULL if the attribute data could not
|
||||||
|
// be decrypted (internal error of the exchange, likely the
|
||||||
|
// attribute key was changed).
|
||||||
|
attributes?: Object;
|
||||||
|
|
||||||
|
// Time when the KYC data was collected
|
||||||
|
collection_time: Timestamp;
|
||||||
|
|
||||||
|
// Time when the validity of the KYC data will expire
|
||||||
|
expiration_time: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Timestamp {
|
||||||
|
// Seconds since epoch, or the special
|
||||||
|
// value "never" to represent an event that will
|
||||||
|
// never happen.
|
||||||
|
t_s: number | "never";
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaytoHash = string;
|
||||||
|
type Integer = number;
|
||||||
|
type Amount = string;
|
||||||
|
|
||||||
|
export interface AmlRecords {
|
||||||
|
// Array of AML records matching the query.
|
||||||
|
records: AmlRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AmlRecord {
|
||||||
|
// Which payto-address is this record about.
|
||||||
|
// Identifies a GNU Taler wallet or an affected bank account.
|
||||||
|
h_payto: PaytoHash;
|
||||||
|
|
||||||
|
// What is the current AML state.
|
||||||
|
current_state: AmlState;
|
||||||
|
|
||||||
|
// Monthly transaction threshold before a review will be triggered
|
||||||
|
threshold: Amount;
|
||||||
|
|
||||||
|
// RowID of the record.
|
||||||
|
rowid: Integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AmlState {
|
||||||
|
normal = 0,
|
||||||
|
pending = 1,
|
||||||
|
frozen = 2,
|
||||||
|
}
|
@ -24,6 +24,6 @@ function getBrowserLang(): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLang(initial?: string): Required<LocalStorageState> {
|
export function useLang(initial?: string): Required<LocalStorageState> {
|
||||||
const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
|
const defaultValue = (getBrowserLang() || initial || "en").substring(0, 2);
|
||||||
return useLocalStorage("lang-preference", defaultLang);
|
return useLocalStorage("lang-preference", { defaultValue: defaultValue });
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Codec } from "@gnu-taler/taler-util";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
ObservableMap,
|
ObservableMap,
|
||||||
@ -27,9 +28,9 @@ import {
|
|||||||
memoryMap,
|
memoryMap,
|
||||||
} from "../utils/observable.js";
|
} from "../utils/observable.js";
|
||||||
|
|
||||||
export interface LocalStorageState {
|
export interface LocalStorageState<Type = string> {
|
||||||
value?: string;
|
value?: Type;
|
||||||
update: (s: string) => void;
|
update: (s: Type) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,33 +48,62 @@ const storage: ObservableMap<string, string> = (function buildStorage() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export function useLocalStorage(
|
//with initial value
|
||||||
|
export function useLocalStorage<Type = string>(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue: string,
|
options?: {
|
||||||
): Required<LocalStorageState>;
|
defaultValue: Type;
|
||||||
export function useLocalStorage(key: string): LocalStorageState;
|
codec?: Codec<Type>;
|
||||||
export function useLocalStorage(
|
},
|
||||||
|
): Required<LocalStorageState<Type>>;
|
||||||
|
//without initial value
|
||||||
|
export function useLocalStorage<Type = string>(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue?: string,
|
options?: {
|
||||||
): LocalStorageState {
|
codec?: Codec<Type>;
|
||||||
const [storedValue, setStoredValue] = useState<string | undefined>(
|
},
|
||||||
(): string | undefined => {
|
): LocalStorageState<Type>;
|
||||||
return storage.get(key) ?? initialValue;
|
// impl
|
||||||
|
export function useLocalStorage<Type = string>(
|
||||||
|
key: string,
|
||||||
|
options?: {
|
||||||
|
defaultValue?: Type;
|
||||||
|
codec?: Codec<Type>;
|
||||||
|
},
|
||||||
|
): LocalStorageState<Type> {
|
||||||
|
function convert(updated: string | undefined): Type | undefined {
|
||||||
|
if (updated === undefined) return options?.defaultValue; //optional
|
||||||
|
try {
|
||||||
|
return !options?.codec
|
||||||
|
? (updated as Type)
|
||||||
|
: options.codec.decode(JSON.parse(updated));
|
||||||
|
} catch (e) {
|
||||||
|
//decode error
|
||||||
|
return options?.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [storedValue, setStoredValue] = useState<Type | undefined>(
|
||||||
|
(): Type | undefined => {
|
||||||
|
const prev = storage.get(key);
|
||||||
|
return convert(prev);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return storage.onUpdate(key, () => {
|
return storage.onUpdate(key, () => {
|
||||||
const newValue = storage.get(key);
|
const newValue = storage.get(key);
|
||||||
setStoredValue(newValue ?? initialValue);
|
setStoredValue(convert(newValue));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setValue = (value?: string): void => {
|
const setValue = (value?: Type): void => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
storage.delete(key);
|
storage.delete(key);
|
||||||
} else {
|
} else {
|
||||||
storage.set(key, value);
|
storage.set(
|
||||||
|
key,
|
||||||
|
options?.codec ? JSON.stringify(value) : (value as string),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +111,7 @@ export function useLocalStorage(
|
|||||||
value: storedValue,
|
value: storedValue,
|
||||||
update: setValue,
|
update: setValue,
|
||||||
reset: () => {
|
reset: () => {
|
||||||
setValue(initialValue);
|
setValue(options?.defaultValue);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user