From 191fc73b2bf443fc3998124b3bfa96a702653183 Mon Sep 17 00:00:00 2001 From: sstefdev Date: Wed, 18 Dec 2024 18:09:30 +0100 Subject: [PATCH 1/4] feat: added filtering to invoice dashboard --- package-lock.json | 6 +- .../src/lib/dashboard/invoice-view.svelte | 165 ++++++++---- .../src/lib/view-requests.svelte | 240 +++++++++++++----- shared/components/dropdown.svelte | 132 ++++------ shared/components/input.svelte | 77 +++--- .../searchable-checkbox-dropdown.svelte | 237 +++++++++++++++++ 6 files changed, 638 insertions(+), 219 deletions(-) create mode 100644 shared/components/searchable-checkbox-dropdown.svelte diff --git a/package-lock.json b/package-lock.json index d7234103..40b850c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11354,7 +11354,7 @@ }, "packages/create-invoice-form": { "name": "@requestnetwork/create-invoice-form", - "version": "0.11.10", + "version": "0.11.12", "license": "MIT", "dependencies": { "@requestnetwork/data-format": "0.19.5", @@ -11374,7 +11374,7 @@ }, "packages/invoice-dashboard": { "name": "@requestnetwork/invoice-dashboard", - "version": "0.11.8", + "version": "0.11.10", "license": "MIT", "dependencies": { "@requestnetwork/payment-detection": "0.49.0", @@ -11409,7 +11409,7 @@ }, "packages/payment-widget": { "name": "@requestnetwork/payment-widget", - "version": "0.3.6", + "version": "0.3.7", "license": "MIT", "dependencies": { "@requestnetwork/payment-processor": "0.52.0", diff --git a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte index 00cc6b28..c99eae73 100644 --- a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte +++ b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte @@ -372,24 +372,78 @@ paymentCurrencies: any[], signer: any ) => { - const approvalCheckers: { [key: string]: () => Promise } = { - [Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: () => - hasErc20Approval(requestData!, address!, signer), - [Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY]: () => - hasErc20ApprovalForProxyConversion( - requestData!, - address!, - paymentCurrencies[0]?.address, - signer, - requestData.expectedAmount - ), - }; + try { + if (!paymentNetworkExtension?.id || !address || !signer) { + console.log("Missing dependencies:", { + network: paymentNetworkExtension?.id, + address, + signer: !!signer, + }); + return false; + } + + // Skip approval check if payment is not required + if (requestData?.balance?.balance >= requestData?.expectedAmount) { + console.log("Payment already completed, skipping approval check"); + return true; + } + + // Validate payment currency + if (!paymentCurrencies[0]?.address) { + console.log("Invalid payment currency:", paymentCurrencies[0]); + return false; + } + + // Check if we're on the correct network + const chainId = await signer.getChainId(); + const expectedChainId = getNetworkIdFromNetworkName(network); + const expectedChainIdNumber = parseInt(expectedChainId, 16); + + if (chainId !== expectedChainIdNumber) { + console.log("Wrong network:", { + current: `0x${chainId.toString(16)}`, + expected: expectedChainId, + }); + return false; + } - return ( - (paymentNetworkExtension?.id && - (await approvalCheckers[paymentNetworkExtension.id]?.())) || - false - ); + if ( + paymentNetworkExtension.id === + Types.Extension.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ) { + try { + return await hasErc20Approval(requestData!, address!, signer).catch( + () => false + ); + } catch { + console.log("ERC20_FEE_PROXY_CONTRACT approval check failed"); + return false; + } + } + + if ( + paymentNetworkExtension.id === + Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + ) { + try { + return await hasErc20ApprovalForProxyConversion( + requestData!, + address!, + paymentCurrencies[0]?.address, + signer, + requestData.expectedAmount + ).catch(() => false); + } catch { + console.log("ANY_TO_ERC20_PROXY approval check failed"); + return false; + } + } + + return false; + } catch (error) { + console.log("General approval check error:", error); + return false; + } }; async function approve() { @@ -485,49 +539,74 @@ : value.toFixed(maxDecimals); } - async function checkBalance() { + const checkBalance = async () => { try { - if (!address || !paymentCurrencies[0] || !network) { - console.log("Missing required parameters for balance check:", { - address, - paymentCurrency: paymentCurrencies[0], - network, + if (!account?.address || !requestData || !paymentCurrencies?.[0]) { + console.log("Missing dependencies for balance check:", { + hasAddress: !!account?.address, + hasRequestData: !!requestData, + hasPaymentCurrency: !!paymentCurrencies?.[0], }); return; } - const invoiceNetworkId = Number(getNetworkIdFromNetworkName(network)); + // Check if we're on the correct network + const hexStringChain = "0x" + account.chainId.toString(16); + const expectedChainId = getNetworkIdFromNetworkName(network); - if (account.chainId !== invoiceNetworkId) { + if (hexStringChain !== String(expectedChainId)) { + console.log("Wrong network for balance check:", { + current: hexStringChain, + expected: expectedChainId, + }); + userBalance = "0"; hasEnoughBalance = false; - console.log("Wrong network - balance check skipped"); return; } - if (paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20) { + // Rest of the balance check logic... + if ( + !paymentCurrencies[0].address || + paymentCurrencies[0].type === Types.RequestLogic.CURRENCY.ETH + ) { const balance = await getBalance(wagmiConfig, { - address, - token: paymentCurrencies[0].address as `0x${string}`, - chainId: invoiceNetworkId, + address: account.address, }); - const balanceNum = BigInt(balance.formatted); - userBalance = formatBalance(balanceNum); - hasEnoughBalance = balance.value >= BigInt(request.expectedAmount); - } else { + + userBalance = formatUnits(balance.value, balance.decimals); + const balanceInWei = BigInt(Math.floor(Number(balance.value))); + const expectedAmountBigInt = BigInt(requestData.expectedAmount || 0); + hasEnoughBalance = balanceInWei >= expectedAmountBigInt; + + return; + } + + // For ERC20 tokens + try { const balance = await getBalance(wagmiConfig, { - address, - chainId: invoiceNetworkId, + address: account.address, + token: paymentCurrencies[0].address, }); - const balanceNum = BigInt(balance.formatted); - userBalance = formatBalance(balanceNum); - hasEnoughBalance = balance.value >= BigInt(request.expectedAmount); + + if (balance && typeof balance.value !== "undefined") { + userBalance = formatUnits(balance.value, balance.decimals); + const balanceInWei = BigInt(Math.floor(Number(balance.value))); + const expectedAmountBigInt = BigInt(requestData.expectedAmount || 0); + hasEnoughBalance = balanceInWei >= expectedAmountBigInt; + } else { + throw new Error("Invalid balance response"); + } + } catch (tokenError) { + console.error("Error checking ERC20 balance:", tokenError); + userBalance = "0"; + hasEnoughBalance = false; } - } catch (err) { - console.error("Error checking balance:", err); - hasEnoughBalance = false; + } catch (error) { + console.error("Error checking balance:", error); userBalance = "0"; + hasEnoughBalance = false; } - } + }; const currentStatusIndex = statuses.length - 1; diff --git a/packages/invoice-dashboard/src/lib/view-requests.svelte b/packages/invoice-dashboard/src/lib/view-requests.svelte index 1eb9aeb3..ecc108ea 100644 --- a/packages/invoice-dashboard/src/lib/view-requests.svelte +++ b/packages/invoice-dashboard/src/lib/view-requests.svelte @@ -17,6 +17,7 @@ import DashboardSkeleton from "@requestnetwork/shared-components/dashboard-skeleton.svelte"; import { toast } from "svelte-sonner"; import Modal from "@requestnetwork/shared-components/modal.svelte"; + import SearchableDropdownCheckbox from "@requestnetwork/shared-components/searchable-checkbox-dropdown.svelte"; // Icons import ChevronDown from "@requestnetwork/shared-icons/chevron-down.svelte"; import ChevronLeft from "@requestnetwork/shared-icons/chevron-left.svelte"; @@ -106,6 +107,27 @@ let sortOrder = "desc"; let sortColumn = "timestamp"; + let selectedNetworks: string[] = []; + let networkOptions: { value: string; checked: boolean }[] = []; + + let selectedTxTypes: string[] = []; + let txTypeOptions = [ + { value: "IN", checked: false }, + { value: "OUT", checked: false }, + ]; + + let selectedStatuses: string[] = []; + let statusOptions = [ + { value: "paid", checked: false }, + { value: "partially paid", checked: false }, + { value: "accepted", checked: false }, + { value: "awaiting payment", checked: false }, + { value: "canceled", checked: false }, + { value: "rejected", checked: false }, + { value: "overdue", checked: false }, + { value: "pending", checked: false }, + ]; + const handleWalletConnection = async () => { account = getAccount(wagmiConfig); await loadRequests(sliderValueForDecryption, account, requestNetwork); @@ -159,16 +181,32 @@ account: GetAccountReturnType, requestNetwork: RequestNetwork | undefined | null ) => { - if (!account?.address || !requestNetwork) return; + if (!account?.address || !requestNetwork) { + return; + } try { const requestsData = await requestNetwork?.fromIdentity({ type: Types.Identity.TYPE.ETHEREUM_ADDRESS, value: account?.address, }); + requests = requestsData ?.map((request) => request.getData()) .sort((a, b) => b.timestamp - a.timestamp); + + const uniqueNetworks = new Set(); + requests?.forEach((request) => { + const network = request.currencyInfo.network; + if (network) { + uniqueNetworks.add(network); + } + }); + + networkOptions = Array.from(uniqueNetworks).map((network) => ({ + value: network, + checked: selectedNetworks.includes(network), + })); } catch (error) { console.error("Failed to fetch requests:", error); } @@ -222,13 +260,29 @@ $: filteredRequests = requests?.filter((request) => { const terms = searchQuery.toLowerCase(); + const network = request.currencyInfo.network; + const txType = signer === request.payer?.value ? "OUT" : "IN"; + const status = checkStatus(request).toLowerCase(); + + const networkMatch = + selectedNetworks.length === 0 || + (network && selectedNetworks.includes(network)); + + const txTypeMatch = + selectedTxTypes.length === 0 || selectedTxTypes.includes(txType); + + const statusMatch = + selectedStatuses.length === 0 || selectedStatuses.includes(status); if ( - currentTab === "All" || - (currentTab === "Get Paid" && - request.payee?.value?.toLowerCase() === signer?.toLowerCase()) || - (currentTab === "Pay" && - request.payer?.value?.toLowerCase() === signer?.toLowerCase()) + networkMatch && + txTypeMatch && + statusMatch && + (currentTab === "All" || + (currentTab === "Get Paid" && + request.payee?.value?.toLowerCase() === signer?.toLowerCase()) || + (currentTab === "Pay" && + request.payer?.value?.toLowerCase() === signer?.toLowerCase())) ) { const invoiceMatches = request.contentData?.invoiceNumber ?.toString() @@ -361,6 +415,7 @@ const handleSearchChange = (event: Event) => { const { value } = event.target as HTMLInputElement; searchQuery = value; + currentPage = 1; }; const handleSort = (column: string) => { @@ -397,38 +452,66 @@ return; loading = true; - if (sliderValue === "on") { - try { - const signer = await getEthersSigner(wagmiConfig); - if (signer && currentAccount?.address) { - loadSessionSignatures = - localStorage?.getItem("lit-wallet-sig") === null; - await cipherProvider?.getSessionSignatures( - signer, - currentAccount.address, - window.location.host, - "Sign in to Lit Protocol through Request Network" - ); - cipherProvider?.enableDecryption(true); - localStorage?.setItem("isDecryptionEnabled", JSON.stringify(true)); + const previousNetworks = [...selectedNetworks]; // Store current selection + + try { + if (sliderValue === "on") { + try { + const signer = await getEthersSigner(wagmiConfig); + if (signer && currentAccount?.address) { + loadSessionSignatures = + localStorage?.getItem("lit-wallet-sig") === null; + await cipherProvider?.getSessionSignatures( + signer, + currentAccount.address, + window.location.host, + "Sign in to Lit Protocol through Request Network" + ); + cipherProvider?.enableDecryption(true); + localStorage?.setItem("isDecryptionEnabled", JSON.stringify(true)); + } + } catch (error) { + console.error("Failed to enable decryption:", error); + toast.error("Failed to enable decryption."); + return; + } finally { + loadSessionSignatures = false; } - } catch (error) { - console.error("Failed to enable decryption:", error); - toast.error("Failed to enable decryption."); - loading = false; - return; - } finally { - loadSessionSignatures = false; + } else { + cipherProvider?.enableDecryption(false); + localStorage?.setItem("isDecryptionEnabled", JSON.stringify(false)); } - } else { - cipherProvider?.enableDecryption(false); - localStorage?.setItem("isDecryptionEnabled", JSON.stringify(false)); + await getRequests(currentAccount, currentRequestNetwork); + selectedNetworks = previousNetworks; // Restore selection + } finally { + loading = false; } - await getRequests(currentAccount, currentRequestNetwork); - loading = false; }; $: loadRequests(sliderValueForDecryption, account, requestNetwork); + + const handleNetworkSelection = async (networks: string[]) => { + selectedNetworks = networks; + currentPage = 1; + if (networks.length === 0 && selectedNetworks.length > 0) { + loading = true; + try { + await getRequests(account!, requestNetwork!); + } finally { + loading = false; + } + } + }; + + const handleTxTypeSelection = (types: string[]) => { + selectedTxTypes = types; + currentPage = 1; + }; + + const handleStatusSelection = (statuses: string[]) => { + selectedStatuses = statuses; + currentPage = 1; + };
-
- -
- -
- - {#if cipherProvider} -
- -
- {/if} + +
+ +
+ + {#if cipherProvider} +
+ +
+ {/if} + + - -
@@ -902,6 +1010,7 @@ display: flex; justify-content: space-between; align-items: center; + margin-bottom: 12px; } @media only screen and (max-width: 880px) { @@ -1088,4 +1197,15 @@ margin-bottom: 0.5rem; color: #4b5563; } + + .dropdown-controls { + display: flex; + align-items: center; + margin-left: auto; + gap: 8px; + } + + .switch-wrapper { + margin-left: 8px; + } diff --git a/shared/components/dropdown.svelte b/shared/components/dropdown.svelte index c711f024..c97d2520 100644 --- a/shared/components/dropdown.svelte +++ b/shared/components/dropdown.svelte @@ -2,6 +2,12 @@ import { openDropdown } from "../store/dropwdown"; import { onMount } from "svelte"; + const CheckIcon = ` + + + + `; + export let config; export let selectedValue = ""; export let options: { value: string; label: string; checked?: boolean }[] = @@ -13,10 +19,16 @@ let dropdownId: string; let isOpen = false; let dropdownContainer: HTMLElement; + let localOptions = options; const selectOption = (value: string, checked?: boolean) => { if (type === "checkbox") { - onchange(value, checked); + localOptions = localOptions.map((option) => + option.value === value + ? { ...option, checked: !option.checked } + : option + ); + onchange(value, !checked); } else { selectedValue = options.find((option) => option.value === value)?.label || ""; @@ -51,7 +63,7 @@ {#if isOpen} - +