taler-typescript-core

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

commit 17eb4673564ac5abe3cd150d722502e784be1669
parent 93b73848f7cf47e21ec256094610c03f67659324
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 11 Feb 2025 11:28:13 -0300

kyc status field and #9512

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/merchant-backoffice-ui/src/components/product/ProductForm.tsx | 19+++++++++++++------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx | 203++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx | 225++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 60++++++++++++++++++++++++++++++++++++------------------------
5 files changed, 479 insertions(+), 113 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -19,7 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerError } from "@gnu-taler/taler-util"; +import { + getMerchantAccountKycStatusSimplified, + MerchantAccountKycStatus, + MerchantAccountKycStatusSimplified, + TalerError, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useSessionContext } from "../../context/session.js"; @@ -40,14 +45,20 @@ export function Sidebar({ mobile }: Props): VNode { const kycStatus = useInstanceKYCDetails(); const [pref] = usePreference(); - const needKYC = + const allKycData = kycStatus !== undefined && !(kycStatus instanceof TalerError) && kycStatus.type === "ok" && - !!kycStatus.body && - kycStatus.body.kyc_data.findIndex( - (d) => d.payto_kycauths !== undefined || d.access_token !== undefined, - ) !== -1; + !!kycStatus.body + ? kycStatus.body.kyc_data + : []; + + const simplifiedKycStatus = allKycData.reduce((prev, cur) => { + const st = getMerchantAccountKycStatusSimplified(cur.status); + if (st > prev) return st; + return prev; + }, MerchantAccountKycStatusSimplified.OK); + const isLoggedIn = state.status === "loggedIn"; const hasToken = isLoggedIn && state.token !== undefined; @@ -145,23 +156,51 @@ export function Sidebar({ mobile }: Props): VNode { </a> </li> ) : undefined} - {needKYC && ( - <li class="is-warning"> - <a - href={"/kyc"} - class="has-icon" - style={{ - backgroundColor: "darkorange", - color: "black", - }} - > - <span class="icon"> - <i class="mdi mdi-account-check" /> - </span> - <span class="menu-item-label">KYC Status</span> - </a> - </li> - )} + <li + class={ + simplifiedKycStatus === + MerchantAccountKycStatusSimplified.WARNING + ? "is-warning" + : simplifiedKycStatus === + MerchantAccountKycStatusSimplified.ERROR + ? "is-error" + : simplifiedKycStatus === + MerchantAccountKycStatusSimplified.ACTION_REQUIRED + ? "is-warning" + : undefined + } + > + <a + href={"/kyc"} + class="has-icon" + style={ + simplifiedKycStatus === + MerchantAccountKycStatusSimplified.WARNING + ? { + backgroundColor: "darkorange", + color: "black", + } + : simplifiedKycStatus === + MerchantAccountKycStatusSimplified.ERROR + ? { + backgroundColor: "darkred", + color: "black", + } + : simplifiedKycStatus === + MerchantAccountKycStatusSimplified.ACTION_REQUIRED + ? { + backgroundColor: "darkorange", + color: "black", + } + : undefined + } + > + <span class="icon"> + <i class="mdi mdi-account-check" /> + </span> + <span class="menu-item-label">KYC Status</span> + </a> + </li> </ul> <p class="menu-label"> <i18n.Translate>Configuration</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -26,7 +26,7 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h } from "preact"; +import { Fragment, h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; import { useSessionContext } from "../../context/session.js"; import { undefinedIfEmpty } from "../../utils/table.js"; @@ -41,6 +41,7 @@ import { InputWithAddon } from "../form/InputWithAddon.js"; import { InputArray } from "../form/InputArray.js"; import { useInstanceCategories } from "../../hooks/category.js"; import { ErrorLoadingMerchant } from "../ErrorLoadingMerchant.js"; +import { usePreference } from "../../hooks/preference.js"; type Entity = TalerMerchantApi.ProductDetail & { product_id: string; @@ -55,6 +56,8 @@ interface Props { export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { const { i18n } = useTranslationContext(); const { state, lib } = useSessionContext(); + const [preference] = usePreference(); + // FIXME: if the category list is big the will bring a lot of info // we could find a lazy way to add up on searches const categoriesResult = useInstanceCategories(); @@ -227,11 +230,15 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { alreadyExist={alreadyExist} tooltip={i18n.str`Inventory for products with finite supply (for internal use only).`} /> - <InputTaxes<Entity> - name="taxes" - label={i18n.str`Taxes`} - tooltip={i18n.str`Taxes included in the product price, exposed to customers.`} - /> + {preference.developerMode ? ( + <InputTaxes<Entity> + name="taxes" + label={i18n.str`Taxes`} + tooltip={i18n.str`Taxes included in the product price, exposed to customers.`} + /> + ) : ( + <Fragment /> + )} <InputArray name="categories_map" label={i18n.str`Categories`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -19,14 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + getMerchantAccountKycStatusSimplified, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; export interface Props { status: TalerMerchantApi.MerchantAccountKycRedirectsResponse; // onGetInfo: (url: string, token: AccessToken) => void; - onShowInstructions: (toAccounts: string[], fromAccount: string) => void; + onShowInstructions: (e: TalerMerchantApi.MerchantAccountKycRedirect) => void; } export function ListPage({ @@ -71,7 +75,7 @@ export function ListPage({ interface PendingTableProps { entries: TalerMerchantApi.MerchantAccountKycRedirect[]; // onGetInfo: (url: string, token: AccessToken) => void; - onShowInstructions: (toAccounts: string[], fromAccount: string) => void; + onShowInstructions: (e: TalerMerchantApi.MerchantAccountKycRedirect) => void; } interface TimedOutTableProps { @@ -96,55 +100,160 @@ function PendingTable({ <i18n.Translate>Account</i18n.Translate> </th> <th> - <i18n.Translate>Reason</i18n.Translate> + <i18n.Translate>Status</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> </th> </tr> </thead> <tbody> {entries.map((e, i) => { - if (e.payto_kycauths === undefined) { - const spa = new URL(`kyc-spa/${e.access_token}`, e.exchange_url) - .href; - return ( - <tr key={i}> - <td>{e.exchange_url}</td> - <td>{e.payto_uri}</td> - <td> - {e.exchange_http_status === 200 || - e.exchange_http_status === 204 ? ( - <i18n.Translate>Ready</i18n.Translate> - ) : ( - <a href={spa} target="_black" rel="noreferrer"> - <i18n.Translate> - Pending KYC process, click here to complete - </i18n.Translate> - </a> - )} - </td> - </tr> - ); - } else { - const accounts = e.payto_kycauths; - return ( - <tr key={i}> - <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> - {e.exchange_url} - </td> - <td - onClick={() => onShowInstructions(accounts, e.payto_uri)} - style={{ cursor: "pointer" }} - > - {e.payto_uri} - </td> - <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> - <i18n.Translate> - The Payment Service Provider requires an account - verification. - </i18n.Translate> - </td> - </tr> - ); - } + return ( + <tr + key={i} + onClick={() => onShowInstructions(e)} + style={{ cursor: "pointer" }} + > + <td>{e.exchange_url}</td> + <td>{e.payto_uri}</td> + <td> + {(function (): VNode { + const st = getMerchantAccountKycStatusSimplified(e.status); + switch (st) { + case TalerMerchantApi.MerchantAccountKycStatusSimplified + .OK: + return <i18n.Translate>Ok</i18n.Translate>; + case TalerMerchantApi.MerchantAccountKycStatusSimplified + .ACTION_REQUIRED: + return <i18n.Translate>Action required</i18n.Translate>; + case TalerMerchantApi.MerchantAccountKycStatusSimplified + .WARNING: + return <i18n.Translate>Warning</i18n.Translate>; + case TalerMerchantApi.MerchantAccountKycStatusSimplified + .ERROR: + return <i18n.Translate>Error</i18n.Translate>; + default: + assertUnreachable(st); + } + })()} + </td> + <td> + {(function (): VNode { + switch (e.status) { + case TalerMerchantApi.MerchantAccountKycStatus + .KYC_WIRE_REQUIRED: + // action required + return ( + <i18n.Translate> + KYC wire transfer required, click for details. + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .KYC_REQUIRED: + // action required + return ( + <i18n.Translate> + KYC required, click here to proceed + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .AWAITING_AML_REVIEW: + // FIXME: can the account be used? + return ( + <i18n.Translate>Awaiting AML review</i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus.READY: + return <i18n.Translate>Ready</i18n.Translate>; + + case TalerMerchantApi.MerchantAccountKycStatus + .NO_EXCHANGE_KEY: + return ( + <i18n.Translate> + Updating exchange keys... + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .EXCHANGE_INTERNAL_ERROR: + return ( + <i18n.Translate> + Exchange internal error. Contact administrator or + check again later. + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .EXCHANGE_GATEWAY_TIMEOUT: + return ( + <i18n.Translate> + Exchange timeout. Contact administrator or check + again later. + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .EXCHANGE_UNREACHABLE: + return ( + <i18n.Translate> + Exchange unreachable. Contact administrator or check + again later. + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus + .KYC_WIRE_IMPOSSIBLE: + return ( + <i18n.Translate> + Can't complete due to wire transfer incomptability. + </i18n.Translate> + ); + case TalerMerchantApi.MerchantAccountKycStatus.LOGIC_BUG: + return ( + <i18n.Translate> + Merchant internal error. Contact administrator or + check again later. + </i18n.Translate> + ); + + case TalerMerchantApi.MerchantAccountKycStatus + .EXCHANGE_STATUS_INVALID: + return ( + <i18n.Translate> + Exchange response is invalid. Contact administrator + or check again later. + </i18n.Translate> + ); + + default: + assertUnreachable(e.status); + } + })()} + </td> + </tr> + ); + // if (e.payto_kycauths === undefined) { + // const spa = new URL(`kyc-spa/${e.access_token}`, e.exchange_url) + // .href; + // } else { + // const accounts = e.payto_kycauths; + // return ( + // <tr key={i}> + // <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> + // {e.exchange_url} + // </td> + // <td + // onClick={() => onShowInstructions(accounts, e.payto_uri)} + // style={{ cursor: "pointer" }} + // > + // {e.payto_uri} + // </td> + // <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> + // {e.status} + // <i18n.Translate> + // The Payment Service Provider requires an account + // verification. + // </i18n.Translate> + // </td> + // </tr> + // ); + // } })} </tbody> </table> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -23,6 +23,7 @@ import { HttpStatusCode, TalerError, TalerExchangeHttpClient, + TalerMerchantApi, assertUnreachable, parsePaytoUri, } from "@gnu-taler/taler-util"; @@ -32,7 +33,12 @@ import { Loading } from "../../../../components/exception/loading.js"; import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; import { ListPage } from "./ListPage.js"; import { useState } from "preact/hooks"; -import { ValidBankAccount } from "../../../../components/modal/index.js"; +import { + ConfirmModal, + ValidBankAccount, +} from "../../../../components/modal/index.js"; +import { useTransition } from "preact/compat"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; interface Props { // onGetInfo: (id: string) => void; @@ -42,7 +48,7 @@ interface Props { export default function ListKYC(_p: Props): VNode { const result = useInstanceKYCDetails(); const [showingInstructions, setShowingInstructions] = useState< - { toAccounts: string[]; fromAccount: string } | undefined + TalerMerchantApi.MerchantAccountKycRedirect | undefined >(undefined); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -91,25 +97,218 @@ export default function ListKYC(_p: Props): VNode { return ( <Fragment> {showingInstructions !== undefined ? ( - <Fragment> - <ValidBankAccount - origin={parsePaytoUri(showingInstructions.fromAccount)!} - targets={showingInstructions.toAccounts.map( - (d) => parsePaytoUri(d)!, - )} - onCancel={() => setShowingInstructions(undefined)} - /> - </Fragment> + <ShowInstructionForKycRedirect + e={showingInstructions} + onCancel={() => setShowingInstructions(undefined)} + /> ) : undefined} <ListPage status={status} // onGetInfo={async (exchange, ac) => { // new URL() // }} - onShowInstructions={(toAccounts, fromAccount) => { - setShowingInstructions({ toAccounts, fromAccount }); + onShowInstructions={(e) => { + setShowingInstructions(e); }} /> </Fragment> ); } + +function ShowInstructionForKycRedirect({ + e, + onCancel, +}: { + e: TalerMerchantApi.MerchantAccountKycRedirect; + onCancel: () => void; +}): VNode { + const { i18n } = useTranslationContext(); + switch (e.status) { + case TalerMerchantApi.MerchantAccountKycStatus.KYC_WIRE_REQUIRED: + return ( + <ValidBankAccount + origin={parsePaytoUri(e.payto_uri)!} + targets={(e.payto_kycauths ?? []).map((d) => parsePaytoUri(d)!)} + onCancel={onCancel} + /> + ); + case TalerMerchantApi.MerchantAccountKycStatus.NO_EXCHANGE_KEY: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`No exchange keys`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The backend service is still synchonizing with the payment service + provider. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.KYC_WIRE_IMPOSSIBLE: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Can't wire from this account`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The payment service provider requires you to make a wire transfer + using the account provided but this account is not compatible with + the exchange. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + + case TalerMerchantApi.MerchantAccountKycStatus.KYC_REQUIRED: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`KYC Required`} + active + onCancel={onCancel} + > + <a href="#"> + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The payment provider requires information to enable the account. + </i18n.Translate> + </p> + </a> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.AWAITING_AML_REVIEW: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Awaiting AML review`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + This account is can't be used. The payment service provider is + doing manual review of the account information. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.READY: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Ready`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + This account is ready to be used with the payment service + provider. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.LOGIC_BUG: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Logic bug`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The backend service detected an internal error, contact the system + administrator or check again later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_INTERNAL_ERROR: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Internal error`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The payment service provider detected an internal error, contact + the system administrator or check again later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Logic bug`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The backend service couldn't contact the payment service provider + due to a timeout, contact the system administrator or check again + later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_UNREACHABLE: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Logic bug`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + Unable to reach the payment service provider, contact the system + administrator or check again later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_STATUS_INVALID: + { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Logic bug`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The payment service provider replied with a invalid status, + contact the system administrator or check again later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } + return <div />; + default: + assertUnreachable(e.status); + } +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -137,7 +137,7 @@ function Table({ onLoadMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); - const [settings] = usePreference(); + const [preference] = usePreference(); return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -157,12 +157,18 @@ function Table({ <th> <i18n.Translate>Price per unit</i18n.Translate> </th> - <th> - <i18n.Translate>Taxes</i18n.Translate> - </th> - <th> - <i18n.Translate>Sales</i18n.Translate> - </th> + {preference.developerMode ? ( + <Fragment> + <th> + <i18n.Translate>Taxes</i18n.Translate> + </th> + <th> + <i18n.Translate>Sales</i18n.Translate> + </th> + </Fragment> + ) : ( + <Fragment /> + )} <th> <i18n.Translate>Stock</i18n.Translate> </th> @@ -180,7 +186,7 @@ function Table({ ? "never" : `restock at ${format( new Date(i.next_restock.t_s * 1000), - dateFormatForSettings(settings), + dateFormatForSettings(preference), )}`; let stockInfo: ComponentChildren = ""; if (i.total_stock < 0) { @@ -235,22 +241,28 @@ function Table({ > {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`} </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {sum(i.taxes)} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {difference(i.price, sum(i.taxes))} - </td> + {preference.developerMode ? ( + <Fragment> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {sum(i.taxes)} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {difference(i.price, sum(i.taxes))} + </td> + </Fragment> + ) : ( + <Fragment /> + )} <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)