diff --git a/package-lock.json b/package-lock.json index b0a36ab5..2b5b0f86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11007,7 +11007,7 @@ }, "packages/create-invoice-form": { "name": "@requestnetwork/create-invoice-form", - "version": "0.11.8", + "version": "0.11.10", "license": "MIT", "dependencies": { "@requestnetwork/data-format": "0.19.4", @@ -11027,7 +11027,7 @@ }, "packages/invoice-dashboard": { "name": "@requestnetwork/invoice-dashboard", - "version": "0.11.5", + "version": "0.11.8", "license": "MIT", "dependencies": { "@requestnetwork/payment-detection": "0.48.0", @@ -11062,7 +11062,7 @@ }, "packages/payment-widget": { "name": "@requestnetwork/payment-widget", - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "dependencies": { "@requestnetwork/payment-processor": "0.51.0", diff --git a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte index 432f7dac..26601133 100644 --- a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte +++ b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte @@ -20,7 +20,6 @@ import Button from "@requestnetwork/shared-components/button.svelte"; import Tooltip from "@requestnetwork/shared-components/tooltip.svelte"; // Icons - import Check from "@requestnetwork/shared-icons/check.svelte"; import Download from "@requestnetwork/shared-icons/download.svelte"; // Utils import { formatDate } from "@requestnetwork/shared-utils/formatDate"; @@ -40,7 +39,6 @@ } interface BuyerInfo extends EntityInfo {} - interface SellerInfo extends EntityInfo {} export let config; @@ -55,7 +53,6 @@ let currency: CurrencyTypes.CurrencyDefinition | undefined = getCurrencyFromManager(request.currencyInfo, currencyManager); let paymentCurrencies: (CurrencyTypes.CurrencyDefinition | undefined)[] = []; - let statuses: any = []; let isPaid = false; let loading = false; let requestData: any = null; @@ -74,9 +71,28 @@ let paymentNetworkExtension: | Types.Extension.IPaymentNetworkState | undefined; + let statuses: any[] = [ + { + name: "CORRECT_NETWORK", + message: "Correct Network", + done: correctChain, + }, + { + name: "SIGN_TRANSACTION", + message: "Sign Transaction", + done: false, + }, + { + name: "PAYMENT_DETECTED", + message: "Payment Detected", + done: false, + }, + ]; let status = checkStatus(requestData || request); + let isSigningTransaction = false; + const generateDetailParagraphs = (info: any) => { const fullName = [info?.firstName, info?.lastName] .filter(Boolean) @@ -126,11 +142,14 @@ buyerInfo = generateDetailParagraphs(request?.contentData?.buyerInfo); } - onMount(() => { - checkInvoice(); - }); + let previousRequestId: string | null = null; - $: request, checkInvoice(); + $: { + if (request?.requestId !== previousRequestId) { + previousRequestId = request?.requestId; + checkInvoice(); + } + } $: { account = account; @@ -142,6 +161,7 @@ try { unsupportedNetwork = false; loading = true; + const singleRequest = await requestNetwork?.fromRequestId( request!.requestId ); @@ -151,6 +171,9 @@ requestData = singleRequest?.getData(); paymentNetworkExtension = getPaymentNetworkExtension(requestData); + isPaid = requestData?.balance?.balance >= requestData?.expectedAmount; + + // Handle payment currencies setup if ( paymentNetworkExtension?.id === Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY @@ -185,12 +208,49 @@ network = paymentCurrencies[0]?.network || "mainnet"; + // Check ERC20 approval if needed if (paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20) { approved = await checkApproval(requestData, paymentCurrencies, signer); } else { approved = true; } + // Build status flow based on conditions + const baseStatuses = [ + { + name: "CORRECT_NETWORK", + message: "Correct Network", + done: correctChain, + }, + ]; + + // Add ERC20 approval status if needed + if ( + paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20 && + !approved + ) { + baseStatuses.push({ + name: "APPROVE_ERC20", + message: "Approve ERC20", + done: false, + }); + } + + // Add transaction and payment detection statuses + baseStatuses.push( + { + name: "SIGN_TRANSACTION", + message: "Sign Transaction", + done: false, + }, + { + name: "PAYMENT_DETECTED", + message: "Payment Detected", + done: false, + } + ); + + statuses = baseStatuses; status = checkStatus(requestData || request); } catch (err: any) { console.error("Error while checking invoice: ", err); @@ -205,11 +265,18 @@ const payTheRequest = async () => { try { loading = true; + isSigningTransaction = true; + + if (!requestNetwork || !requestData?.requestId) { + throw new Error("Request network or request data not available"); + } + const _request = await requestNetwork?.fromRequestId( - requestData?.requestId! + requestData.requestId ); - - statuses = [...statuses, "Waiting for payment"]; + if (!_request) { + throw new Error("Could not fetch request details"); + } let paymentSettings = undefined; if ( @@ -218,10 +285,14 @@ paymentNetworkExtension?.id === Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ETH_PROXY ) { + if (!currency || !paymentCurrencies[0]) { + throw new Error("Currency information not available"); + } + const { conversion } = await getConversionPaymentValues({ - baseAmount: requestData?.expectedAmount, - denominationCurrency: currency!, - selectedPaymentCurrency: paymentCurrencies[0]!, + baseAmount: requestData.expectedAmount, + denominationCurrency: currency, + selectedPaymentCurrency: paymentCurrencies[0], currencyManager, provider: signer, fromAddress: address, @@ -236,23 +307,53 @@ undefined, paymentSettings ); + + // Update sign transaction status + const signStatus = statuses.find((s) => s.name === "SIGN_TRANSACTION"); + if (signStatus) signStatus.done = true; + statuses = [...statuses]; + await paymentTx.wait(); - statuses = [...statuses, "Payment detected"]; + // Update payment detected status + const paymentStatus = statuses.find((s) => s.name === "PAYMENT_DETECTED"); + if (paymentStatus) paymentStatus.done = true; + statuses = [...statuses]; + while (requestData.balance?.balance! < requestData.expectedAmount) { requestData = await _request?.refresh(); await new Promise((resolve) => setTimeout(resolve, 1000)); } - statuses = [...statuses, "Payment confirmed"]; + requestData = await _request?.refresh(); + request = requestData; // Update the parent request object + isPaid = true; - loading = false; - statuses = []; + status = checkStatus(requestData); isRequestPayed = true; } catch (err) { console.error("Something went wrong while paying : ", err); + + if ( + String(err).includes("ACTION_REJECTED") || + String(err).includes("User rejected") + ) { + toast.error("Transaction cancelled", { + description: "You rejected the transaction", + }); + } else { + toast.error("Payment failed", { + description: err instanceof Error ? err.message : String(err), + }); + } + + statuses = statuses.map((status) => ({ + ...status, + done: false, + })); + } finally { loading = false; - statuses = []; + isSigningTransaction = false; } }; @@ -309,6 +410,11 @@ ) { await approvers[paymentNetworkExtension.id](); } + + const approveStatus = statuses.find((s) => s.name === "APPROVE_ERC20"); + + if (approveStatus) approveStatus.done = true; + statuses = [...statuses]; } catch (err) { console.error("Something went wrong while approving ERC20: ", err); } finally { @@ -323,8 +429,14 @@ signer = await getEthersSigner(wagmiConfig); correctChain = true; + + // Update network switch status + const networkStatus = statuses.find((s) => s.name === "CORRECT_NETWORK"); + if (networkStatus) networkStatus.done = true; } catch (err) { console.error("Something went wrong while switching networks: ", err); + toast.error("Failed to switch network"); + throw err; } } @@ -356,6 +468,36 @@ ? `${integerPart}.${decimalPart.substring(0, maxDecimalDigits)}` : value; } + + async function handlePayment() { + try { + await switchNetworkIfNeeded(network || "mainnet"); + + if ( + !approved && + paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20 + ) { + await approve(); + + const approveStatus = statuses.find((s) => s.name === "APPROVE_ERC20"); + if (approveStatus) approveStatus.done = true; + + return; + } + + await payTheRequest(); + } catch (err) { + console.error("Error during payment process:", err); + toast.error("Payment process failed", { + description: String(err), + }); + // Reset statuses on error make them all false + statuses = statuses.map((status) => ({ + ...status, + done: false, + })); + } + }
-
-
- {#if statuses.length > 0 && loading} - {#each statuses as status, index (index)} -
- {status || "-"} - {#if (index === 0 && statuses.length === 2) || (index === 1 && statuses.length === 3)} - - - - {/if} -
- {/each} - {/if} + {#if !isPaid && !isPayee} +
+
+ {#if statuses?.length > 0} +
    + {#each statuses as status, index} +
  • + + {#if status.done} + + + + {:else} + + + + {/if} + + {status.message} +
    +
  • + {/each} +
+ {/if} +
+ {/if} + {#if !isPayee && !unsupportedNetwork && !isPaid && !isSigningTransaction} +
-
{#if unsupportedNetwork}
Unsupported payment network!
{/if} @@ -810,8 +986,7 @@ .status-container { display: flex; align-items: center; - gap: 10px; - justify-content: space-between; + justify-content: center; margin-top: 1rem; } @@ -819,6 +994,8 @@ display: flex; flex-direction: column; gap: 10px; + width: 100%; + margin-bottom: 32px; } .status { @@ -834,6 +1011,78 @@ gap: 0.25rem; } + .status-list { + display: flex; + align-items: center; + list-style: none; + padding: 0; + margin-left: 85px; + } + + .status-item { + display: flex; + align-items: center; + position: relative; + text-align: center; + width: 45%; + } + + .status-list:has(:nth-child(3):last-child) .status-item { + width: 35%; + } + + .status-item:first-child { + padding-left: 50px; + } + + .status-item:first-child .status-text { + padding-left: 50px; + } + + .status-item:last-child { + width: 20%; + } + + .status-item:last-child .progress-line { + width: 170px; + } + + .progress-line { + position: absolute; + left: 40%; + height: 8px; + z-index: 0; + transform: translateX(-50%); + width: 300px; + border-radius: 100px; + z-index: 10; + } + + .status-icon-wrapper { + display: flex; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + border-radius: 9999px; + padding: 4px; + position: relative; + z-index: 20; + } + + .status-text { + font-size: 14px; + color: #272d41; + position: absolute; + top: -30px; + left: -30px; + } + + .checkmark { + margin-left: 5px; + color: #58e1a5; + } + .invoice-view-actions { padding: 0.75rem 1rem; font-size: 0.875rem; @@ -850,17 +1099,6 @@ height: fit-content !important; } - .loading { - padding: 0.75rem 1rem; - font-size: 0.875rem; - font-weight: 500; - text-align: center; - border-radius: 0.5rem; - background-color: var(--mainColor); - color: white; - animation: pulse 1s infinite; - } - .unsupported-network { font-size: 12px; color: #e89e14ee; @@ -884,10 +1122,22 @@ background-color: var(--secondaryColor); } + .bg-blue { + background-color: #759aff; + } + .bg-green { background-color: #0bb489; } + .bg-success { + background-color: #cdf6e4; + } + + .bg-waiting { + background-color: #c7e7ff; + } + .bg-zinc { background-color: #a1a1aa; } @@ -907,4 +1157,10 @@ .email-link:hover { text-decoration: underline; } + + :global(.pay-button) { + padding: 8px 12px !important; + width: fit-content; + margin-left: auto; + } diff --git a/packages/invoice-dashboard/src/lib/view-requests.svelte b/packages/invoice-dashboard/src/lib/view-requests.svelte index 868f2102..90110586 100644 --- a/packages/invoice-dashboard/src/lib/view-requests.svelte +++ b/packages/invoice-dashboard/src/lib/view-requests.svelte @@ -53,11 +53,20 @@ export let requestNetwork: RequestNetwork | null | undefined; export let currencies: CurrencyTypes.CurrencyInput[] = []; - let cipherProvider: CipherProviderTypes.ICipherProvider & { - getSessionSignatures: (signer: ethers.Signer, walletAddress: `0x${string}`) => Promise; - } | undefined; + let cipherProvider: + | (CipherProviderTypes.ICipherProvider & { + getSessionSignatures: ( + signer: ethers.Signer, + walletAddress: `0x${string}` + ) => Promise; + }) + | undefined; - let sliderValueForDecryption = JSON.parse(localStorage?.getItem('isDecryptionEnabled') ?? "false") ? "on" : "off"; + let sliderValueForDecryption = JSON.parse( + localStorage?.getItem("isDecryptionEnabled") ?? "false" + ) + ? "on" + : "off"; let signer: `0x${string}` | undefined; let activeConfig = config ? config : defaultConfig; @@ -93,18 +102,27 @@ let sortOrder = "desc"; let sortColumn = "timestamp"; - $: { - if (wagmiConfig) { - account = getAccount(wagmiConfig); - } - } + let previousAddress: string | undefined; $: { - if (account?.address) { - tick().then(() => { - enableDecryption(); - getRequests(); - }); + if (wagmiConfig) { + const newAccount = getAccount(wagmiConfig); + if (newAccount?.address !== previousAddress) { + account = newAccount; + previousAddress = newAccount?.address; + + if (newAccount?.address) { + tick().then(() => { + enableDecryption(); + getRequests(); + }); + } else { + requests = []; + activeRequest = undefined; + previousWalletAddress = undefined; + previousNetwork = undefined; + } + } } } @@ -113,9 +131,11 @@ onMount(() => { unwatchAccount = watchAccount(wagmiConfig, { onChange(data) { - if (data?.address !== account?.address) { + if (data?.address !== previousAddress) { account = data; - if (account?.address) { + previousAddress = data?.address; + + if (data?.address) { getRequests(); } else { requests = []; @@ -132,12 +152,7 @@ if (typeof unwatchAccount === "function") unwatchAccount(); }); - $: cipherProvider = requestNetwork?.getCipherProvider() as CipherProviderTypes.ICipherProvider & { - getSessionSignatures: ( - signer: ethers.Signer, - walletAddress: `0x${string}` - ) => Promise; - }; + $: cipherProvider = undefined; $: { signer = account?.address; @@ -340,7 +355,7 @@ BigInt(request.expectedAmount), currencyInfo?.decimals ?? 18 ), - currencySymbol: currencyInfo!.symbol, + currencySymbol: currencyInfo?.symbol, paymentCurrencies, }; } @@ -400,16 +415,16 @@ const handleRemoveSelectedRequest = () => { activeRequest = undefined; }; - + const enableDecryption = async () => { loading = true; - if(sliderValueForDecryption === 'on') { + if (sliderValueForDecryption === "on") { try { const signer = await getEthersSigner(wagmiConfig); if (signer && account?.address) { await cipherProvider?.getSessionSignatures(signer, account.address); cipherProvider?.enableDecryption(true); - localStorage?.setItem('isDecryptionEnabled', JSON.stringify(true)); + localStorage?.setItem("isDecryptionEnabled", JSON.stringify(true)); } } catch (error) { console.error("Failed to enable decryption:", error); @@ -419,13 +434,12 @@ } } else { cipherProvider?.enableDecryption(false); - localStorage?.setItem('isDecryptionEnabled', JSON.stringify(false)); + localStorage?.setItem("isDecryptionEnabled", JSON.stringify(false)); } await getRequests(); loading = false; - } + }; $: sliderValueForDecryption, enableDecryption(); -
{#if cipherProvider}
- +
{/if}
- + {/each} - {:else} - {#if loading} - - {/if} + {:else if loading} + {/if}