From c680f5aa71b08e978444df07f93c381f9d47ab82 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 5 Jun 2023 10:04:09 -0300 Subject: rename aml --- .../src/pages/AntiMoneyLaunderingForm.tsx | 90 +++++ .../aml-backoffice-ui/src/pages/CaseDetails.tsx | 447 +++++++++++++++++++++ packages/aml-backoffice-ui/src/pages/Cases.tsx | 288 +++++++++++++ .../aml-backoffice-ui/src/pages/CreateAccount.tsx | 102 +++++ .../src/pages/HandleAccountNotReady.tsx | 34 ++ packages/aml-backoffice-ui/src/pages/Home.tsx | 5 + .../aml-backoffice-ui/src/pages/NewFormEntry.tsx | 76 ++++ packages/aml-backoffice-ui/src/pages/Officer.tsx | 55 +++ packages/aml-backoffice-ui/src/pages/Settings.tsx | 5 + .../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 81 ++++ packages/aml-backoffice-ui/src/pages/Welcome.tsx | 9 + 11 files changed, 1192 insertions(+) create mode 100644 packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/CaseDetails.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/Cases.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/CreateAccount.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/Home.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/Officer.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/Settings.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx create mode 100644 packages/aml-backoffice-ui/src/pages/Welcome.tsx (limited to 'packages/aml-backoffice-ui/src/pages') diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx new file mode 100644 index 000000000..713c0d7c1 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx @@ -0,0 +1,90 @@ +import { h } from "preact"; +import { NiceForm } from "../NiceForm.js"; +import { v1 as form_902_11e_v1 } from "../forms/902_11e.js"; +import { v1 as form_902_12e_v1 } from "../forms/902_12e.js"; +import { v1 as form_902_13e_v1 } from "../forms/902_13e.js"; +import { v1 as form_902_15e_v1 } from "../forms/902_15e.js"; +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_5e_v1 } from "../forms/902_5e.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 { 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 }) { + const selectedForm = Number.parseInt(number ?? "0", 10); + if (Number.isNaN(selectedForm)) { + return
WHAT! {number}
; + } + const showingFrom = allForms[selectedForm].impl; + const storedValue = { + fullName: "loggedIn_user_fullname", + when: AbsoluteTime.now(), + }; + return ( + {}} + /> + ); +} + +export interface State { + state: AmlState; + threshold: AmountJson; +} + +export const allForms = [ + { + name: "Simple comment", + icon: DocumentDuplicateIcon, + impl: simplest, + }, + { + name: "Identification form (902.1e)", + icon: DocumentDuplicateIcon, + impl: form_902_1e_v1, + }, + { + name: "Operational legal entity or partnership (902.11e)", + icon: DocumentDuplicateIcon, + impl: form_902_11e_v1, + }, + { + name: "Foundations (902.12e)", + icon: DocumentDuplicateIcon, + impl: form_902_12e_v1, + }, + { + name: "Declaration for trusts (902.13e)", + icon: DocumentDuplicateIcon, + impl: form_902_13e_v1, + }, + { + name: "Information on life insurance policies (902.15e)", + icon: DocumentDuplicateIcon, + impl: form_902_15e_v1, + }, + { + name: "Declaration of beneficial owner (902.9e)", + icon: DocumentDuplicateIcon, + impl: form_902_9e_v1, + }, + { + name: "Customer profile (902.5e)", + icon: DocumentDuplicateIcon, + impl: form_902_5e_v1, + }, + { + name: "Risk profile (902.4e)", + icon: DocumentDuplicateIcon, + impl: form_902_4e_v1, + }, +]; diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx new file mode 100644 index 000000000..e5fb8eaba --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -0,0 +1,447 @@ +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: AbsoluteTime.fromProtocolTimestamp(k.collection_time), + values: !k.attributes ? {} : k.attributes, + provider: k.provider_section, + }); + prev.push({ + type: "kyc-expiration", + title: "expired" as TranslatedString, + when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time), + fields: !k.attributes ? [] : Object.keys(k.attributes), + }); + return prev; + }, [] as AmlEvent[]); + return ae.concat(ke).sort(selectSooner); +} + +export function CaseDetails({ 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( + events[events.length - 1 - 2], + ); + return ( +
+ + New AML form + + +
+

+ Case history +

+
+
+
    + {events.map((e, idx) => { + const isLast = events.length - 1 === idx; + return ( +
  • { + setSelected(e); + }} + > +
    + {!isLast ? ( + + ) : undefined} +
    + {(() => { + switch (e.type) { + case "aml-form": { + switch (e.state) { + case AmlState.normal: { + return ( +
    + + Normal + + + {e.threshold.currency}{" "} + {Amounts.stringifyValue(e.threshold)} + +
    + ); + } + case AmlState.pending: { + return ( +
    + + Pending + + + {e.threshold.currency}{" "} + {Amounts.stringifyValue(e.threshold)} + +
    + ); + } + case AmlState.frozen: { + return ( +
    + + Frozen + + + {e.threshold.currency}{" "} + {Amounts.stringifyValue(e.threshold)} + +
    + ); + } + } + } + case "kyc-collection": { + return ( + + ); + } + case "kyc-expiration": { + return ; + } + } + })()} +
    +
    +

    {e.title}

    +
    +
    + {e.when.t_ms === "never" ? ( + "never" + ) : ( + + )} +
    +
    +
    +
    +
  • + ); + })} +
+
+ {selected && } + {selected && } +
+ ); +} + +function ShowEventDetails({ event }: { event: AmlEvent }): VNode { + return
type {event.type}
; +} + +function ShowConsolidated({ + history, + until, +}: { + history: AmlEvent[]; + until: AmlEvent; +}): VNode { + console.log("UNTIL", until); + const cons = getConsolidated(history, until.when); + + const form: FlexibleForm = { + 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 ( + +

+ Consolidated information after{" "} + {until.when.t_ms === "never" + ? "never" + : format(until.when.t_ms, "dd MMMM yyyy")} +

+ {}} + /> +
+ ); +} + +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}`); + } +} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx new file mode 100644 index 000000000..28b9d2a88 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -0,0 +1,288 @@ +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 "./CaseDetails.js"; +import { useState } from "preact/hooks"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { useOfficer } from "../hooks/useOfficer.js"; + +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 officer = useOfficer(); + if (officer.state !== "ready") { + return ; + } + const form = createNewForm<{ + state: AmlState; + }>(); + const initial = { state: AmlState.pending }; + const [list, setList] = useState(doFilter(response.records, initial.state)); + return ( +
+
+
+
+

+ Cases +

+

+ A list of all the account with the status +

+
+ { + setList(doFilter(response.records, v.state)); + }} + onSubmit={(v) => {}} + > + + +
+
+
+
+ + + + + + + + + + + {list.map((r) => { + return ( + + + + + + ); + })} + +
+ Account Id + + Status + + Threshold +
+ + + {((state: AmlState): VNode => { + switch (state) { + case AmlState.normal: { + return ( + + Normal + + ); + } + case AmlState.pending: { + return ( + + Pending + + ); + } + case AmlState.frozen: { + return ( + + Frozen + + ); + } + } + })(r.current_state)} + + {r.threshold} +
+ +
+
+
+
+
+ ); +} + +function Pagination() { + return ( + + ); +} diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx new file mode 100644 index 000000000..5dcb8b21d --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -0,0 +1,102 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { + notifyError, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { createNewForm } from "../handlers/forms.js"; + +export function CreateAccount({ + onNewAccount, +}: { + onNewAccount: (password: string) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const Form = createNewForm<{ + password: string; + repeat: string; + }>(); + + return ( +
+
+

+ Create account +

+
+ +
+
+ { + 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, s) => { + console.log(v, s); + const error = s?.password?.error ?? s?.repeat?.error; + console.log(error); + if (error) { + notifyError( + "Can't create account" as TranslatedString, + error as TranslatedString, + ); + } else { + onNewAccount(v.password!); + } + }} + > +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx new file mode 100644 index 000000000..05fd0a019 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx @@ -0,0 +1,34 @@ +import { VNode, h } from "preact"; +import { OfficerNotReady } from "../hooks/useOfficer.js"; +import { CreateAccount } from "./CreateAccount.js"; +import { UnlockAccount } from "./UnlockAccount.js"; + +export function HandleAccountNotReady({ + officer, +}: { + officer: OfficerNotReady; +}): VNode { + if (officer.state === "not-found") { + return ( + { + officer.create(password); + }} + /> + ); + } + + if (officer.state === "locked") { + return ( + { + officer.forget(); + }} + onAccountUnlocked={(pwd) => { + officer.tryUnlock(pwd); + }} + /> + ); + } + throw Error(`unexpected account state ${(officer as any).state}`); +} diff --git a/packages/aml-backoffice-ui/src/pages/Home.tsx b/packages/aml-backoffice-ui/src/pages/Home.tsx new file mode 100644 index 000000000..838032d63 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Home.tsx @@ -0,0 +1,5 @@ +import { h } from "preact"; + +export function Home() { + return
Home
; +} diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx new file mode 100644 index 000000000..fdb255701 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx @@ -0,0 +1,76 @@ +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 { AbsoluteTime, Amounts } from "@gnu-taler/taler-util"; + +export function NewFormEntry({ + account, + type, +}: { + account?: string; + type?: string; +}): VNode { + if (!account) { + return
no account
; + } + if (!type) { + return ; + } + + const selectedForm = Number.parseInt(type ?? "0", 10); + if (Number.isNaN(selectedForm)) { + return
WHAT! {type}
; + } + const showingFrom = allForms[selectedForm].impl; + const initial = { + fullName: "loggedIn_user_fullname", + when: AbsoluteTime.now(), + state: AmlState.pending, + threshold: Amounts.parseOrThrow("USD:10"), + }; + return ( + { + alert(JSON.stringify(v)); + }} + > +
+ + Cancel + + +
+
+ ); +} + +function SelectForm({ account }: { account: string }) { + return ( +
+
New form for account: {account}
+ {allForms.map((form, idx) => { + return ( + + {form.name} + + ); + })} +
+ ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx new file mode 100644 index 000000000..5320369e4 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx @@ -0,0 +1,55 @@ +import { Fragment, h } from "preact"; +import { useOfficer } from "../hooks/useOfficer.js"; +import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; + +export function Officer() { + const officer = useOfficer(); + if (officer.state !== "ready") { + return ; + } + + return ( +
+

+ Public key +

+
+

{officer.account.accountId}

+
+

+ + Request account activation + +

+

+ +

+

+ +

+
+ ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Settings.tsx b/packages/aml-backoffice-ui/src/pages/Settings.tsx new file mode 100644 index 000000000..ccff3b210 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Settings.tsx @@ -0,0 +1,5 @@ +import { h } from "preact"; + +export function Settings() { + return
Settings
; +} diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx new file mode 100644 index 000000000..2ebac0718 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -0,0 +1,81 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { UnwrapKeyError } from "../account.js"; +import { createNewForm } from "../handlers/forms.js"; + +export function UnlockAccount({ + onAccountUnlocked, + onRemoveAccount, +}: { + onAccountUnlocked: (password: string) => void; + onRemoveAccount: () => void; +}): VNode { + const Form = createNewForm<{ + password: string; + }>(); + + return ( +
+
+

+ Account locked +

+

+ Your account is normally locked anytime you reload. To unlock type + your password again. +

+
+ +
+
+ { + try { + await onAccountUnlocked(v.password!); + + notifyInfo("Account unlocked" as TranslatedString); + } catch (e) { + if (e instanceof UnwrapKeyError) { + notifyError( + "Could not unlock account" as any, + e.message as any, + ); + } else { + throw e; + } + } + }} + > +
+ +
+ +
+ +
+
+
+ +
+
+ ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Welcome.tsx b/packages/aml-backoffice-ui/src/pages/Welcome.tsx new file mode 100644 index 000000000..433fbcf59 --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Welcome.tsx @@ -0,0 +1,9 @@ +import { h } from "preact"; + +export function Welcome({ name, asd }: { asd?: string; name?: string }) { + return ( +
+ {asd} Hello {name} +
+ ); +} -- cgit v1.2.3