taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 318d7ba24624b5840bb9a838ede520cca0e0aa27
parent 8e02b885e3a8f3a39577548e6bd0200a625c870e
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon,  6 Jan 2025 19:45:28 -0300

several changes related UI

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 6++++--
Mpackages/aml-backoffice-ui/src/forms/ganaForms.ts | 5+----
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 148++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mpackages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/Cases.tsx | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 47+++++++++++++++++++++++++++++------------------
Mpackages/aml-backoffice-ui/src/stories.test.ts | 4+++-
Mpackages/aml-backoffice-ui/src/stories.tsx | 3++-
9 files changed, 399 insertions(+), 103 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -163,10 +163,12 @@ function PrivateRouting(): VNode { return <SelectForm account={location.values.cid} />; } case "investigation": { - return <CasesUnderInvestigation />; + return ( + <CasesUnderInvestigation caseByIdRoute={privatePages.caseDetails} /> + ); } case "active": { - return <Cases />; + return <Cases caseByIdRoute={privatePages.caseDetails} />; } case "search": { return <Search />; diff --git a/packages/aml-backoffice-ui/src/forms/ganaForms.ts b/packages/aml-backoffice-ui/src/forms/ganaForms.ts @@ -230,10 +230,7 @@ function convertGanaJsonToDoubleColumnFormSection( }, {} as FieldsBySection); Object.values(sections).forEach((sec) => { - sec.sort((a, b) => { - console.log("a", a.order, b.order); - return (a.order ?? 0) - (b.order ?? 0); - }); + sec.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); }); return sections; } diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -39,6 +39,7 @@ import { Attention, Button, convertUiField, + CopyButton, DefaultForm, FormConfiguration, FormMetadata, @@ -178,7 +179,7 @@ export function getEventsFromAmlHistory( const ke = events.map((event) => { return { type: "kyc-collection", - title: i18n.str`collection`, + title: i18n.str`User filled a form`, when: AbsoluteTime.fromProtocolTimestamp(event.collection_time), values: !event.attributes ? {} : event.attributes, provider: event.provider_name, @@ -274,7 +275,7 @@ export function CaseDetails({ expiration_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.never(), ), - rules: FREEZE_RULES(config.currency), + rules: FREEZE_RULES(config.config.currency), }, }, askInformation: false, @@ -298,11 +299,10 @@ export function CaseDetails({ <div class="min-w-60"> <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"> - <i18n.Translate> - Case history for account{" "} - <span title={account}>{account.substring(0, 16)}...</span> - </i18n.Translate> + <i18n.Translate>Case history for account: </i18n.Translate> </h1> + <div>{account}</div> + <CopyButton class="" getContent={() => account} /> </header> {!activeDecision || !activeDecision.to_investigate ? undefined : ( @@ -331,7 +331,7 @@ export function CaseDetails({ expiration_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.never(), ), - rules: FREEZE_RULES(config.currency), + rules: FREEZE_RULES(config.config.currency), successor_measure: "verboten", }, }, @@ -358,7 +358,7 @@ export function CaseDetails({ expiration_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.never(), ), - rules: THRESHOLD_100_HOUR(config.currency), + rules: THRESHOLD_100_HOUR(config.config.currency), successor_measure: "verboten", }, }, @@ -385,7 +385,7 @@ export function CaseDetails({ expiration_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.never(), ), - rules: THRESHOLD_2000_WEEK(config.currency), + rules: THRESHOLD_2000_WEEK(config.config.currency), successor_measure: "verboten", }, }, @@ -415,7 +415,7 @@ export function CaseDetails({ expiration_time: AbsoluteTime.toProtocolTimestamp( AbsoluteTime.never(), ), - rules: FREEZE_RULES(config.currency), + rules: FREEZE_RULES(config.config.currency), }, }, askInformation: true, @@ -471,20 +471,8 @@ export function CaseDetails({ ) : ( <ShowTimeline history={events} - onSelect={(e) => { - switch (e.type) { - case "aml-form": { - // const { justification, metadata } = e; - // setShowForm({ justification, metadata }); - break; - } - case "kyc-collection": - case "kyc-expiration": { - setSelected(e.when); - break; - } - case "aml-form-error": - } + onSelect={(time) => { + setSelected(time); }} /> )} @@ -724,6 +712,7 @@ function SubmitNewDecision({ /> <ShowDecisionLimitInfo + fixed since={AbsoluteTime.fromProtocolTimestamp( decision.request.decision_time, )} @@ -855,12 +844,14 @@ function ShowDecisionLimitInfo({ until, startOpen, justification, + fixed, }: { since: AbsoluteTime; until: AbsoluteTime; justification?: string; ruleSet: LegitimizationRuleSet; startOpen?: boolean; + fixed?: boolean; }): VNode { const { i18n } = useTranslationContext(); const { config } = useExchangeApiContext(); @@ -869,32 +860,74 @@ function ShowDecisionLimitInfo({ function Header() { return ( <div - class="p-4 relative bg-gray-50 flex justify-between cursor-pointer" - onClick={() => setOpened((o) => !o)} + data-fixed={!!fixed} + class="p-4 relative bg-gray-200 flex justify-between data-[fixed=false]:cursor-pointer" + onClick={() => { + if (!fixed) { + setOpened((o) => !o); + } + }} > <div class="flex min-w-0 gap-x-4"> <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3"> + <div class="pointer-events-none bg-gray-300 inset-y-0 flex items-center px-3"> <i18n.Translate>Since</i18n.Translate> </div> - <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"> + <div class="p-2 bg-gray-50 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-50 sm:text-sm sm:leading-6"> <Time format="dd/MM/yyyy HH:mm:ss" timestamp={since} /> </div> </div> </div> <div class="flex shrink-0 items-center gap-x-4"> <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3"> + <div class="pointer-events-none bg-gray-300 inset-y-0 flex items-center px-3"> {AbsoluteTime.isExpired(until) ? ( <i18n.Translate>Expired</i18n.Translate> ) : ( <i18n.Translate>Expires</i18n.Translate> )} </div> - <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"> + <div class="p-2 bg-gray-50 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-50 sm:text-sm sm:leading-6"> <Time format="dd/MM/yyyy HH:mm:ss" timestamp={until} /> </div> </div> + {fixed ? ( + <Fragment /> + ) : ( + <div class="rounded-full bg-gray-50 p-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + {opened ? ( + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m19.5 8.25-7.5 7.5-7.5-7.5" + /> + ) : ( + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m4.5 15.75 7.5-7.5 7.5 7.5" + /> + )} + </svg> + {/* <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + </svg> */} + </div> + )} </div> </div> ); @@ -902,7 +935,7 @@ function ShowDecisionLimitInfo({ if (!opened) { return ( - <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl"> + <div class="overflow-hidden border border-gray-800 rounded-xl"> <Header /> </div> ); @@ -912,7 +945,7 @@ function ShowDecisionLimitInfo({ ); return ( - <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl"> + <div class="overflow-hidden border border-gray-800 rounded-xl"> <Header /> <div class="p-4 grid gap-y-4"> {!justification ? undefined : ( @@ -948,7 +981,7 @@ function ShowDecisionLimitInfo({ ) : ( <RenderAmount value={Amounts.parseOrThrow(balanceLimit.threshold)} - spec={config.currency_specification} + spec={config.config.currency_specification} /> )} </div> @@ -1008,7 +1041,7 @@ function ShowDecisionLimitInfo({ <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 text-right"> <RenderAmount value={Amounts.parseOrThrow(r.threshold)} - spec={config.currency_specification} + spec={config.config.currency_specification} /> </td> </tr> @@ -1086,30 +1119,26 @@ function ShowTimeline({ history, onSelect, }: { - onSelect: (e: AmlEvent) => void; + onSelect: (e: AbsoluteTime) => void; history: AmlEvent[]; }): VNode { + const { i18n } = useTranslationContext(); return ( <div class="flow-root"> <ul role="list"> {history.map((e, idx) => { - const isLast = history.length - 1 === idx; + // const isLast = history.length - 1 === idx; return ( <li key={idx} data-ok={e.type !== "aml-form-error"} class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer" onClick={() => { - onSelect(e); + onSelect(e.when); }} > - <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 pb-3"> + <span class="absolute left-3 top-5 -ml-px h-full w-1 bg-gray-200"></span> <div class="relative flex space-x-3"> {(() => { switch (e.type) { @@ -1192,6 +1221,32 @@ function ShowTimeline({ </li> ); })} + <li + class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer" + onClick={() => { + onSelect(AbsoluteTime.now()); + }} + > + <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + </svg> + <p class="text-sm text-gray-900"> + <i18n.Translate>Now </i18n.Translate> + </p> + </div> + </li> </ul> </div> ); @@ -1240,12 +1295,13 @@ function InputAmount( if ( sep_pos !== -1 && l - sep_pos - 1 > - config.currency_specification.num_fractional_input_digits + config.config.currency_specification.num_fractional_input_digits ) { e.currentTarget.value = e.currentTarget.value.substring( 0, sep_pos + - config.currency_specification.num_fractional_input_digits + + config.config.currency_specification + .num_fractional_input_digits + 1, ); } diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -94,7 +94,7 @@ export function CaseUpdate({ const initial: FormType = { when: AbsoluteTime.now(), state: TalerExchangeApi.AmlState.pending, - threshold: Amounts.zeroOfCurrency(config.currency), + threshold: Amounts.zeroOfCurrency(config.config.currency), comment: "", }; diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -22,6 +22,7 @@ import { import { Attention, Loading, + RouteDefinition, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -33,18 +34,66 @@ import { import { privatePages } from "../Routing.js"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { Officer } from "./Officer.js"; +import { useState } from "preact/hooks"; type FormType = { // state: TalerExchangeApi.AmlState; }; +function JumpByIdForm({ + caseByIdRoute, +}: { + caseByIdRoute: RouteDefinition<{ cid: string }>; +}): VNode { + const { i18n } = useTranslationContext(); + const [account, setAccount] = useState<string>(""); + return ( + <form class="mt-5 sm:flex sm:items-center"> + <div class="w-full sm:max-w-xs"> + <input + type="email" + name="email" + id="email" + onChange={(e) => { + setAccount(e.currentTarget.value); + }} + aria-label="Email" + class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" + placeholder={i18n.str`Search by ID`} + /> + </div> + <a + href={caseByIdRoute.url({ cid: account })} + class="mt-3 inline-flex w-full items-center justify-center 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 sm:ml-3 sm:mt-0 sm:w-auto" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" + /> + </svg> + </a> + </form> + ); +} + export function CasesUI({ records, onFirstPage, onNext, filtered, + caseByIdRoute, }: { filtered: boolean; + caseByIdRoute: RouteDefinition<{ cid: string }>; onFirstPage?: () => void; onNext?: () => void; records: TalerExchangeApi.AmlDecision[]; @@ -113,6 +162,8 @@ export function CasesUI({ </p> </div> )} + + <JumpByIdForm caseByIdRoute={caseByIdRoute} /> </div> <div class="mt-8 flow-root"> <div class="overflow-x-auto"> @@ -147,9 +198,9 @@ export function CasesUI({ href={privatePages.caseDetails.url({ cid: r.h_payto, })} - class="text-indigo-600 hover:text-indigo-900" + class="text-indigo-600 hover:text-indigo-900 font-mono" > - {r.h_payto.substring(0, 16)}... + {r.h_payto} </a> </div> </td> @@ -174,7 +225,11 @@ export function CasesUI({ ); } -export function Cases() { +export function Cases({ + caseByIdRoute, +}: { + caseByIdRoute: RouteDefinition<{ cid: string }>; +}) { const list = useCurrentDecisions(); const { i18n } = useTranslationContext(); @@ -234,6 +289,7 @@ export function Cases() { <CasesUI filtered={false} records={list.body} + caseByIdRoute={caseByIdRoute} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} // filter={stateFilter} @@ -243,7 +299,11 @@ export function Cases() { /> ); } -export function CasesUnderInvestigation() { +export function CasesUnderInvestigation({ + caseByIdRoute, +}: { + caseByIdRoute: RouteDefinition<{ cid: string }>; +}) { const list = useCurrentDecisionsUnderInvestigation(); const { i18n } = useTranslationContext(); @@ -303,6 +363,7 @@ export function CasesUnderInvestigation() { <CasesUI filtered={true} records={list.body} + caseByIdRoute={caseByIdRoute} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} // filter={stateFilter} @@ -370,14 +431,18 @@ export const HomeIcon = () => ( </svg> ); export const FormIcon = () => ( -<svg -xmlns="http://www.w3.org/2000/svg" -viewBox="0 0 24 24" -fill="currentColor" -class="w-6 h-6" -> - <path fillRule="evenodd" d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625ZM21 9.375A.375.375 0 0 0 20.625 9h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5ZM10.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5ZM3.375 15h7.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h7.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 10.875 9h-7.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Z" clipRule="evenodd" /> -</svg> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-6 h-6" + > + <path + fillRule="evenodd" + d="M1.5 5.625c0-1.036.84-1.875 1.875-1.875h17.25c1.035 0 1.875.84 1.875 1.875v12.75c0 1.035-.84 1.875-1.875 1.875H3.375A1.875 1.875 0 0 1 1.5 18.375V5.625ZM21 9.375A.375.375 0 0 0 20.625 9h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5Zm0 3.75a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5a.375.375 0 0 0 .375-.375v-1.5ZM10.875 18.75a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375h7.5ZM3.375 15h7.5a.375.375 0 0 0 .375-.375v-1.5a.375.375 0 0 0-.375-.375h-7.5a.375.375 0 0 0-.375.375v1.5c0 .207.168.375.375.375Zm0-3.75h7.5a.375.375 0 0 0 .375-.375v-1.5A.375.375 0 0 0 10.875 9h-7.5A.375.375 0 0 0 3 9.375v1.5c0 .207.168.375.375.375Z" + clipRule="evenodd" + /> + </svg> ); export const SearchIcon = () => ( diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -18,6 +18,7 @@ import { assertUnreachable, buildPayto, encodeCrock, + getURLHostnamePortPath, hashNormalizedPaytoUri, HttpStatusCode, parsePaytoUri, @@ -37,6 +38,7 @@ import { Time, UIFormElementConfig, UIHandlerId, + useExchangeApiContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -99,14 +101,26 @@ export function Search() { </div> </form> - {paytoForm.status.status !== "ok" ? undefined : paytoForm.status.result - .paytoType === "x-taler-bank" ? ( - <XTalerBankForm onSearch={setPayto} /> - ) : paytoForm.status.result.paytoType === "iban" ? ( - <IbanForm onSearch={setPayto} /> - ) : ( - <GenericForm onSearch={setPayto} /> - )} + {(function () { + if (paytoForm.status.status !== "ok") return undefined; + switch (paytoForm.status.result.paytoType) { + case "iban": { + return <IbanForm onSearch={setPayto} />; + } + case "generic": { + return <GenericForm onSearch={setPayto} />; + } + case "x-taler-bank": { + return <XTalerBankForm onSearch={setPayto} />; + } + case "wallet": { + return <WalletForm onSearch={setPayto} />; + } + default: { + assertUnreachable(paytoForm.status.result.paytoType); + } + } + })()} {!paytoUri ? undefined : <ShowResult payto={paytoUri} />} </div> ); @@ -378,6 +392,56 @@ function IbanForm({ </form> ); } +function WalletForm({ + onSearch, +}: { + onSearch: (p: PaytoUri | undefined) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const { config } = useExchangeApiContext(); + const fields = walletFields(i18n); + const form = useFormState( + getShapeFromFields(fields), + { + exchange: getURLHostnamePortPath(config.keys.base_url), + }, + createTalerPaytoValidator(i18n), + ); + const paytoUri = + form.status.status === "fail" + ? undefined + : buildPayto( + "taler", + form.status.result.exchange, + form.status.result.reservePub, + ); + + return ( + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); + }} + autoCapitalize="none" + autoCorrect="off" + > + <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"> + <RenderAllFieldsByUiConfig + fields={convertUiField(i18n, fields, form.handler, getConverterById)} + /> + </div> + <button + disabled={form.status.status === "fail"} + class="disabled:bg-gray-100 disabled:text-gray-500 m-4 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" + onClick={() => onSearch(paytoUri)} + > + <i18n.Translate>Search</i18n.Translate> + </button> + </form> + ); +} + function GenericForm({ onSearch, }: { @@ -421,7 +485,7 @@ function GenericForm({ } interface FormPayto { - paytoType: "generic" | "iban" | "x-taler-bank"; + paytoType: "generic" | "iban" | "x-taler-bank" | "wallet"; } function createFormValidator(i18n: InternationalizationAPI) { @@ -558,6 +622,46 @@ function createTalerBankPaytoValidator(i18n: InternationalizationAPI) { }; } +interface PaytoUriTalerForm { + exchange: string; + reservePub: string; +} +function createTalerPaytoValidator(i18n: InternationalizationAPI) { + return function check( + state: RecursivePartial<FormValues<PaytoUriTalerForm>>, + ): FormStatus<PaytoUriTalerForm> { + const errors = undefinedIfEmpty<FormErrors<PaytoUriTalerForm>>({ + exchange: !state.exchange ? i18n.str`required` : undefined, + reservePub: !state.reservePub + ? i18n.str`required` + : state.reservePub.length !== 16 + ? i18n.str`Should be 16 charaters` + : undefined, + }); + + if (errors === undefined) { + const result: PaytoUriTalerForm = { + exchange: state.exchange!, + reservePub: state.reservePub!, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<PaytoUriTalerForm> = { + exchange: state.exchange, + reservePub: state.reservePub, + }; + return { + status: "fail", + result, + errors, + }; + }; +} + const paytoTypeField: ( i18n: InternationalizationAPI, ) => UIFormElementConfig[] = (i18n) => [ @@ -575,6 +679,10 @@ const paytoTypeField: ( label: i18n.str`Taler Bank`, }, { + value: "wallet", + label: i18n.str`Wallet`, + }, + { value: "generic", label: i18n.str`Generic Payto:// URI`, }, @@ -613,20 +721,74 @@ const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( id: "account" as UIHandlerId, type: "text", required: true, - label: i18n.str`Account`, + label: i18n.str`IBAN`, help: i18n.str`International Bank Account Number`, placeholder: i18n.str`DE1231231231`, // validator: (value) => validateIBAN(value, i18n), }, - receiverName(i18n), + // receiverName(i18n), + // { + // id: "bic" as UIHandlerId, + // type: "text", + // label: i18n.str`Bank`, + // help: i18n.str`Business Identifier Code`, + // placeholder: i18n.str`GENODEM1GLS`, + // // validator: (value) => validateIBAN(value, i18n), + // }, +]; +const paytoHashFields: ( + i18n: InternationalizationAPI, +) => UIFormElementConfig[] = (i18n) => [ { - id: "bic" as UIHandlerId, + id: "account" as UIHandlerId, type: "text", - label: i18n.str`Bank`, - help: i18n.str`Business Identifier Code`, - placeholder: i18n.str`GENODEM1GLS`, + required: true, + label: i18n.str`ID`, + help: i18n.str`Normalized payto:// hash`, + placeholder: i18n.str`ABC123`, // validator: (value) => validateIBAN(value, i18n), }, + // receiverName(i18n), + // { + // id: "bic" as UIHandlerId, + // type: "text", + // label: i18n.str`Bank`, + // help: i18n.str`Business Identifier Code`, + // placeholder: i18n.str`GENODEM1GLS`, + // // validator: (value) => validateIBAN(value, i18n), + // }, +]; + +const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( + i18n, +) => [ + { + id: "exchange" as UIHandlerId, + type: "text", + required: true, + label: i18n.str`Host`, + help: i18n.str`Exchange hostname`, + placeholder: i18n.str`exchange.taler.net`, + // validator: (value) => validateIBAN(value, i18n), + }, + { + id: "reservePub" as UIHandlerId, + type: "text", + required: true, + label: i18n.str`ID`, + help: i18n.str`Wallet reserve public key`, + placeholder: i18n.str`abcdef1235`, + // validator: (value) => validateIBAN(value, i18n), + }, + // receiverName(i18n), + // { + // id: "bic" as UIHandlerId, + // type: "text", + // label: i18n.str`Bank`, + // help: i18n.str`Business Identifier Code`, + // placeholder: i18n.str`GENODEM1GLS`, + // // validator: (value) => validateIBAN(value, i18n), + // }, ]; const talerBankFields: ( @@ -636,8 +798,8 @@ const talerBankFields: ( id: "account" as UIHandlerId, type: "text", required: true, - label: i18n.str`Bank account`, - help: i18n.str`Bank account id`, + label: i18n.str`Account`, + help: i18n.str`Bank account identification`, placeholder: i18n.str`DE123123123`, }, { @@ -649,7 +811,7 @@ const talerBankFields: ( placeholder: i18n.str`bank.demo.taler.net`, // validator: (value) => validateTalerBank(value, i18n), }, - receiverName(i18n), + // receiverName(i18n), ]; function validateIBAN( diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -68,24 +68,35 @@ export function ShowConsolidated({ const formConfig: FormConfiguration = { type: "double-column", - design: Object.entries(fixed).length > 0 ? [ - { - title: i18n.str`KYC collected info`, - fields: Object.entries(fixed).map(([key, field]) => { - const result: UIFormElementConfig = { - type: "text", - label: key as TranslatedString, - id: `${key}.value` as UIHandlerId, - disabled: true, - help: `At ${field.since.t_ms === "never" - ? "never" - : format(field.since.t_ms, "dd/MM/yyyy HH:mm:ss") - }` as TranslatedString, - }; - return result; - }), - } - ] : [], + design: + Object.entries(fixed).length > 0 + ? [ + { + title: i18n.str`KYC collected info`, + description: + until.t_ms === "never" + ? undefined + : i18n.str`All information known until ${format( + until.t_ms, + "dd/MM/yyyy HH:mm:ss", + )}`, + fields: Object.entries(fixed).map(([key, field]) => { + const result: UIFormElementConfig = { + type: "text", + label: key as TranslatedString, + id: `${key}.value` as UIHandlerId, + disabled: true, + help: `At ${ + field.since.t_ms === "never" + ? "never" + : format(field.since.t_ms, "dd/MM/yyyy HH:mm:ss") + }` as TranslatedString, + }; + return result; + }), + }, + ] + : [], }; const shape: Array<UIHandlerId> = formConfig.design.flatMap((field) => getShapeFromFields(field.fields), diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts @@ -68,9 +68,11 @@ function DefaultTestingContext({ supported_kyc_requirements: [], version: "asd", }; + const keys: TalerExchangeApi.ExchangeKeysResponse = {} as any; + const value: ExchangeContextType = { cancelRequest: () => null, - config, + config: { config, keys }, url: new URL("/", "http://localhost"), hints: [], lib: { diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx @@ -57,9 +57,10 @@ function getWrapperForGroup(): FunctionComponent { supported_kyc_requirements: [], version: "asd", }; + const keys: TalerExchangeApi.ExchangeKeysResponse = {} as any; const value: ExchangeContextType = { cancelRequest: () => null, - config, + config: { config, keys }, url: new URL("/", "http://localhost"), hints: [], lib: {