commit 17eb4673564ac5abe3cd150d722502e784be1669
parent 93b73848f7cf47e21ec256094610c03f67659324
Author: Sebastian <sebasjm@gmail.com>
Date: Tue, 11 Feb 2025 11:28:13 -0300
kyc status field and #9512
Diffstat:
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)