From 33c8d31bf2b0b07d60892b7d57cf89e5c071e864 Mon Sep 17 00:00:00 2001 From: sstefdev Date: Sun, 24 Nov 2024 16:13:03 +0100 Subject: [PATCH 01/18] feat: added single-invoice web component --- package.json | 1 + packages/payment-widget/tsconfig.json | 6 +- packages/single-invoice/README.md | 53 + packages/single-invoice/package.json | 66 ++ packages/single-invoice/src/lib/index.ts | 1 + .../src/lib/react/SingleInvoice.d.ts | 24 + .../src/lib/react/SingleInvoice.jsx | 19 + .../single-invoice/src/lib/react/global.d.ts | 8 + .../src/lib/single-invoice.svelte | 943 ++++++++++++++++++ .../single-invoice/src/lib/types/index.ts | 61 ++ .../src/lib/utils/capitalize.d.ts | 1 + .../src/lib/utils/capitalize.ts | 1 + .../src/lib/utils/chainlink.d.ts | 10 + .../single-invoice/src/lib/utils/chainlink.ts | 40 + .../src/lib/utils/conversion.d.ts | 13 + .../src/lib/utils/conversion.ts | 50 + .../src/lib/utils/debounce.d.ts | 1 + .../single-invoice/src/lib/utils/debounce.ts | 11 + .../src/lib/utils/ethersAdapterProvider.d.ts | 6 + .../src/lib/utils/formatAddress.d.ts | 1 + .../src/lib/utils/formatAddress.ts | 16 + .../src/lib/utils/generateInvoice.d.ts | 6 + .../lib/utils/getConversionPaymentValues.d.ts | 42 + .../lib/utils/getConversionPaymentValues.ts | 168 ++++ .../single-invoice/src/lib/utils/index.d.ts | 4 + .../single-invoice/src/lib/utils/index.ts | 4 + .../src/lib/utils/loadScript.d.ts | 1 + .../src/lib/utils/wallet-utils.d.ts | 8 + .../src/lib/utils/wallet-utils.ts | 38 + packages/single-invoice/svelte.config.js | 11 + packages/single-invoice/tsconfig.json | 23 + packages/single-invoice/tsconfig.react.json | 15 + packages/single-invoice/vite.config.ts | 5 + packages/single-invoice/vite.wc.config.ts | 42 + shared/components/status-label.svelte | 103 ++ shared/utils/capitalize.ts | 2 + shared/utils/checkStatus.ts | 37 + 37 files changed, 1838 insertions(+), 3 deletions(-) create mode 100644 packages/single-invoice/README.md create mode 100644 packages/single-invoice/package.json create mode 100644 packages/single-invoice/src/lib/index.ts create mode 100644 packages/single-invoice/src/lib/react/SingleInvoice.d.ts create mode 100644 packages/single-invoice/src/lib/react/SingleInvoice.jsx create mode 100644 packages/single-invoice/src/lib/react/global.d.ts create mode 100644 packages/single-invoice/src/lib/single-invoice.svelte create mode 100644 packages/single-invoice/src/lib/types/index.ts create mode 100644 packages/single-invoice/src/lib/utils/capitalize.d.ts create mode 100644 packages/single-invoice/src/lib/utils/capitalize.ts create mode 100644 packages/single-invoice/src/lib/utils/chainlink.d.ts create mode 100644 packages/single-invoice/src/lib/utils/chainlink.ts create mode 100644 packages/single-invoice/src/lib/utils/conversion.d.ts create mode 100644 packages/single-invoice/src/lib/utils/conversion.ts create mode 100644 packages/single-invoice/src/lib/utils/debounce.d.ts create mode 100644 packages/single-invoice/src/lib/utils/debounce.ts create mode 100644 packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts create mode 100644 packages/single-invoice/src/lib/utils/formatAddress.d.ts create mode 100644 packages/single-invoice/src/lib/utils/formatAddress.ts create mode 100644 packages/single-invoice/src/lib/utils/generateInvoice.d.ts create mode 100644 packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts create mode 100644 packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts create mode 100644 packages/single-invoice/src/lib/utils/index.d.ts create mode 100644 packages/single-invoice/src/lib/utils/index.ts create mode 100644 packages/single-invoice/src/lib/utils/loadScript.d.ts create mode 100644 packages/single-invoice/src/lib/utils/wallet-utils.d.ts create mode 100644 packages/single-invoice/src/lib/utils/wallet-utils.ts create mode 100644 packages/single-invoice/svelte.config.js create mode 100644 packages/single-invoice/tsconfig.json create mode 100644 packages/single-invoice/tsconfig.react.json create mode 100644 packages/single-invoice/vite.config.ts create mode 100644 packages/single-invoice/vite.wc.config.ts create mode 100644 shared/components/status-label.svelte create mode 100644 shared/utils/capitalize.ts create mode 100644 shared/utils/checkStatus.ts diff --git a/package.json b/package.json index 016e1338..3643fb0b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build:dashboard": "turbo run build --filter=@requestnetwork/invoice-dashboard", "build:stakeholder": "turbo run build --filter=@requestnetwork/add-stakeholder", "build:payment-widget": "turbo run build --filter=@requestnetwork/payment-widget", + "build:single-invoice": "turbo run build --filter=@requestnetwork/single-invoice", "link:react": "npm link $npm_config_app_path/node_modules/react $npm_config_app_path/node_modules/react-dom", "link:all": "npm run link:react --app-path=$npm_config_app_path && for d in packages/*; do (cd $d && npm link); done", "unlink:all": "for d in packages/*; do (cd $d && npm unlink); done" diff --git a/packages/payment-widget/tsconfig.json b/packages/payment-widget/tsconfig.json index 3993679a..7766439c 100644 --- a/packages/payment-widget/tsconfig.json +++ b/packages/payment-widget/tsconfig.json @@ -16,8 +16,8 @@ "baseUrl": ".", "paths": { "$src/*": ["src/*"], - "$utils/*": ["src/lib/utils/*"], - }, + "$utils/*": ["src/lib/utils/*"] + } }, - "include": ["src/**/*", "src/*.d.ts"], + "include": ["src/**/*", "src/*.d.ts"] } diff --git a/packages/single-invoice/README.md b/packages/single-invoice/README.md new file mode 100644 index 00000000..f919758d --- /dev/null +++ b/packages/single-invoice/README.md @@ -0,0 +1,53 @@ +# Request Network Payment Widget Web Component + +A web component for accepting crypto payments using Request Network. + +## Introduction + +The Payment Widget web component is a pre-built component that allows users to offer crypto payment options using Request Network without having to implement it themselves. It is built using Svelte but compiled to a Web Component, making it usuable in any web environment, regardless of the framework. + +## Installation + +To install the component, use npm: + +```bash +npm install @requestnetwork/payment-widget +``` + +## Usage + +### Usage in React + +```tsx +import PaymentWidget from "@requestnetwork/payment-widget/react"; + +export default function PaymentPage() { + return ( + { + console.log(request); + }} + onError={(error) => { + console.error(error); + }} + /> + ); +} +``` + +## Additional Information + +For more information, see the [Request Network documentation](https://docs.request.network/). diff --git a/packages/single-invoice/package.json b/packages/single-invoice/package.json new file mode 100644 index 00000000..46cb1a41 --- /dev/null +++ b/packages/single-invoice/package.json @@ -0,0 +1,66 @@ +{ + "name": "@requestnetwork/single-invoice", + "version": "0.1.0", + "main": "./dist/web-component.umd.cjs", + "scripts": { + "dev": "vite dev", + "build": "npm run package", + "build:wc": "vite build -c vite.wc.config.ts", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && npm run build:wc && publint", + "prepublishOnly": "npm run package", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "clean": "rm -rf dist && rm -rf .svelte-kit", + "check-release-type": "bash ../../scripts/check-release-type.sh", + "publish-next-release": "bash ../../scripts/publish-next-release.sh" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "require": "./dist/web-component.umd.cjs", + "default": "./dist/web-component.js" + }, + "./react": { + "types": "./dist/react/SingleInvoice.d.ts", + "default": "./dist/react/SingleInvoice.jsx" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.5.2", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "sass": "^1.77.8", + "svelte": "^4.0.5", + "svelte-check": "^3.6.0", + "typescript": "^5.0.0", + "vite": "^4.4.2" + }, + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@requestnetwork/payment-processor": "0.48.0", + "@requestnetwork/request-client.js": "0.50.0", + "@requestnetwork/web3-signature": "0.8.1", + "@web3modal/ethers5": "^5.0.11", + "ethers": "^5.7.2", + "vite-plugin-node-polyfills": "^0.22.0" + } +} diff --git a/packages/single-invoice/src/lib/index.ts b/packages/single-invoice/src/lib/index.ts new file mode 100644 index 00000000..39307c45 --- /dev/null +++ b/packages/single-invoice/src/lib/index.ts @@ -0,0 +1 @@ +export { default as SingleInvoice } from "./single-invoice.svelte"; diff --git a/packages/single-invoice/src/lib/react/SingleInvoice.d.ts b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts new file mode 100644 index 00000000..988e782b --- /dev/null +++ b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts @@ -0,0 +1,24 @@ +import React from "react"; + +export interface SingleInvoiceProps { + requestId: string; + config: IConfig; + wagmiConfig: WagmiConfig; + requestNetwork: RequestNetwork | null | undefined; + currencies: CurrencyTypes.CurrencyInput[]; +} + +/** + * SingleInvoice is a React component that displays a single invoice. + * + * @param {SingleInvoiceProps} props - The component props + * @returns {JSX.Element} + * + * @example + * + */ +declare const SingleInvoice: React.FC; + +export default SingleInvoice; diff --git a/packages/single-invoice/src/lib/react/SingleInvoice.jsx b/packages/single-invoice/src/lib/react/SingleInvoice.jsx new file mode 100644 index 00000000..f1a986d4 --- /dev/null +++ b/packages/single-invoice/src/lib/react/SingleInvoice.jsx @@ -0,0 +1,19 @@ +// @ts-nocheck +import React, { useLayoutEffect, useRef } from "react"; +import("../web-component"); + +export const SingleInvoice = (props) => { + const widgetRef = useRef(null); + + useLayoutEffect(() => { + if (widgetRef.current) { + Object.entries(props).forEach(([key, value]) => { + widgetRef.current[key] = value; + }); + } + }, [props, widgetRef?.current]); + + return React.createElement("single-invoice", { ref: widgetRef }); +}; + +export default SingleInvoice; diff --git a/packages/single-invoice/src/lib/react/global.d.ts b/packages/single-invoice/src/lib/react/global.d.ts new file mode 100644 index 00000000..a0e3e3cc --- /dev/null +++ b/packages/single-invoice/src/lib/react/global.d.ts @@ -0,0 +1,8 @@ +declare namespace JSX { + interface IntrinsicElements { + "single-invoice": React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + >; + } +} diff --git a/packages/single-invoice/src/lib/single-invoice.svelte b/packages/single-invoice/src/lib/single-invoice.svelte new file mode 100644 index 00000000..2fdd26bd --- /dev/null +++ b/packages/single-invoice/src/lib/single-invoice.svelte @@ -0,0 +1,943 @@ + + +
+
+

Issued on: {formatDate(request?.contentData?.creationDate || "-")}

+

+ Due by: {formatDate(request?.contentData?.paymentTerms?.dueDate || "-")} +

+
+

+ Invoice #{request?.contentData?.invoiceNumber || "-"} + + + { + try { + await exportToPDF( + request, + currency, + paymentCurrencies, + config.logo + ); + } catch (error) { + toast.error(`Failed to export PDF`, { + description: `${error}`, + action: { + label: "X", + onClick: () => console.info("Close"), + }, + }); + console.error("Failed to export PDF:", error); + } + }} + /> + +

+
+

From:

+

{request?.payee?.value || "-"}

+
+ {#if sellerInfo.length > 0} +
+ {#each sellerInfo as { value, isCompany, isEmail }} +

+ {#if isEmail} + + {:else if isCompany} + {value} + {:else} + {value} + {/if} +

+ {/each} +
+ {/if} +
+
+

Billed to:

+

{request?.payer?.value || "-"}

+
+ {#if buyerInfo.length > 0} +
+ {#each buyerInfo as { value, isCompany, isEmail }} +

+ {#if isEmail} + + {:else if isCompany} + {value} + {:else} + {value} + {/if} +

+ {/each} +
+ {/if} + +

+ Payment Chain: + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.network || "-" + : ""} +

+

+ Invoice Currency: + {currency?.symbol || "-"} +

+ +

+ Settlement Currency: + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.symbol || "-" + : ""} +

+ + {#if request?.contentData?.invoiceItems} +
+ + + + + + + + + + + + + {#each firstItems as item, index (index)} + + + + + + + + + {/each} + +
DescriptionQtyUnit PriceDiscountTaxAmount
+

{item.name || "-"}

+
{item.quantity || "-"}{item.unitPrice + ? formatUnits(item.unitPrice, currency?.decimals ?? 18) + : "-"}{item.discount + ? formatUnits(item.discount, currency?.decimals ?? 18) + : "-"}{Number(item.tax.amount || "-")}{truncateNumberString( + formatUnits( + // @ts-expect-error + calculateItemTotal(item), + currency?.decimals ?? 18 + ), + 2 + )}
+
+ {#if otherItems.length > 0} + +
+ + + + + + + + + + + + + {#each otherItems as item, index (index)} + + + + + + + + + {/each} +
+

+ {item.name || "-"} +

+
{item.quantity || "-"}{item.unitPrice + ? formatUnits(item.unitPrice, currency?.decimals ?? 18) + : "-"}{item.discount + ? formatUnits(item.discount, currency?.decimals ?? 18) + : "-"}{Number(item.tax.amount || "-")}{truncateNumberString( + formatUnits( + // @ts-expect-error + calculateItemTotal(item), + currency?.decimals ?? 18 + ), + 2 + )}
+
+
+ {/if} + {/if} + {#if request?.contentData.note} +
+

+ Memo:
+ {request.contentData.note || "-"} +

+
+ {/if} +
+ {#if request?.contentData?.miscellaneous?.labels} + {#each request?.contentData?.miscellaneous?.labels as label, index (index)} +
+ {label || "-"} +
+ {/each} + {/if} +
+
+
+ {#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 loading} +
Loading...
+ {:else if !correctChain && !isPayee} +
+
+ {#if unsupportedNetwork} +
Unsupported payment network!
+ {/if} +
+ + diff --git a/packages/single-invoice/src/lib/types/index.ts b/packages/single-invoice/src/lib/types/index.ts new file mode 100644 index 00000000..31e6289e --- /dev/null +++ b/packages/single-invoice/src/lib/types/index.ts @@ -0,0 +1,61 @@ +import type { CURRENCY_ID, NETWORK_LABEL } from "../utils/currencies"; + +export interface Address { + "street-address"?: string; + locality?: string; + region?: string; + "country-name"?: string; + "postal-code"?: string; +} + +export interface SellerInfo { + logo?: string; + name?: string; + email?: string; + firstName?: string; + lastName?: string; + businessName?: string; + phone?: string; + address?: Address; + taxRegistration?: string; + companyRegistration?: string; +} + +export interface BuyerInfo { + email?: string; + firstName?: string; + lastName?: string; + businessName?: string; + phone?: string; + address?: Address; + taxRegistration?: string; + companyRegistration?: string; +} + +export type ProductInfo = { + name?: string; + description?: string; + image?: string; +}; + +export type AmountInUSD = number; + +export type CurrencyID = (typeof CURRENCY_ID)[keyof typeof CURRENCY_ID]; +export type SupportedCurrencies = [CurrencyID, ...CurrencyID[]]; + +export type Currency = { + id: string; + hash: string; + address?: string; + network: keyof typeof NETWORK_LABEL; + decimals: number; + symbol: string; + type: "ERC20" | "ETH"; + name?: string; +}; + +export type PaymentStep = + | "currency" + | "buyer-info" + | "confirmation" + | "complete"; diff --git a/packages/single-invoice/src/lib/utils/capitalize.d.ts b/packages/single-invoice/src/lib/utils/capitalize.d.ts new file mode 100644 index 00000000..87012def --- /dev/null +++ b/packages/single-invoice/src/lib/utils/capitalize.d.ts @@ -0,0 +1 @@ +export declare const capitalize: (str: string) => string; diff --git a/packages/single-invoice/src/lib/utils/capitalize.ts b/packages/single-invoice/src/lib/utils/capitalize.ts new file mode 100644 index 00000000..ac21f51c --- /dev/null +++ b/packages/single-invoice/src/lib/utils/capitalize.ts @@ -0,0 +1 @@ +export const capitalize = (str: string) => (str && str[0].toUpperCase() + str.slice(1)) || "" diff --git a/packages/single-invoice/src/lib/utils/chainlink.d.ts b/packages/single-invoice/src/lib/utils/chainlink.d.ts new file mode 100644 index 00000000..85bae116 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/chainlink.d.ts @@ -0,0 +1,10 @@ +import { CurrencyTypes } from "@requestnetwork/types"; +import { BigNumber, providers } from "ethers"; +export declare const getChainlinkRate: (from: CurrencyTypes.CurrencyDefinition, to: CurrencyTypes.CurrencyDefinition, { network, provider, currencyManager, }: { + network: CurrencyTypes.EvmChainName; + provider: providers.Provider; + currencyManager: CurrencyTypes.ICurrencyManager; +}) => Promise<{ + value: BigNumber; + decimals: number; +} | null>; diff --git a/packages/single-invoice/src/lib/utils/chainlink.ts b/packages/single-invoice/src/lib/utils/chainlink.ts new file mode 100644 index 00000000..a7c72b1f --- /dev/null +++ b/packages/single-invoice/src/lib/utils/chainlink.ts @@ -0,0 +1,40 @@ +import { isISO4217Currency } from "@requestnetwork/currency"; +import { chainlinkConversionPath } from "@requestnetwork/smart-contracts"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { BigNumber, providers } from "ethers"; + +export const getChainlinkRate = async ( + from: CurrencyTypes.CurrencyDefinition, + to: CurrencyTypes.CurrencyDefinition, + { + network, + provider, + currencyManager, + }: { + network: CurrencyTypes.EvmChainName; + provider: providers.Provider; + currencyManager: CurrencyTypes.ICurrencyManager; + }, +) => { + try { + const chainlink = chainlinkConversionPath.connect(network, provider); + const path = currencyManager.getConversionPath(from, to, network); + if (!path) return null; + const result = await chainlink.getRate(path); + if (!result) return null; + + // ChainlinkConversionPath uses 8 decimals for fiat. + const fromDecimals = isISO4217Currency(from) ? 8 : from.decimals; + const toDecimals = isISO4217Currency(to) ? 8 : to.decimals; + const value = result.rate + .mul(BigNumber.from(10).pow(fromDecimals)) + .div(BigNumber.from(10).pow(toDecimals)); + return { + value, + decimals: result.decimals.toString().length - 1, + }; + } catch (e) { + console.error('Error fetching Chainlink rate:', e); + return null; + } +}; diff --git a/packages/single-invoice/src/lib/utils/conversion.d.ts b/packages/single-invoice/src/lib/utils/conversion.d.ts new file mode 100644 index 00000000..de34c35e --- /dev/null +++ b/packages/single-invoice/src/lib/utils/conversion.d.ts @@ -0,0 +1,13 @@ +import { CurrencyTypes } from "@requestnetwork/types"; +import { providers } from "ethers"; +export declare const lowVolatilityTokens: string[]; +export declare const getSlippageMargin: (currency: CurrencyTypes.CurrencyInput) => 1.03 | 1.01; +/** + * Get the conversion rate between two currencies. + */ +export declare const getConversionRate: ({ from, to, currencyManager, provider, }: { + from: CurrencyTypes.CurrencyDefinition; + to: CurrencyTypes.CurrencyDefinition; + currencyManager?: CurrencyTypes.ICurrencyManager; + provider?: providers.Provider; +}) => Promise; diff --git a/packages/single-invoice/src/lib/utils/conversion.ts b/packages/single-invoice/src/lib/utils/conversion.ts new file mode 100644 index 00000000..e42a6da3 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/conversion.ts @@ -0,0 +1,50 @@ +import { isISO4217Currency } from "@requestnetwork/currency"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { EvmChainName } from "@requestnetwork/types/dist/currency-types"; +import { providers, utils } from "ethers"; +import { getChainlinkRate } from './chainlink' + +/** + * Maximum slippage (swap) or conversion evolution (conversion pn) + */ +const MAX_SLIPPAGE_DEFAULT = 1.03; +const MAX_SLIPPAGE_LOW_VOLATILITY = 1.01; +export const lowVolatilityTokens = ["DAI", "USDC", "USDT"]; + +export const getSlippageMargin = (currency: CurrencyTypes.CurrencyInput) => { + return lowVolatilityTokens.includes(currency.symbol) + ? MAX_SLIPPAGE_LOW_VOLATILITY + : MAX_SLIPPAGE_DEFAULT; +}; + +/** + * Get the conversion rate between two currencies. + */ +export const getConversionRate = async ({ + from, + to, + currencyManager, + provider, +}: { + from: CurrencyTypes.CurrencyDefinition; + to: CurrencyTypes.CurrencyDefinition; + currencyManager?: CurrencyTypes.ICurrencyManager; + provider?: providers.Provider; +}): Promise => { + if (!isISO4217Currency(to) && currencyManager && provider) { + const network = to.network as EvmChainName; + try { + const chainlinkRate = await getChainlinkRate(from, to, { + network, + currencyManager, + provider, + }); + if (chainlinkRate) { + return Number(utils.formatUnits(chainlinkRate.value, chainlinkRate.decimals)) + } + } catch (e) { + console.error("Error getting chainlink rate", e); + throw e; + } + } +}; diff --git a/packages/single-invoice/src/lib/utils/debounce.d.ts b/packages/single-invoice/src/lib/utils/debounce.d.ts new file mode 100644 index 00000000..5a22f761 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/debounce.d.ts @@ -0,0 +1 @@ +export declare const debounce: (func: Function, wait: number) => (...args: any[]) => void; diff --git a/packages/single-invoice/src/lib/utils/debounce.ts b/packages/single-invoice/src/lib/utils/debounce.ts new file mode 100644 index 00000000..1462f81b --- /dev/null +++ b/packages/single-invoice/src/lib/utils/debounce.ts @@ -0,0 +1,11 @@ +export const debounce = (func: Function, wait: number) => { + let timeout: NodeJS.Timeout; + return (...args: any[]) => { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; diff --git a/packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts b/packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts new file mode 100644 index 00000000..e6b4285b --- /dev/null +++ b/packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts @@ -0,0 +1,6 @@ +import { providers } from "ethers"; +import type { Chain, Client, Transport } from "viem"; +export declare function clientToProvider(client: Client): providers.JsonRpcProvider | providers.FallbackProvider; +export declare function useEthersProvider({ chainId, }?: { + chainId?: number | undefined; +}): providers.JsonRpcProvider | providers.FallbackProvider | undefined; diff --git a/packages/single-invoice/src/lib/utils/formatAddress.d.ts b/packages/single-invoice/src/lib/utils/formatAddress.d.ts new file mode 100644 index 00000000..8c3caee1 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/formatAddress.d.ts @@ -0,0 +1 @@ +export declare const formatAddress: (address: string, first?: number, last?: number) => string; diff --git a/packages/single-invoice/src/lib/utils/formatAddress.ts b/packages/single-invoice/src/lib/utils/formatAddress.ts new file mode 100644 index 00000000..cf55278b --- /dev/null +++ b/packages/single-invoice/src/lib/utils/formatAddress.ts @@ -0,0 +1,16 @@ +import { getAddress } from "viem"; +import { checkAddress } from "@requestnetwork/shared-utils/checkEthAddress"; + +export const formatAddress = ( + address: string, + first: number = 6, + last: number = 4 +): string => { + if (!checkAddress(address)) { + console.error("Invalid address!"); + } + + const checksumAddress = getAddress(address); + + return `${checksumAddress.slice(0, first)}...${checksumAddress.slice(-last)}`; +}; diff --git a/packages/single-invoice/src/lib/utils/generateInvoice.d.ts b/packages/single-invoice/src/lib/utils/generateInvoice.d.ts new file mode 100644 index 00000000..b00d0a1b --- /dev/null +++ b/packages/single-invoice/src/lib/utils/generateInvoice.d.ts @@ -0,0 +1,6 @@ +declare global { + interface Window { + html2pdf: any; + } +} +export declare const exportToPDF: (invoice: any, currency: any, logo: string) => Promise; diff --git a/packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts b/packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts new file mode 100644 index 00000000..02b4ebf6 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts @@ -0,0 +1,42 @@ +import { CurrencyManager } from "@requestnetwork/currency"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { BigNumber, BigNumberish, providers } from "ethers"; +export type SlippageLevel = "safe" | "risky"; +interface ConversionPaymentValues { + conversion: { + currency: CurrencyTypes.ICurrency; + maxToSpend: string; + currencyManager: CurrencyManager; + }; + slippageLevel: SlippageLevel; + totalAmountInPaymentCurrency: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + safeBalance: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + rate: string; +} +export declare const formatUnits: (amount: BigNumber, token: CurrencyTypes.CurrencyDefinition) => string; +export declare const toFixedDecimal: (numberToFormat: number, decimals?: number) => number; +export declare const amountToFixedDecimal: (amount: BigNumberish, currency: CurrencyTypes.CurrencyDefinition, decimals?: number) => number; +/** + * Converts a number, even floating, to a BigNumber + * @param amount Number to convert, may float + * @param token Token + */ +export declare const bigAmountify: (amount: number, token: Pick) => BigNumber; +/** + * Utility method to compute various settings associated to a payment involving conversion only + */ +export declare const getConversionPaymentValues: ({ baseAmount, denominationCurrency, selectedPaymentCurrency, currencyManager, provider, fromAddress, }: { + baseAmount: number; + denominationCurrency: CurrencyTypes.CurrencyDefinition; + selectedPaymentCurrency: CurrencyTypes.CurrencyDefinition; + currencyManager: CurrencyManager; + provider?: providers.Provider; + fromAddress?: string; +}) => Promise; +export {}; diff --git a/packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts b/packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts new file mode 100644 index 00000000..4afc8b6e --- /dev/null +++ b/packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts @@ -0,0 +1,168 @@ +import { CurrencyManager } from "@requestnetwork/currency"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { getAnyErc20Balance } from "@requestnetwork/payment-processor"; +import { BigNumber, BigNumberish, providers, utils } from "ethers"; + +import { getConversionRate, getSlippageMargin } from './conversion' + +export type SlippageLevel = "safe" | "risky"; + +interface ConversionPaymentValues { + conversion: { + currency: CurrencyTypes.ICurrency; + maxToSpend: string; + currencyManager: CurrencyManager; + }; + slippageLevel: SlippageLevel; + totalAmountInPaymentCurrency: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + safeBalance: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + rate: string; +} + +export const formatUnits = ( + amount: BigNumber, + token: CurrencyTypes.CurrencyDefinition, +): string => { + return utils.formatUnits(amount, token.decimals); +}; + +export const toFixedDecimal = (numberToFormat: number, decimals?: number) => { + const MAX_DECIMALS = decimals !== undefined ? decimals : 5; + return Number(numberToFormat.toFixed(MAX_DECIMALS)); +}; + +export const amountToFixedDecimal = ( + amount: BigNumberish, + currency: CurrencyTypes.CurrencyDefinition, + decimals?: number, +) => { + return toFixedDecimal( + Number.parseFloat(formatUnits(BigNumber.from(amount), currency)), + decimals, + ); +}; + +/** + * Converts a number, even floating, to a BigNumber + * @param amount Number to convert, may float + * @param token Token + */ +export const bigAmountify = ( + amount: number, + token: Pick, +): BigNumber => { + let [whole, decimals] = amount.toString().split("."); + let pow = "0"; + let powSign = true; + + if (decimals && decimals.includes("e")) { + powSign = !decimals.includes("e-"); + [decimals, pow] = powSign ? decimals.split("e") : decimals.split("e-"); + whole = whole; + } + + const wholeBn = utils.parseUnits(whole, token.decimals); + const power = BigNumber.from(10).pow(pow); + + if (decimals) { + const decimalsBn = utils + .parseUnits(decimals, token.decimals) + .div(BigNumber.from(10).pow(decimals.length)); + return powSign + ? wholeBn.add(decimalsBn).mul(power) + : wholeBn.add(decimalsBn).div(power); + } + return wholeBn; +}; + +/** + * Utility method to compute various settings associated to a payment involving conversion only + */ +export const getConversionPaymentValues = async ({ + baseAmount, + denominationCurrency, + selectedPaymentCurrency, + currencyManager, + provider, + fromAddress, +}: { + baseAmount: number; + denominationCurrency: CurrencyTypes.CurrencyDefinition; + selectedPaymentCurrency: CurrencyTypes.CurrencyDefinition; + currencyManager: CurrencyManager; + provider?: providers.Provider; + fromAddress?: string; +}): Promise => { + const conversionRate = await getConversionRate({ + from: denominationCurrency, + to: selectedPaymentCurrency, + currencyManager, + provider, + }); + + const minConversionAmount = bigAmountify( + baseAmount * Number(conversionRate), + selectedPaymentCurrency, + ); + + const safeConversionAmount = bigAmountify( + baseAmount * Number(conversionRate) * getSlippageMargin(selectedPaymentCurrency), + selectedPaymentCurrency, + ); + + const userBalance = BigNumber.from( + fromAddress && + provider && + "address" in selectedPaymentCurrency && + selectedPaymentCurrency.address + ? await getAnyErc20Balance( + selectedPaymentCurrency.address, + fromAddress, + provider, + ) + : safeConversionAmount, + ); + + const hasEnoughForSlippage = userBalance.gte(safeConversionAmount); + const hasEnough = userBalance.gte(minConversionAmount); + const isRisky = hasEnough && !hasEnoughForSlippage; + const slippageLevel = isRisky ? "risky" : ("safe" as SlippageLevel); + const conversionAmount = isRisky ? userBalance : safeConversionAmount; + + const conversion = { + currency: CurrencyManager.toStorageCurrency(selectedPaymentCurrency), + maxToSpend: conversionAmount.toString(), + currencyManager, + }; + + const totalAmountInPaymentCurrency = { + value: amountToFixedDecimal( + minConversionAmount, + selectedPaymentCurrency, + 4, + ), + currency: selectedPaymentCurrency, + }; + const safeBalance = { + value: amountToFixedDecimal( + safeConversionAmount, + selectedPaymentCurrency, + 4, + ), + currency: selectedPaymentCurrency, + }; + + return { + conversion, + slippageLevel, + totalAmountInPaymentCurrency, + safeBalance, + rate: conversionRate, + }; +}; diff --git a/packages/single-invoice/src/lib/utils/index.d.ts b/packages/single-invoice/src/lib/utils/index.d.ts new file mode 100644 index 00000000..97101620 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/index.d.ts @@ -0,0 +1,4 @@ +export { debounce } from "./debounce"; +export { formatAddress } from "./formatAddress"; +export { publicClientToProvider, getEthersSigner } from "./wallet-utils"; +export { capitalize } from "./capitalize"; diff --git a/packages/single-invoice/src/lib/utils/index.ts b/packages/single-invoice/src/lib/utils/index.ts new file mode 100644 index 00000000..97101620 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/index.ts @@ -0,0 +1,4 @@ +export { debounce } from "./debounce"; +export { formatAddress } from "./formatAddress"; +export { publicClientToProvider, getEthersSigner } from "./wallet-utils"; +export { capitalize } from "./capitalize"; diff --git a/packages/single-invoice/src/lib/utils/loadScript.d.ts b/packages/single-invoice/src/lib/utils/loadScript.d.ts new file mode 100644 index 00000000..4d5f7e32 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/loadScript.d.ts @@ -0,0 +1 @@ +export declare const loadScript: (src: string) => Promise; diff --git a/packages/single-invoice/src/lib/utils/wallet-utils.d.ts b/packages/single-invoice/src/lib/utils/wallet-utils.d.ts new file mode 100644 index 00000000..961786b6 --- /dev/null +++ b/packages/single-invoice/src/lib/utils/wallet-utils.d.ts @@ -0,0 +1,8 @@ +import { Config } from "@wagmi/core"; +import { providers } from "ethers"; +import type { Account, Chain, Client, Transport } from "viem"; +export declare const publicClientToProvider: (publicClient: any) => providers.JsonRpcProvider; +export declare function clientToSigner(client: Client): providers.JsonRpcSigner; +export declare function getEthersSigner(config: Config, { chainId }?: { + chainId?: number; +}): Promise; diff --git a/packages/single-invoice/src/lib/utils/wallet-utils.ts b/packages/single-invoice/src/lib/utils/wallet-utils.ts new file mode 100644 index 00000000..e6aa563f --- /dev/null +++ b/packages/single-invoice/src/lib/utils/wallet-utils.ts @@ -0,0 +1,38 @@ +import { Config, getConnectorClient } from "@wagmi/core"; +import { providers } from "ethers"; +import type { Account, Chain, Client, Transport } from "viem"; + +export const publicClientToProvider = (publicClient: any) => { + const { chains, provider } = publicClient; + const network = { + chainId: parseInt(chains[0].id, 16), + name: chains[0].name, + }; + + return new providers.JsonRpcProvider(provider.url as string, network); +}; + +export function clientToSigner(client: Client) { + const { account, chain, transport } = client; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + }; + + const provider = new providers.Web3Provider(transport, network); + const signer = provider.getSigner(account.address); + return signer; +} + +export async function getEthersSigner( + config: Config, + { chainId }: { chainId?: number } = {} +) { + try { + const client = await getConnectorClient(config, { chainId }); + return clientToSigner(client); + } catch (e) { + console.log("Failed to obtain client from getConnectorClient"); + } +} diff --git a/packages/single-invoice/svelte.config.js b/packages/single-invoice/svelte.config.js new file mode 100644 index 00000000..919755ca --- /dev/null +++ b/packages/single-invoice/svelte.config.js @@ -0,0 +1,11 @@ +import { vitePreprocess } from "@sveltejs/kit/vite"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + compilerOptions: { + customElement: true, + }, +}; + +export default config; diff --git a/packages/single-invoice/tsconfig.json b/packages/single-invoice/tsconfig.json new file mode 100644 index 00000000..7766439c --- /dev/null +++ b/packages/single-invoice/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "typeRoots": ["./src/types", "./node_modules/@types"], + "baseUrl": ".", + "paths": { + "$src/*": ["src/*"], + "$utils/*": ["src/lib/utils/*"] + } + }, + "include": ["src/**/*", "src/*.d.ts"] +} diff --git a/packages/single-invoice/tsconfig.react.json b/packages/single-invoice/tsconfig.react.json new file mode 100644 index 00000000..f4c55ad0 --- /dev/null +++ b/packages/single-invoice/tsconfig.react.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/react", + "declaration": true, + "jsx": "react-jsx", + "lib": ["ES2017", "DOM"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowJs": true, + "noImplicitAny": false, + "strict": false + }, + "include": ["src/lib/react/**/*"] +} diff --git a/packages/single-invoice/vite.config.ts b/packages/single-invoice/vite.config.ts new file mode 100644 index 00000000..b1edb182 --- /dev/null +++ b/packages/single-invoice/vite.config.ts @@ -0,0 +1,5 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; +export default defineConfig({ + plugins: [sveltekit()], +}); diff --git a/packages/single-invoice/vite.wc.config.ts b/packages/single-invoice/vite.wc.config.ts new file mode 100644 index 00000000..2526415a --- /dev/null +++ b/packages/single-invoice/vite.wc.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; + +export default defineConfig({ + define: { + global: "globalThis", + }, + plugins: [ + nodePolyfills({ + include: ["buffer", "crypto"], + exclude: ["http", "stream", "zlib", "assert"], + }), + svelte({ + compilerOptions: { + customElement: true, + }, + }), + ], + build: { + emptyOutDir: false, + sourcemap: true, + target: "modules", + lib: { + entry: "./src/lib/index.ts", + name: "<>", + fileName: "web-component", + }, + commonjsOptions: { + transformMixedEsModules: true, + }, + rollupOptions: { + external: ["react", "react-dom"], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, +}); diff --git a/shared/components/status-label.svelte b/shared/components/status-label.svelte new file mode 100644 index 00000000..3503b755 --- /dev/null +++ b/shared/components/status-label.svelte @@ -0,0 +1,103 @@ + + +
+ {capitalize(status)} +
+ + diff --git a/shared/utils/capitalize.ts b/shared/utils/capitalize.ts new file mode 100644 index 00000000..30824cd3 --- /dev/null +++ b/shared/utils/capitalize.ts @@ -0,0 +1,2 @@ +export const capitalize = (str: string) => + (str && str[0].toUpperCase() + str.slice(1)) || ""; diff --git a/shared/utils/checkStatus.ts b/shared/utils/checkStatus.ts new file mode 100644 index 00000000..2bba38e9 --- /dev/null +++ b/shared/utils/checkStatus.ts @@ -0,0 +1,37 @@ +import { capitalize } from "./capitalize"; +import { Types } from "@requestnetwork/request-client.js"; + +export const checkStatus = (request: Types.IRequestDataWithEvents | null) => { + const balance = BigInt(request?.balance?.balance ?? 0); + const expectedAmount = BigInt(request?.expectedAmount ?? 0); + const today = new Date(); + const dueDate = new Date(request?.contentData?.paymentTerms?.dueDate); + const isPaid = balance >= expectedAmount ? "Paid" : "Partially Paid"; + + const eventStatus = { + reject: "Rejected", + cancel: "Canceled", + }; + + for (const [event, status] of Object.entries(eventStatus)) { + if ( + request?.events?.some( + (e: { name?: string }) => e?.name?.toLowerCase() === event.toLowerCase() + ) + ) { + return capitalize(status); + } + } + + if (dueDate < today) { + if (balance === BigInt(0)) { + return "Overdue"; + } + return isPaid; + } else { + if (balance === BigInt(0)) { + return "Awaiting Payment"; + } + return isPaid; + } +}; From d3a2a49890940ae322ab73c5b1f803c9ff23d727 Mon Sep 17 00:00:00 2001 From: sstefdev Date: Thu, 9 Jan 2025 14:18:45 +0100 Subject: [PATCH 02/18] feat: updated and cleaned single invoice component, removed unused variables, updated package.json, updated configs --- package-lock.json | 35 + packages/single-invoice/README.md | 53 - packages/single-invoice/package.json | 30 +- .../src/lib/react/SingleInvoice.d.ts | 21 +- .../src/lib/single-invoice.svelte | 1193 +++++++++++------ .../single-invoice/src/lib/types/index.ts | 61 - .../src/{lib => }/utils/capitalize.d.ts | 0 .../src/{lib => }/utils/capitalize.ts | 0 .../src/{lib => }/utils/chainlink.d.ts | 0 .../src/{lib => }/utils/chainlink.ts | 0 .../src/{lib => }/utils/conversion.d.ts | 0 .../src/{lib => }/utils/conversion.ts | 0 .../src/{lib => }/utils/debounce.d.ts | 0 .../src/{lib => }/utils/debounce.ts | 0 .../utils/ethersAdapterProvider.d.ts | 0 .../src/{lib => }/utils/formatAddress.d.ts | 0 .../src/{lib => }/utils/formatAddress.ts | 0 .../src/{lib => }/utils/generateInvoice.d.ts | 0 .../utils/getConversionPaymentValues.d.ts | 0 .../utils/getConversionPaymentValues.ts | 0 .../src/{lib => }/utils/index.d.ts | 0 .../src/{lib => }/utils/index.ts | 0 .../src/{lib => }/utils/loadScript.d.ts | 0 .../src/{lib => }/utils/wallet-utils.d.ts | 0 .../src/{lib => }/utils/wallet-utils.ts | 0 packages/single-invoice/svelte.config.js | 8 + packages/single-invoice/vite.config.ts | 1 + ...s.timestamp-1736352084979-555798874d03.mjs | 10 + packages/single-invoice/vite.wc.config.ts | 5 - 29 files changed, 906 insertions(+), 511 deletions(-) delete mode 100644 packages/single-invoice/README.md delete mode 100644 packages/single-invoice/src/lib/types/index.ts rename packages/single-invoice/src/{lib => }/utils/capitalize.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/capitalize.ts (100%) rename packages/single-invoice/src/{lib => }/utils/chainlink.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/chainlink.ts (100%) rename packages/single-invoice/src/{lib => }/utils/conversion.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/conversion.ts (100%) rename packages/single-invoice/src/{lib => }/utils/debounce.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/debounce.ts (100%) rename packages/single-invoice/src/{lib => }/utils/ethersAdapterProvider.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/formatAddress.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/formatAddress.ts (100%) rename packages/single-invoice/src/{lib => }/utils/generateInvoice.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/getConversionPaymentValues.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/getConversionPaymentValues.ts (100%) rename packages/single-invoice/src/{lib => }/utils/index.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/index.ts (100%) rename packages/single-invoice/src/{lib => }/utils/loadScript.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/wallet-utils.d.ts (100%) rename packages/single-invoice/src/{lib => }/utils/wallet-utils.ts (100%) create mode 100644 packages/single-invoice/vite.config.ts.timestamp-1736352084979-555798874d03.mjs diff --git a/package-lock.json b/package-lock.json index e32a86da..52edcd3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2964,6 +2964,10 @@ "resolved": "shared/utils", "link": true }, + "node_modules/@requestnetwork/single-invoice": { + "resolved": "packages/single-invoice", + "link": true + }, "node_modules/@requestnetwork/smart-contracts": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/@requestnetwork/smart-contracts/-/smart-contracts-0.43.0.tgz", @@ -11436,6 +11440,37 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "packages/single-invoice": { + "name": "@requestnetwork/single-invoice", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@requestnetwork/payment-detection": "0.49.0", + "@requestnetwork/payment-processor": "0.52.0", + "@requestnetwork/request-client.js": "0.54.0", + "@wagmi/core": "^2.15.2", + "ethers": "^5.7.2", + "viem": "^2.21.53", + "wagmi": "^2.13.3" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.27.4", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^2.5.2", + "publint": "^0.1.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "svelte": "^4.0.5", + "svelte-check": "^3.6.0", + "typescript": "^5.0.0", + "vite": "^4.4.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "shared/components": { "name": "@requestnetwork/shared-components", "license": "MIT", diff --git a/packages/single-invoice/README.md b/packages/single-invoice/README.md deleted file mode 100644 index f919758d..00000000 --- a/packages/single-invoice/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Request Network Payment Widget Web Component - -A web component for accepting crypto payments using Request Network. - -## Introduction - -The Payment Widget web component is a pre-built component that allows users to offer crypto payment options using Request Network without having to implement it themselves. It is built using Svelte but compiled to a Web Component, making it usuable in any web environment, regardless of the framework. - -## Installation - -To install the component, use npm: - -```bash -npm install @requestnetwork/payment-widget -``` - -## Usage - -### Usage in React - -```tsx -import PaymentWidget from "@requestnetwork/payment-widget/react"; - -export default function PaymentPage() { - return ( - { - console.log(request); - }} - onError={(error) => { - console.error(error); - }} - /> - ); -} -``` - -## Additional Information - -For more information, see the [Request Network documentation](https://docs.request.network/). diff --git a/packages/single-invoice/package.json b/packages/single-invoice/package.json index 46cb1a41..37efc677 100644 --- a/packages/single-invoice/package.json +++ b/packages/single-invoice/package.json @@ -32,35 +32,35 @@ "!dist/**/*.test.*", "!dist/**/*.spec.*" ], + "dependencies": { + "@requestnetwork/payment-detection": "0.49.0", + "@requestnetwork/payment-processor": "0.52.0", + "@requestnetwork/request-client.js": "0.54.0", + "@wagmi/core": "^2.15.2", + "ethers": "^5.7.2", + "viem": "^2.21.53", + "wagmi": "^2.13.3" + }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.27.4", + "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^2.5.2", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "sass": "^1.77.8", + "publint": "^0.1.9", "svelte": "^4.0.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", "svelte-check": "^3.6.0", "typescript": "^5.0.0", "vite": "^4.4.2" }, - "svelte": "./dist/index.js", - "types": "./dist/index.d.ts", "type": "module", "license": "MIT", "publishConfig": { "access": "public" - }, - "dependencies": { - "@requestnetwork/payment-processor": "0.48.0", - "@requestnetwork/request-client.js": "0.50.0", - "@requestnetwork/web3-signature": "0.8.1", - "@web3modal/ethers5": "^5.0.11", - "ethers": "^5.7.2", - "vite-plugin-node-polyfills": "^0.22.0" } } diff --git a/packages/single-invoice/src/lib/react/SingleInvoice.d.ts b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts index 988e782b..c56a2e01 100644 --- a/packages/single-invoice/src/lib/react/SingleInvoice.d.ts +++ b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts @@ -1,22 +1,37 @@ import React from "react"; +import { Config as WagmiConfig } from "@wagmi/core"; +import type { IConfig } from "@requestnetwork/shared-types"; +import type { RequestNetwork } from "@requestnetwork/request-client.js"; export interface SingleInvoiceProps { + /** The unique identifier of the request/invoice to display */ requestId: string; + /** Configuration object for the Request Network integration */ config: IConfig; + /** Wagmi configuration for Web3 wallet interactions */ wagmiConfig: WagmiConfig; + /** Instance of the Request Network client */ requestNetwork: RequestNetwork | null | undefined; - currencies: CurrencyTypes.CurrencyInput[]; + /** Currency manager instance for handling different currencies */ + currencies?: CurrencyTypes.CurrencyInput[]; } /** - * SingleInvoice is a React component that displays a single invoice. + * SingleInvoice is a React component that displays and manages a single invoice from the Request Network. + * + * This component wraps the web component 'single-invoice' and handles the proper passing and updating + * of props to ensure the invoice display stays in sync with the provided data. * * @param {SingleInvoiceProps} props - The component props * @returns {JSX.Element} * * @example * */ declare const SingleInvoice: React.FC; diff --git a/packages/single-invoice/src/lib/single-invoice.svelte b/packages/single-invoice/src/lib/single-invoice.svelte index 2fdd26bd..dfa24f8b 100644 --- a/packages/single-invoice/src/lib/single-invoice.svelte +++ b/packages/single-invoice/src/lib/single-invoice.svelte @@ -1,7 +1,9 @@ + + -
-
-

Issued on: {formatDate(request?.contentData?.creationDate || "-")}

-

- Due by: {formatDate(request?.contentData?.paymentTerms?.dueDate || "-")} -

-
-

- Invoice #{request?.contentData?.invoiceNumber || "-"} - - - { - try { - await exportToPDF( - request, - currency, - paymentCurrencies, - config.logo - ); - } catch (error) { - toast.error(`Failed to export PDF`, { - description: `${error}`, - action: { - label: "X", - onClick: () => console.info("Close"), - }, - }); - console.error("Failed to export PDF:", error); - } - }} - /> - -

-
-

From:

-

{request?.payee?.value || "-"}

-
- {#if sellerInfo.length > 0} -
- {#each sellerInfo as { value, isCompany, isEmail }} +{#if loading} +
Loading...
+{:else if request} +
+
+

- {#if isEmail} - - {:else if isCompany} - {value} - {:else} - {value} - {/if} + Issued on: {formatDate(request?.contentData?.creationDate || "-")}

- {/each} -
- {/if} -
-
-

Billed to:

-

{request?.payer?.value || "-"}

-
- {#if buyerInfo.length > 0} -
- {#each buyerInfo as { value, isCompany, isEmail }}

- {#if isEmail} - - {:else if isCompany} - {value} - {:else} - {value} - {/if} + Due by: {formatDate( + request?.contentData?.paymentTerms?.dueDate || "-" + )}

- {/each} -
- {/if} - -

- Payment Chain: - {paymentCurrencies && paymentCurrencies.length > 0 - ? paymentCurrencies[0]?.network || "-" - : ""} -

-

- Invoice Currency: - {currency?.symbol || "-"} -

- -

- Settlement Currency: - {paymentCurrencies && paymentCurrencies.length > 0 - ? paymentCurrencies[0]?.symbol || "-" - : ""} -

- - {#if request?.contentData?.invoiceItems} -
- - - - - - - - - - - - - {#each firstItems as item, index (index)} - - - - - - - - + +

+ Invoice #{request?.contentData?.invoiceNumber || "-"} + + + { + try { + await exportToPDF( + request, + currency, + paymentCurrencies, + config.logo + ); + } catch (error) { + toast.error(`Failed to export PDF`, { + description: `${error}`, + action: { + label: "X", + onClick: () => console.info("Close"), + }, + }); + console.error("Failed to export PDF:", error); + } + }} + /> + +

+
+

From:

+

{request?.payee?.value || "-"}

+
+ {#if sellerInfo.length > 0} +
+ {#each sellerInfo as { value, isCompany, isEmail }} +

+ {#if isEmail} + + {:else if isCompany} + {value} + {:else} + {value} + {/if} +

{/each} -
-
DescriptionQtyUnit PriceDiscountTaxAmount
-

{item.name || "-"}

-
{item.quantity || "-"}{item.unitPrice - ? formatUnits(item.unitPrice, currency?.decimals ?? 18) - : "-"}{item.discount - ? formatUnits(item.discount, currency?.decimals ?? 18) - : "-"}{Number(item.tax.amount || "-")}{truncateNumberString( - formatUnits( - // @ts-expect-error - calculateItemTotal(item), - currency?.decimals ?? 18 - ), - 2 - )}
-
- {#if otherItems.length > 0} - +
+ {/if} +
+
+

Billed to:

+

{request?.payer?.value || "-"}

+
+ {#if buyerInfo.length > 0} +
+ {#each buyerInfo as { value, isCompany, isEmail }} +

+ {#if isEmail} + + {:else if isCompany} + {value} + {:else} + {value} + {/if} +

+ {/each} +
+ {/if} + +

+ Payment Chain: + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.network || "Unknown" + : ""} +

+

+ Invoice Currency: + {currency?.symbol || "Unknown"} +

+ +

+ Settlement Currency: + {paymentCurrencies && paymentCurrencies.length > 0 + ? paymentCurrencies[0]?.symbol || "Unknown" + : ""} +

+ + {#if request?.contentData?.invoiceItems}
- + - + @@ -562,109 +760,250 @@ - {#each otherItems as item, index (index)} + {#each firstItems as item, index (index)} - - + + : "-"} + - + - {/each} + {/each} +
DescriptionDescription Qty Unit Price Discount
-

- {item.name || "-"} -

+

{item.name || "-"}

{item.quantity || "-"}{item.unitPrice - ? formatUnits(item.unitPrice, currency?.decimals ?? 18) - : "-"}{item.discount + + {#if unknownCurrency} + Unknown + {:else} + {item.unitPrice + ? formatUnits(item.unitPrice, currency?.decimals ?? 18) + : "-"} + {/if} + + {item.discount ? formatUnits(item.discount, currency?.decimals ?? 18) - : "-"} {Number(item.tax.amount || "-")}{truncateNumberString( - formatUnits( - // @ts-expect-error - calculateItemTotal(item), - currency?.decimals ?? 18 - ), - 2 - )} + {#if unknownCurrency} + Unknown + {:else} + {truncateNumberString( + formatUnits( + calculateItemTotal(item), + currency?.decimals ?? 18 + ), + 2 + )} + {/if} +
- - {/if} - {/if} - {#if request?.contentData.note} -
-

- Memo:
- {request.contentData.note || "-"} -

-
- {/if} -
- {#if request?.contentData?.miscellaneous?.labels} - {#each request?.contentData?.miscellaneous?.labels as label, index (index)} -
- {label || "-"} + {#if otherItems.length > 0} + +
+ + + + + + + + + + + + + {#each otherItems as item, index (index)} + + + + + + + + + {/each} +
+

+ {item.name || "-"} +

+
{item.quantity || "-"} + {#if unknownCurrency} + Unknown + {:else} + {item.unitPrice + ? formatUnits( + item.unitPrice, + currency?.decimals ?? 18 + ) + : "-"} + {/if} + + {item.discount + ? formatUnits(item.discount, currency?.decimals ?? 18) + : "-"} + {Number(item.tax.amount || "-")} + {#if unknownCurrency} + Unknown + {:else} + {truncateNumberString( + formatUnits( + calculateItemTotal(item), + currency?.decimals ?? 18 + ), + 2 + )} + {/if} +
+
+
+ {/if} + {/if} + {#if request?.contentData?.note} +
+

+ Memo:
+ {request.contentData?.note || "-"} +

- {/each} - {/if} -
-
-
- {#if statuses.length > 0 && loading} - {#each statuses as status, index (index)} -
- {status || "-"} - {#if (index === 0 && statuses.length === 2) || (index === 1 && statuses.length === 3)} - - - + {/if} +
+ {#if request?.contentData?.miscellaneous?.labels} + {#each request?.contentData?.miscellaneous?.labels as label, index (index)} +
+ {label || "-"} +
+ {/each} + {/if} +
+ {#if shouldShowStatuses} +
+
+ {#if statuses?.length > 0} +
    + {#each statuses as status, index} +
  • + + {#if status.done} + + + + {:else} + + + + {/if} + + {status.message} +
    +
  • + {/each} +
{/if}
- {/each} +
{/if} -
- -
- {#if loading} -
Loading...
- {:else if !correctChain && !isPayee} -
+ + {#if unsupportedNetwork} +
Unsupported payment network!
{/if}
- {#if unsupportedNetwork} -
Unsupported payment network!
- {/if} -
+{:else} +
No invoice found
+{/if} diff --git a/packages/single-invoice/src/lib/types/index.ts b/packages/single-invoice/src/lib/types/index.ts deleted file mode 100644 index 31e6289e..00000000 --- a/packages/single-invoice/src/lib/types/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { CURRENCY_ID, NETWORK_LABEL } from "../utils/currencies"; - -export interface Address { - "street-address"?: string; - locality?: string; - region?: string; - "country-name"?: string; - "postal-code"?: string; -} - -export interface SellerInfo { - logo?: string; - name?: string; - email?: string; - firstName?: string; - lastName?: string; - businessName?: string; - phone?: string; - address?: Address; - taxRegistration?: string; - companyRegistration?: string; -} - -export interface BuyerInfo { - email?: string; - firstName?: string; - lastName?: string; - businessName?: string; - phone?: string; - address?: Address; - taxRegistration?: string; - companyRegistration?: string; -} - -export type ProductInfo = { - name?: string; - description?: string; - image?: string; -}; - -export type AmountInUSD = number; - -export type CurrencyID = (typeof CURRENCY_ID)[keyof typeof CURRENCY_ID]; -export type SupportedCurrencies = [CurrencyID, ...CurrencyID[]]; - -export type Currency = { - id: string; - hash: string; - address?: string; - network: keyof typeof NETWORK_LABEL; - decimals: number; - symbol: string; - type: "ERC20" | "ETH"; - name?: string; -}; - -export type PaymentStep = - | "currency" - | "buyer-info" - | "confirmation" - | "complete"; diff --git a/packages/single-invoice/src/lib/utils/capitalize.d.ts b/packages/single-invoice/src/utils/capitalize.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/capitalize.d.ts rename to packages/single-invoice/src/utils/capitalize.d.ts diff --git a/packages/single-invoice/src/lib/utils/capitalize.ts b/packages/single-invoice/src/utils/capitalize.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/capitalize.ts rename to packages/single-invoice/src/utils/capitalize.ts diff --git a/packages/single-invoice/src/lib/utils/chainlink.d.ts b/packages/single-invoice/src/utils/chainlink.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/chainlink.d.ts rename to packages/single-invoice/src/utils/chainlink.d.ts diff --git a/packages/single-invoice/src/lib/utils/chainlink.ts b/packages/single-invoice/src/utils/chainlink.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/chainlink.ts rename to packages/single-invoice/src/utils/chainlink.ts diff --git a/packages/single-invoice/src/lib/utils/conversion.d.ts b/packages/single-invoice/src/utils/conversion.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/conversion.d.ts rename to packages/single-invoice/src/utils/conversion.d.ts diff --git a/packages/single-invoice/src/lib/utils/conversion.ts b/packages/single-invoice/src/utils/conversion.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/conversion.ts rename to packages/single-invoice/src/utils/conversion.ts diff --git a/packages/single-invoice/src/lib/utils/debounce.d.ts b/packages/single-invoice/src/utils/debounce.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/debounce.d.ts rename to packages/single-invoice/src/utils/debounce.d.ts diff --git a/packages/single-invoice/src/lib/utils/debounce.ts b/packages/single-invoice/src/utils/debounce.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/debounce.ts rename to packages/single-invoice/src/utils/debounce.ts diff --git a/packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts b/packages/single-invoice/src/utils/ethersAdapterProvider.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/ethersAdapterProvider.d.ts rename to packages/single-invoice/src/utils/ethersAdapterProvider.d.ts diff --git a/packages/single-invoice/src/lib/utils/formatAddress.d.ts b/packages/single-invoice/src/utils/formatAddress.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/formatAddress.d.ts rename to packages/single-invoice/src/utils/formatAddress.d.ts diff --git a/packages/single-invoice/src/lib/utils/formatAddress.ts b/packages/single-invoice/src/utils/formatAddress.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/formatAddress.ts rename to packages/single-invoice/src/utils/formatAddress.ts diff --git a/packages/single-invoice/src/lib/utils/generateInvoice.d.ts b/packages/single-invoice/src/utils/generateInvoice.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/generateInvoice.d.ts rename to packages/single-invoice/src/utils/generateInvoice.d.ts diff --git a/packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts b/packages/single-invoice/src/utils/getConversionPaymentValues.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/getConversionPaymentValues.d.ts rename to packages/single-invoice/src/utils/getConversionPaymentValues.d.ts diff --git a/packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts b/packages/single-invoice/src/utils/getConversionPaymentValues.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/getConversionPaymentValues.ts rename to packages/single-invoice/src/utils/getConversionPaymentValues.ts diff --git a/packages/single-invoice/src/lib/utils/index.d.ts b/packages/single-invoice/src/utils/index.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/index.d.ts rename to packages/single-invoice/src/utils/index.d.ts diff --git a/packages/single-invoice/src/lib/utils/index.ts b/packages/single-invoice/src/utils/index.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/index.ts rename to packages/single-invoice/src/utils/index.ts diff --git a/packages/single-invoice/src/lib/utils/loadScript.d.ts b/packages/single-invoice/src/utils/loadScript.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/loadScript.d.ts rename to packages/single-invoice/src/utils/loadScript.d.ts diff --git a/packages/single-invoice/src/lib/utils/wallet-utils.d.ts b/packages/single-invoice/src/utils/wallet-utils.d.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/wallet-utils.d.ts rename to packages/single-invoice/src/utils/wallet-utils.d.ts diff --git a/packages/single-invoice/src/lib/utils/wallet-utils.ts b/packages/single-invoice/src/utils/wallet-utils.ts similarity index 100% rename from packages/single-invoice/src/lib/utils/wallet-utils.ts rename to packages/single-invoice/src/utils/wallet-utils.ts diff --git a/packages/single-invoice/svelte.config.js b/packages/single-invoice/svelte.config.js index 919755ca..ff60aa7c 100644 --- a/packages/single-invoice/svelte.config.js +++ b/packages/single-invoice/svelte.config.js @@ -1,8 +1,16 @@ +import adapter from "@sveltejs/adapter-auto"; import { vitePreprocess } from "@sveltejs/kit/vite"; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + alias: { + $src: "./src", + $utils: "./src/utils", + }, + }, compilerOptions: { customElement: true, }, diff --git a/packages/single-invoice/vite.config.ts b/packages/single-invoice/vite.config.ts index b1edb182..80864b9d 100644 --- a/packages/single-invoice/vite.config.ts +++ b/packages/single-invoice/vite.config.ts @@ -1,5 +1,6 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; + export default defineConfig({ plugins: [sveltekit()], }); diff --git a/packages/single-invoice/vite.config.ts.timestamp-1736352084979-555798874d03.mjs b/packages/single-invoice/vite.config.ts.timestamp-1736352084979-555798874d03.mjs new file mode 100644 index 00000000..6142a8e8 --- /dev/null +++ b/packages/single-invoice/vite.config.ts.timestamp-1736352084979-555798874d03.mjs @@ -0,0 +1,10 @@ +// vite.config.ts +import { sveltekit } from "file:///Users/stefdev/Desktop/RN/web-components/node_modules/@sveltejs/kit/src/exports/vite/index.js"; +import { defineConfig } from "file:///Users/stefdev/Desktop/RN/web-components/node_modules/vite/dist/node/index.js"; +var vite_config_default = defineConfig({ + plugins: [sveltekit()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc3RlZmRldi9EZXNrdG9wL1JOL3dlYi1jb21wb25lbnRzL3BhY2thZ2VzL3NpbmdsZS1pbnZvaWNlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvc3RlZmRldi9EZXNrdG9wL1JOL3dlYi1jb21wb25lbnRzL3BhY2thZ2VzL3NpbmdsZS1pbnZvaWNlL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9zdGVmZGV2L0Rlc2t0b3AvUk4vd2ViLWNvbXBvbmVudHMvcGFja2FnZXMvc2luZ2xlLWludm9pY2Uvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBzdmVsdGVraXQgfSBmcm9tIFwiQHN2ZWx0ZWpzL2tpdC92aXRlXCI7XG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbc3ZlbHRla2l0KCldLFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWtYLFNBQVMsaUJBQWlCO0FBQzVZLFNBQVMsb0JBQW9CO0FBRTdCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxVQUFVLENBQUM7QUFDdkIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/packages/single-invoice/vite.wc.config.ts b/packages/single-invoice/vite.wc.config.ts index 2526415a..41f3e837 100644 --- a/packages/single-invoice/vite.wc.config.ts +++ b/packages/single-invoice/vite.wc.config.ts @@ -1,16 +1,11 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; -import { nodePolyfills } from "vite-plugin-node-polyfills"; export default defineConfig({ define: { global: "globalThis", }, plugins: [ - nodePolyfills({ - include: ["buffer", "crypto"], - exclude: ["http", "stream", "zlib", "assert"], - }), svelte({ compilerOptions: { customElement: true, From 5d71ab16d37d974c963c76642edf718d5ea3e6f0 Mon Sep 17 00:00:00 2001 From: sstefdev Date: Mon, 13 Jan 2025 17:19:52 +0100 Subject: [PATCH 03/18] fix: updated gitignore, added loading skeleton, resolved comments --- .gitignore | 1 + .../src/lib/loading-skeleton.svelte | 255 +++++++++++++++ .../src/lib/single-invoice.svelte | 308 +++++++++++++----- ...s.timestamp-1736352084979-555798874d03.mjs | 10 - 4 files changed, 483 insertions(+), 91 deletions(-) create mode 100644 packages/single-invoice/src/lib/loading-skeleton.svelte delete mode 100644 packages/single-invoice/vite.config.ts.timestamp-1736352084979-555798874d03.mjs diff --git a/.gitignore b/.gitignore index ed6e2172..8748ae94 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules .turbo dist .svelte-kit +vite.congig.ts.timestamp* \ No newline at end of file diff --git a/packages/single-invoice/src/lib/loading-skeleton.svelte b/packages/single-invoice/src/lib/loading-skeleton.svelte new file mode 100644 index 00000000..fd9e344f --- /dev/null +++ b/packages/single-invoice/src/lib/loading-skeleton.svelte @@ -0,0 +1,255 @@ + + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + +
+ +
+ +
+
+ +
+ + +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
DESCRIPTIONQTYUNIT PRICEDISCOUNTTAXAMOUNT
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ + diff --git a/packages/single-invoice/src/lib/single-invoice.svelte b/packages/single-invoice/src/lib/single-invoice.svelte index dfa24f8b..cf152716 100644 --- a/packages/single-invoice/src/lib/single-invoice.svelte +++ b/packages/single-invoice/src/lib/single-invoice.svelte @@ -1,9 +1,13 @@ {#if loading} -
Loading...
+ +{:else if !requestId} +
Error: Missing required Request ID
{:else if request}
Date: Wed, 15 Jan 2025 20:09:42 +0100 Subject: [PATCH 04/18] fix: decoupling utils, using new token list --- .../src/lib/react/SingleInvoice.d.ts | 3 - .../src/lib/single-invoice.svelte | 80 ++++++--- .../src/utils/wallet-utils.d.ts | 8 - shared/utils/chainlink.ts | 40 +++++ shared/utils/conversion.ts | 50 ++++++ shared/utils/debounce.ts | 11 ++ shared/utils/formatAddress.ts | 11 +- shared/utils/getConversionPaymentValues.ts | 170 ++++++++++++++++++ .../src => shared}/utils/wallet-utils.ts | 0 9 files changed, 330 insertions(+), 43 deletions(-) delete mode 100644 packages/single-invoice/src/utils/wallet-utils.d.ts create mode 100644 shared/utils/chainlink.ts create mode 100644 shared/utils/conversion.ts create mode 100644 shared/utils/debounce.ts create mode 100644 shared/utils/getConversionPaymentValues.ts rename {packages/single-invoice/src => shared}/utils/wallet-utils.ts (100%) diff --git a/packages/single-invoice/src/lib/react/SingleInvoice.d.ts b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts index c56a2e01..959f3452 100644 --- a/packages/single-invoice/src/lib/react/SingleInvoice.d.ts +++ b/packages/single-invoice/src/lib/react/SingleInvoice.d.ts @@ -12,8 +12,6 @@ export interface SingleInvoiceProps { wagmiConfig: WagmiConfig; /** Instance of the Request Network client */ requestNetwork: RequestNetwork | null | undefined; - /** Currency manager instance for handling different currencies */ - currencies?: CurrencyTypes.CurrencyInput[]; } /** @@ -31,7 +29,6 @@ export interface SingleInvoiceProps { * config={config} * wagmiConfig={wagmiConfig} * requestNetwork={requestNetwork} - * currencies={currencies} * /> */ declare const SingleInvoice: React.FC; diff --git a/packages/single-invoice/src/lib/single-invoice.svelte b/packages/single-invoice/src/lib/single-invoice.svelte index cf152716..dd515eaa 100644 --- a/packages/single-invoice/src/lib/single-invoice.svelte +++ b/packages/single-invoice/src/lib/single-invoice.svelte @@ -38,8 +38,8 @@ import { getCurrencyFromManager } from "@requestnetwork/shared-utils/getCurrency"; import { onMount, onDestroy, tick } from "svelte"; import { formatUnits } from "viem"; - import { getConversionPaymentValues } from "../utils/getConversionPaymentValues"; - import { getEthersSigner } from "../utils"; + import { getConversionPaymentValues } from "@requestnetwork/shared-utils/getConversionPaymentValues"; + import { getEthersSigner } from "@requestnetwork/shared-utils/wallet-utils"; import SingleInvoiceSkeleton from "./loading-skeleton.svelte"; interface EntityInfo { @@ -55,7 +55,6 @@ export let wagmiConfig: WagmiConfig; export let requestNetwork: RequestNetwork | null | undefined; export let requestId: string; - export let currencies: CurrencyTypes.CurrencyInput[] = []; let account = getAccount(wagmiConfig); let address = account?.address; @@ -166,7 +165,13 @@ buyerInfo = generateDetailParagraphs(request.contentData?.buyerInfo); } - $: if (request && account && network && !unknownCurrency) { + $: if ( + request && + account && + network && + !unknownCurrency && + paymentCurrencies?.[0] + ) { checkBalance(); } @@ -177,6 +182,23 @@ unknownCurrency = currency ? currency.decimals === undefined : false; } + onMount(async () => { + currencyManager = await initializeCurrencyManager(); + }); + + onMount(() => { + unwatchAccount = watchAccount(wagmiConfig, { + onChange( + account: GetAccountReturnType, + previousAccount: GetAccountReturnType + ) { + tick().then(() => { + handleWalletChange(account, previousAccount); + }); + }, + }); + }); + const getOneRequest = async (requestId: string) => { try { loading = true; @@ -326,23 +348,6 @@ } }; - onMount(() => { - // Initialize currency manager - currencyManager = initializeCurrencyManager(currencies); - - // Set up account watching - unwatchAccount = watchAccount(wagmiConfig, { - onChange( - account: GetAccountReturnType, - previousAccount: GetAccountReturnType - ) { - tick().then(() => { - handleWalletChange(account, previousAccount); - }); - }, - }); - }); - onDestroy(() => { if (typeof unwatchAccount === "function") { unwatchAccount(); @@ -374,6 +379,15 @@ throw new Error("Request network or request data not available"); } + // Ensure we're on the correct network first + await switchNetworkIfNeeded(network || "mainnet"); + + // Get fresh signer after network switch + signer = await getEthersSigner(wagmiConfig); + if (!signer || !signer.provider) { + throw new Error("No signer or provider available"); + } + const _request = await requestNetwork?.fromRequestId( requestData.requestId ); @@ -383,7 +397,6 @@ let paymentSettings = undefined; - // Handle conversion payments if ( paymentNetworkExtension?.id === Types.Extension.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY || @@ -395,20 +408,31 @@ throw new Error("Missing currency information for conversion"); } + // Verify ERC20 approval first if needed + if ( + paymentCurrencies[0]?.type === Types.RequestLogic.CURRENCY.ERC20 && + !approved + ) { + await approve(); + } + const { conversion } = await getConversionPaymentValues({ baseAmount: requestData.expectedAmount, denominationCurrency: currency, selectedPaymentCurrency: paymentCurrencies[0], currencyManager, - provider: signer?.provider, + provider: signer.provider, fromAddress: address, }); paymentSettings = conversion; } catch (conversionError) { - console.error("Conversion calculation failed:", conversionError); - toast.error("Failed to calculate conversion rate", { - description: "Please try again or use a different payment method", + console.error("Conversion calculation failed:", { + error: conversionError, + currency, + paymentCurrency: paymentCurrencies[0], + network, + chainId: await signer.getChainId(), }); throw conversionError; } @@ -444,7 +468,6 @@ statuses = [...statuses]; } - // Wait for payment confirmation while (requestData.balance?.balance! < requestData.expectedAmount) { requestData = await _request?.refresh(); await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -666,6 +689,7 @@ address, paymentCurrency: paymentCurrencies[0], network, + paymentCurrencies, }); return; } @@ -701,8 +725,6 @@ } } - const currentStatusIndex = statuses.length - 1; - const handlePayment = async () => { try { if (!correctChain) { diff --git a/packages/single-invoice/src/utils/wallet-utils.d.ts b/packages/single-invoice/src/utils/wallet-utils.d.ts deleted file mode 100644 index 961786b6..00000000 --- a/packages/single-invoice/src/utils/wallet-utils.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Config } from "@wagmi/core"; -import { providers } from "ethers"; -import type { Account, Chain, Client, Transport } from "viem"; -export declare const publicClientToProvider: (publicClient: any) => providers.JsonRpcProvider; -export declare function clientToSigner(client: Client): providers.JsonRpcSigner; -export declare function getEthersSigner(config: Config, { chainId }?: { - chainId?: number; -}): Promise; diff --git a/shared/utils/chainlink.ts b/shared/utils/chainlink.ts new file mode 100644 index 00000000..a7c72b1f --- /dev/null +++ b/shared/utils/chainlink.ts @@ -0,0 +1,40 @@ +import { isISO4217Currency } from "@requestnetwork/currency"; +import { chainlinkConversionPath } from "@requestnetwork/smart-contracts"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { BigNumber, providers } from "ethers"; + +export const getChainlinkRate = async ( + from: CurrencyTypes.CurrencyDefinition, + to: CurrencyTypes.CurrencyDefinition, + { + network, + provider, + currencyManager, + }: { + network: CurrencyTypes.EvmChainName; + provider: providers.Provider; + currencyManager: CurrencyTypes.ICurrencyManager; + }, +) => { + try { + const chainlink = chainlinkConversionPath.connect(network, provider); + const path = currencyManager.getConversionPath(from, to, network); + if (!path) return null; + const result = await chainlink.getRate(path); + if (!result) return null; + + // ChainlinkConversionPath uses 8 decimals for fiat. + const fromDecimals = isISO4217Currency(from) ? 8 : from.decimals; + const toDecimals = isISO4217Currency(to) ? 8 : to.decimals; + const value = result.rate + .mul(BigNumber.from(10).pow(fromDecimals)) + .div(BigNumber.from(10).pow(toDecimals)); + return { + value, + decimals: result.decimals.toString().length - 1, + }; + } catch (e) { + console.error('Error fetching Chainlink rate:', e); + return null; + } +}; diff --git a/shared/utils/conversion.ts b/shared/utils/conversion.ts new file mode 100644 index 00000000..e42a6da3 --- /dev/null +++ b/shared/utils/conversion.ts @@ -0,0 +1,50 @@ +import { isISO4217Currency } from "@requestnetwork/currency"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { EvmChainName } from "@requestnetwork/types/dist/currency-types"; +import { providers, utils } from "ethers"; +import { getChainlinkRate } from './chainlink' + +/** + * Maximum slippage (swap) or conversion evolution (conversion pn) + */ +const MAX_SLIPPAGE_DEFAULT = 1.03; +const MAX_SLIPPAGE_LOW_VOLATILITY = 1.01; +export const lowVolatilityTokens = ["DAI", "USDC", "USDT"]; + +export const getSlippageMargin = (currency: CurrencyTypes.CurrencyInput) => { + return lowVolatilityTokens.includes(currency.symbol) + ? MAX_SLIPPAGE_LOW_VOLATILITY + : MAX_SLIPPAGE_DEFAULT; +}; + +/** + * Get the conversion rate between two currencies. + */ +export const getConversionRate = async ({ + from, + to, + currencyManager, + provider, +}: { + from: CurrencyTypes.CurrencyDefinition; + to: CurrencyTypes.CurrencyDefinition; + currencyManager?: CurrencyTypes.ICurrencyManager; + provider?: providers.Provider; +}): Promise => { + if (!isISO4217Currency(to) && currencyManager && provider) { + const network = to.network as EvmChainName; + try { + const chainlinkRate = await getChainlinkRate(from, to, { + network, + currencyManager, + provider, + }); + if (chainlinkRate) { + return Number(utils.formatUnits(chainlinkRate.value, chainlinkRate.decimals)) + } + } catch (e) { + console.error("Error getting chainlink rate", e); + throw e; + } + } +}; diff --git a/shared/utils/debounce.ts b/shared/utils/debounce.ts new file mode 100644 index 00000000..1462f81b --- /dev/null +++ b/shared/utils/debounce.ts @@ -0,0 +1,11 @@ +export const debounce = (func: Function, wait: number) => { + let timeout: NodeJS.Timeout; + return (...args: any[]) => { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; diff --git a/shared/utils/formatAddress.ts b/shared/utils/formatAddress.ts index 91fab1ee..cf55278b 100644 --- a/shared/utils/formatAddress.ts +++ b/shared/utils/formatAddress.ts @@ -1,11 +1,16 @@ -import { checkAddress } from "./checkEthAddress"; +import { getAddress } from "viem"; +import { checkAddress } from "@requestnetwork/shared-utils/checkEthAddress"; export const formatAddress = ( address: string, first: number = 6, last: number = 4 ): string => { - if (!address) return ""; + if (!checkAddress(address)) { + console.error("Invalid address!"); + } - return `${address.slice(0, first)}...${address.slice(-last)}`; + const checksumAddress = getAddress(address); + + return `${checksumAddress.slice(0, first)}...${checksumAddress.slice(-last)}`; }; diff --git a/shared/utils/getConversionPaymentValues.ts b/shared/utils/getConversionPaymentValues.ts new file mode 100644 index 00000000..fdd05a13 --- /dev/null +++ b/shared/utils/getConversionPaymentValues.ts @@ -0,0 +1,170 @@ +import { CurrencyManager } from "@requestnetwork/currency"; +import { CurrencyTypes } from "@requestnetwork/types"; +import { getAnyErc20Balance } from "@requestnetwork/payment-processor"; +import { BigNumber, BigNumberish, providers, utils } from "ethers"; + +import { getConversionRate, getSlippageMargin } from "./conversion"; + +export type SlippageLevel = "safe" | "risky"; + +interface ConversionPaymentValues { + conversion: { + currency: CurrencyTypes.ICurrency; + maxToSpend: string; + currencyManager: CurrencyManager; + }; + slippageLevel: SlippageLevel; + totalAmountInPaymentCurrency: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + safeBalance: { + value: number; + currency: CurrencyTypes.CurrencyDefinition; + }; + rate: string; +} + +export const formatUnits = ( + amount: BigNumber, + token: CurrencyTypes.CurrencyDefinition +): string => { + return utils.formatUnits(amount, token.decimals); +}; + +export const toFixedDecimal = (numberToFormat: number, decimals?: number) => { + const MAX_DECIMALS = decimals !== undefined ? decimals : 5; + return Number(numberToFormat.toFixed(MAX_DECIMALS)); +}; + +export const amountToFixedDecimal = ( + amount: BigNumberish, + currency: CurrencyTypes.CurrencyDefinition, + decimals?: number +) => { + return toFixedDecimal( + Number.parseFloat(formatUnits(BigNumber.from(amount), currency)), + decimals + ); +}; + +/** + * Converts a number, even floating, to a BigNumber + * @param amount Number to convert, may float + * @param token Token + */ +export const bigAmountify = ( + amount: number, + token: Pick +): BigNumber => { + let [whole, decimals] = amount.toString().split("."); + let pow = "0"; + let powSign = true; + + if (decimals && decimals.includes("e")) { + powSign = !decimals.includes("e-"); + [decimals, pow] = powSign ? decimals.split("e") : decimals.split("e-"); + whole = whole; + } + + const wholeBn = utils.parseUnits(whole, token.decimals); + const power = BigNumber.from(10).pow(pow); + + if (decimals) { + const decimalsBn = utils + .parseUnits(decimals, token.decimals) + .div(BigNumber.from(10).pow(decimals.length)); + return powSign + ? wholeBn.add(decimalsBn).mul(power) + : wholeBn.add(decimalsBn).div(power); + } + return wholeBn; +}; + +/** + * Utility method to compute various settings associated to a payment involving conversion only + */ +export const getConversionPaymentValues = async ({ + baseAmount, + denominationCurrency, + selectedPaymentCurrency, + currencyManager, + provider, + fromAddress, +}: { + baseAmount: number; + denominationCurrency: CurrencyTypes.CurrencyDefinition; + selectedPaymentCurrency: CurrencyTypes.CurrencyDefinition; + currencyManager: CurrencyManager; + provider?: providers.Provider; + fromAddress?: string; +}): Promise => { + const conversionRate = await getConversionRate({ + from: denominationCurrency, + to: selectedPaymentCurrency, + currencyManager, + provider, + }); + + const minConversionAmount = bigAmountify( + baseAmount * Number(conversionRate), + selectedPaymentCurrency + ); + + const safeConversionAmount = bigAmountify( + baseAmount * + Number(conversionRate) * + getSlippageMargin(selectedPaymentCurrency), + selectedPaymentCurrency + ); + + const userBalance = BigNumber.from( + fromAddress && + provider && + "address" in selectedPaymentCurrency && + selectedPaymentCurrency.address + ? await getAnyErc20Balance( + selectedPaymentCurrency.address, + fromAddress, + provider + ) + : safeConversionAmount + ); + + const hasEnoughForSlippage = userBalance.gte(safeConversionAmount); + const hasEnough = userBalance.gte(minConversionAmount); + const isRisky = hasEnough && !hasEnoughForSlippage; + const slippageLevel = isRisky ? "risky" : ("safe" as SlippageLevel); + const conversionAmount = isRisky ? userBalance : safeConversionAmount; + + const conversion = { + currency: CurrencyManager.toStorageCurrency(selectedPaymentCurrency), + maxToSpend: conversionAmount.toString(), + currencyManager, + }; + + const totalAmountInPaymentCurrency = { + value: amountToFixedDecimal( + minConversionAmount, + selectedPaymentCurrency, + 4 + ), + currency: selectedPaymentCurrency, + }; + const safeBalance = { + value: amountToFixedDecimal( + safeConversionAmount, + selectedPaymentCurrency, + 4 + ), + currency: selectedPaymentCurrency, + }; + + return { + conversion, + slippageLevel, + totalAmountInPaymentCurrency, + safeBalance, + rate: conversionRate, + }; +}; diff --git a/packages/single-invoice/src/utils/wallet-utils.ts b/shared/utils/wallet-utils.ts similarity index 100% rename from packages/single-invoice/src/utils/wallet-utils.ts rename to shared/utils/wallet-utils.ts From 07234636147913b9b2a476d22fcc11eccd8f5afc Mon Sep 17 00:00:00 2001 From: MantisClone Date: Wed, 15 Jan 2025 15:01:24 -0500 Subject: [PATCH 05/18] Update README to include @requestnetwork/single-invoice in link cmds --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35937e7d..b5925820 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npm run link:all --app-path=../rn-checkout cd # Use local packages instead of the deployed ones -npm link @requestnetwork/create-invoice-form @requestnetwork/invoice-dashboard +npm link @requestnetwork/create-invoice-form @requestnetwork/invoice-dashboard @requestnetwork/single-invoice npm link @requestnetwork/payment-widget ``` From a1ab4943e928ff4de960a691bb303b1a3b10d7bb Mon Sep 17 00:00:00 2001 From: MantisClone Date: Wed, 15 Jan 2025 16:13:00 -0500 Subject: [PATCH 06/18] add single-invoice to the publish github action --- .github/workflows/npm-publish.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml index ba6be25f..112f3e27 100644 --- a/.github/workflows/npm-publish.yaml +++ b/.github/workflows/npm-publish.yaml @@ -18,6 +18,7 @@ jobs: - '@requestnetwork/create-invoice-form' - '@requestnetwork/invoice-dashboard' - '@requestnetwork/payment-widget' + - '@requestnetwork/single-invoice' steps: - name: Checkout repository 🛎️ uses: actions/checkout@v4 From 397c437f4e0feb9e2f56da004565039cd294b3a0 Mon Sep 17 00:00:00 2001 From: MantisClone Date: Wed, 15 Jan 2025 16:13:25 -0500 Subject: [PATCH 07/18] Add single-invoice to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b5925820..8e9c4b3f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Usage depends on the component. See packages/\/README.md | [@requestnetwork/create-invoice-form](packages/create-invoice-form/README.md) | [![npm version](https://badge.fury.io/js/%40requestnetwork%2Fcreate-invoice-form.svg)](https://badge.fury.io/js/%40requestnetwork%2Fcreate-invoice-form) | | [@requestnetwork/invoice-dashboard](packages/invoice-dashboard/README.md) | [![npm version](https://badge.fury.io/js/%40requestnetwork%2Finvoice-dashboard.svg)](https://badge.fury.io/js/%40requestnetwork%2Finvoice-dashboard) | | [@requestnetwork/payment-widget](packages/payment-widget/README.md) | [![npm version](https://badge.fury.io/js/%40requestnetwork%2Fpayment-widget.svg)](https://badge.fury.io/js/%40requestnetwork%2Fpayment-widget) | +| [@requestnetwork/single-invoice](packages/single-invoice/README.md) | [![npm version](https://badge.fury.io/js/%40requestnetwork%2Fsingle-invoice.svg)](https://badge.fury.io/js/%40requestnetwork%2Fsingle-invoice) | | [@requestnetwork/shared](packages/shared/README.md) | [![npm version](https://badge.fury.io/js/%40requestnetwork%2Fshared.svg)](https://badge.fury.io/js/%40requestnetwork%2Fshared) | From 0998eabbbe9140a01b148c18ca819c52f10e9e9c Mon Sep 17 00:00:00 2001 From: sstefdev Date: Mon, 20 Jan 2025 11:33:54 +0100 Subject: [PATCH 08/18] fix: added single-invoice share and path prop --- .../src/lib/dashboard/invoice-view.svelte | 31 ++++++++++++++++++- .../src/lib/react/InvoiceDashboard.d.ts | 2 ++ .../src/lib/view-requests.svelte | 2 ++ shared/icons/share.svelte | 21 +++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 shared/icons/share.svelte diff --git a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte index 856a6343..d38917e8 100644 --- a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte +++ b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte @@ -22,6 +22,7 @@ import Tooltip from "@requestnetwork/shared-components/tooltip.svelte"; // Icons import Download from "@requestnetwork/shared-icons/download.svelte"; + import Share from "@requestnetwork/shared-icons/share.svelte"; // Utils import { formatDate } from "@requestnetwork/shared-utils/formatDate"; import { checkStatus } from "@requestnetwork/shared-utils/checkStatus"; @@ -49,6 +50,7 @@ export let currencyManager: any; export let isRequestPayed: boolean; export let wagmiConfig: any; + export let singleInvoicePath: string; let network: string | undefined = request?.currencyInfo?.network || "mainnet"; let currency: CurrencyTypes.CurrencyDefinition | undefined = @@ -512,7 +514,12 @@ zksyncera: "0x144", base: "0x2105", }; - return networkIds[network]; + + const networkId = networkIds[network]; + if (!networkId) { + console.warn(`Unknown network: ${network}`); + } + return networkId; } // FIXME: Add rounding functionality @@ -599,6 +606,16 @@ })); } } + + // Add debug logging to see what's happening + $: { + const hexStringChain = "0x" + account?.chainId?.toString(16); + const networkId = getNetworkIdFromNetworkName(network || "mainnet"); + + correctChain = + hexStringChain.toLowerCase() === String(networkId).toLowerCase() || + Number(hexStringChain) === Number(networkId); + }
+ {#if singleInvoicePath} + + { + const shareUrl = `${window.location.origin}${singleInvoicePath}/${request.requestId}`; + navigator.clipboard.writeText(shareUrl); + toast.success("Share link copied to clipboard!"); + }} + /> + + {/if}

From:

@@ -948,6 +976,7 @@ .invoice-number svg { width: 13px; height: 13px; + cursor: pointer; } .invoice-address { diff --git a/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts b/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts index 9067cf18..17a97049 100644 --- a/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts +++ b/packages/invoice-dashboard/src/lib/react/InvoiceDashboard.d.ts @@ -8,6 +8,7 @@ export interface InvoiceDashboardProps { config: IConfig; wagmiConfig: WagmiConfig; requestNetwork: RequestNetwork | null | undefined; + singleInvoicePath: string; } /** * InvoiceDashboard is a React component that integrates with the Request Network to manage and display invoices. @@ -24,6 +25,7 @@ export interface InvoiceDashboardProps { * config={config} * wagmiConfig={wagmiConfig} * requestNetwork={requestNetwork} + * singleInvoicePath={singleInvoicePath} * /> */ declare const InvoiceDashboard: React.FC; diff --git a/packages/invoice-dashboard/src/lib/view-requests.svelte b/packages/invoice-dashboard/src/lib/view-requests.svelte index 59d23d0b..1677055e 100644 --- a/packages/invoice-dashboard/src/lib/view-requests.svelte +++ b/packages/invoice-dashboard/src/lib/view-requests.svelte @@ -63,6 +63,7 @@ export let config: IConfig; export let wagmiConfig: WagmiConfig; export let requestNetwork: RequestNetwork | null | undefined; + export let singleInvoicePath: string; let cipherProvider: CipherProvider | undefined; @@ -903,6 +904,7 @@ bind:currencyManager config={activeConfig} request={activeRequest} + {singleInvoicePath} /> {/if} diff --git a/shared/icons/share.svelte b/shared/icons/share.svelte new file mode 100644 index 00000000..c81c637d --- /dev/null +++ b/shared/icons/share.svelte @@ -0,0 +1,21 @@ + + + + + From f17559d4d6351ea1775b27d948467ab66a6e8bf4 Mon Sep 17 00:00:00 2001 From: sstefdev Date: Mon, 20 Jan 2025 12:18:26 +0100 Subject: [PATCH 09/18] chore: decouple utils folder --- .../src/lib/create-invoice-form.svelte | 7 +- .../src/lib/invoice/form-view.svelte | 8 +- .../src/lib/invoice/form.svelte | 9 +- .../src/lib/dashboard/invoice-view.svelte | 17 +- .../src/lib/view-requests.svelte | 18 +- .../invoice-dashboard/src/utils/capitalize.ts | 1 - .../invoice-dashboard/src/utils/chainlink.ts | 40 ---- .../invoice-dashboard/src/utils/conversion.ts | 50 ----- .../invoice-dashboard/src/utils/debounce.ts | 11 -- .../src/utils/formatAddress.ts | 21 --- .../src/utils/getConversionPaymentValues.ts | 171 ------------------ packages/invoice-dashboard/src/utils/index.ts | 4 - .../src/utils/wallet-utils.ts | 38 ---- .../lib/components/payment-complete.svelte | 9 +- .../src/lib/components/wallet-info.svelte | 2 +- .../src/lib/single-invoice.svelte | 18 +- shared/utils/index.ts | 41 +++++ 17 files changed, 92 insertions(+), 373 deletions(-) delete mode 100644 packages/invoice-dashboard/src/utils/capitalize.ts delete mode 100644 packages/invoice-dashboard/src/utils/chainlink.ts delete mode 100644 packages/invoice-dashboard/src/utils/conversion.ts delete mode 100644 packages/invoice-dashboard/src/utils/debounce.ts delete mode 100644 packages/invoice-dashboard/src/utils/formatAddress.ts delete mode 100644 packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts delete mode 100644 packages/invoice-dashboard/src/utils/index.ts delete mode 100644 packages/invoice-dashboard/src/utils/wallet-utils.ts create mode 100644 shared/utils/index.ts diff --git a/packages/create-invoice-form/src/lib/create-invoice-form.svelte b/packages/create-invoice-form/src/lib/create-invoice-form.svelte index 603ff7a9..5f12fb06 100644 --- a/packages/create-invoice-form/src/lib/create-invoice-form.svelte +++ b/packages/create-invoice-form/src/lib/create-invoice-form.svelte @@ -16,13 +16,12 @@ import { CurrencyTypes } from "@requestnetwork/types"; // Utils import { getInitialFormData, prepareRequestParams } from "./utils"; - import { config as defaultConfig } from "@requestnetwork/shared-utils/config"; - import { calculateInvoiceTotals } from "@requestnetwork/shared-utils/invoiceTotals"; import { + config as defaultConfig, + calculateInvoiceTotals, getCurrencySupportedNetworksForConversion, initializeCreateInvoiceCurrencyManager, - initializeCurrencyManager, - } from "@requestnetwork/shared-utils/initCurrencyManager"; + } from "@requestnetwork/shared-utils/index"; // Components import { InvoiceForm, InvoiceView } from "./invoice"; import Button from "@requestnetwork/shared-components/button.svelte"; diff --git a/packages/create-invoice-form/src/lib/invoice/form-view.svelte b/packages/create-invoice-form/src/lib/invoice/form-view.svelte index c5578509..12896448 100644 --- a/packages/create-invoice-form/src/lib/invoice/form-view.svelte +++ b/packages/create-invoice-form/src/lib/invoice/form-view.svelte @@ -11,9 +11,11 @@ import { CurrencyTypes } from "@requestnetwork/types"; // Utils - import { config as defaultConfig } from "@requestnetwork/shared-utils/config"; - import { calculateItemTotal } from "@requestnetwork/shared-utils/invoiceTotals"; - import { formatDate } from "@requestnetwork/shared-utils/formatDate"; + import { + formatDate, + calculateItemTotal, + config as defaultConfig, + } from "@requestnetwork/shared-utils/index"; export let defaultCurrencies; export let config: IConfig; diff --git a/packages/create-invoice-form/src/lib/invoice/form.svelte b/packages/create-invoice-form/src/lib/invoice/form.svelte index 8f489a53..8f901659 100644 --- a/packages/create-invoice-form/src/lib/invoice/form.svelte +++ b/packages/create-invoice-form/src/lib/invoice/form.svelte @@ -15,9 +15,12 @@ import type { IConfig, CustomFormData } from "@requestnetwork/shared-types"; // Utils - import { calculateItemTotal } from "@requestnetwork/shared-utils/invoiceTotals"; - import { checkAddress } from "@requestnetwork/shared-utils/checkEthAddress"; - import { inputDateFormat } from "@requestnetwork/shared-utils/formatDate"; + import { + checkAddress, + inputDateFormat, + calculateItemTotal, + } from "@requestnetwork/shared-utils/index"; + import { CurrencyTypes, CipherProviderTypes } from "@requestnetwork/types"; import isEmail from "validator/es/lib/isEmail"; diff --git a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte index d38917e8..d3583668 100644 --- a/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte +++ b/packages/invoice-dashboard/src/lib/dashboard/invoice-view.svelte @@ -24,15 +24,16 @@ import Download from "@requestnetwork/shared-icons/download.svelte"; import Share from "@requestnetwork/shared-icons/share.svelte"; // Utils - import { formatDate } from "@requestnetwork/shared-utils/formatDate"; - import { checkStatus } from "@requestnetwork/shared-utils/checkStatus"; - import { calculateItemTotal } from "@requestnetwork/shared-utils/invoiceTotals"; - import { exportToPDF } from "@requestnetwork/shared-utils/generateInvoice"; - import { getCurrencyFromManager } from "@requestnetwork/shared-utils/getCurrency"; - import { onMount } from "svelte"; + import { + exportToPDF, + formatDate, + checkStatus, + getEthersSigner, + calculateItemTotal, + getCurrencyFromManager, + getConversionPaymentValues, + } from "@requestnetwork/shared-utils/index"; import { formatUnits } from "viem"; - import { getConversionPaymentValues } from "../../utils/getConversionPaymentValues"; - import { getEthersSigner } from "../../utils"; interface EntityInfo { value: string; diff --git a/packages/invoice-dashboard/src/lib/view-requests.svelte b/packages/invoice-dashboard/src/lib/view-requests.svelte index 1677055e..6540fbc7 100644 --- a/packages/invoice-dashboard/src/lib/view-requests.svelte +++ b/packages/invoice-dashboard/src/lib/view-requests.svelte @@ -36,18 +36,22 @@ import type { IConfig } from "@requestnetwork/shared-types"; import type { RequestNetwork } from "@requestnetwork/request-client.js"; // Utils - import { config as defaultConfig } from "@requestnetwork/shared-utils/config"; - import { initializeCurrencyManager } from "@requestnetwork/shared-utils/initCurrencyManager"; - import { exportToPDF } from "@requestnetwork/shared-utils/generateInvoice"; - import { getCurrencyFromManager } from "@requestnetwork/shared-utils/getCurrency"; + import { + debounce, + checkStatus, + formatUnits, + formatAddress, + getEthersSigner, + exportToPDF, + getCurrencyFromManager, + config as defaultConfig, + initializeCurrencyManager, + } from "@requestnetwork/shared-utils/index"; import { CurrencyManager } from "@requestnetwork/currency"; import { onDestroy, onMount, tick } from "svelte"; - import { formatUnits } from "viem"; - import { debounce, formatAddress, getEthersSigner } from "../utils"; import { Drawer, InvoiceView } from "./dashboard"; import { getPaymentNetworkExtension } from "@requestnetwork/payment-detection"; import { CipherProviderTypes, CurrencyTypes } from "@requestnetwork/types"; - import { checkStatus } from "@requestnetwork/shared-utils/checkStatus"; import { ethers } from "ethers"; interface CipherProvider extends CipherProviderTypes.ICipherProvider { diff --git a/packages/invoice-dashboard/src/utils/capitalize.ts b/packages/invoice-dashboard/src/utils/capitalize.ts deleted file mode 100644 index ac21f51c..00000000 --- a/packages/invoice-dashboard/src/utils/capitalize.ts +++ /dev/null @@ -1 +0,0 @@ -export const capitalize = (str: string) => (str && str[0].toUpperCase() + str.slice(1)) || "" diff --git a/packages/invoice-dashboard/src/utils/chainlink.ts b/packages/invoice-dashboard/src/utils/chainlink.ts deleted file mode 100644 index a7c72b1f..00000000 --- a/packages/invoice-dashboard/src/utils/chainlink.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { isISO4217Currency } from "@requestnetwork/currency"; -import { chainlinkConversionPath } from "@requestnetwork/smart-contracts"; -import { CurrencyTypes } from "@requestnetwork/types"; -import { BigNumber, providers } from "ethers"; - -export const getChainlinkRate = async ( - from: CurrencyTypes.CurrencyDefinition, - to: CurrencyTypes.CurrencyDefinition, - { - network, - provider, - currencyManager, - }: { - network: CurrencyTypes.EvmChainName; - provider: providers.Provider; - currencyManager: CurrencyTypes.ICurrencyManager; - }, -) => { - try { - const chainlink = chainlinkConversionPath.connect(network, provider); - const path = currencyManager.getConversionPath(from, to, network); - if (!path) return null; - const result = await chainlink.getRate(path); - if (!result) return null; - - // ChainlinkConversionPath uses 8 decimals for fiat. - const fromDecimals = isISO4217Currency(from) ? 8 : from.decimals; - const toDecimals = isISO4217Currency(to) ? 8 : to.decimals; - const value = result.rate - .mul(BigNumber.from(10).pow(fromDecimals)) - .div(BigNumber.from(10).pow(toDecimals)); - return { - value, - decimals: result.decimals.toString().length - 1, - }; - } catch (e) { - console.error('Error fetching Chainlink rate:', e); - return null; - } -}; diff --git a/packages/invoice-dashboard/src/utils/conversion.ts b/packages/invoice-dashboard/src/utils/conversion.ts deleted file mode 100644 index e42a6da3..00000000 --- a/packages/invoice-dashboard/src/utils/conversion.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { isISO4217Currency } from "@requestnetwork/currency"; -import { CurrencyTypes } from "@requestnetwork/types"; -import { EvmChainName } from "@requestnetwork/types/dist/currency-types"; -import { providers, utils } from "ethers"; -import { getChainlinkRate } from './chainlink' - -/** - * Maximum slippage (swap) or conversion evolution (conversion pn) - */ -const MAX_SLIPPAGE_DEFAULT = 1.03; -const MAX_SLIPPAGE_LOW_VOLATILITY = 1.01; -export const lowVolatilityTokens = ["DAI", "USDC", "USDT"]; - -export const getSlippageMargin = (currency: CurrencyTypes.CurrencyInput) => { - return lowVolatilityTokens.includes(currency.symbol) - ? MAX_SLIPPAGE_LOW_VOLATILITY - : MAX_SLIPPAGE_DEFAULT; -}; - -/** - * Get the conversion rate between two currencies. - */ -export const getConversionRate = async ({ - from, - to, - currencyManager, - provider, -}: { - from: CurrencyTypes.CurrencyDefinition; - to: CurrencyTypes.CurrencyDefinition; - currencyManager?: CurrencyTypes.ICurrencyManager; - provider?: providers.Provider; -}): Promise => { - if (!isISO4217Currency(to) && currencyManager && provider) { - const network = to.network as EvmChainName; - try { - const chainlinkRate = await getChainlinkRate(from, to, { - network, - currencyManager, - provider, - }); - if (chainlinkRate) { - return Number(utils.formatUnits(chainlinkRate.value, chainlinkRate.decimals)) - } - } catch (e) { - console.error("Error getting chainlink rate", e); - throw e; - } - } -}; diff --git a/packages/invoice-dashboard/src/utils/debounce.ts b/packages/invoice-dashboard/src/utils/debounce.ts deleted file mode 100644 index 1462f81b..00000000 --- a/packages/invoice-dashboard/src/utils/debounce.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const debounce = (func: Function, wait: number) => { - let timeout: NodeJS.Timeout; - return (...args: any[]) => { - const later = () => { - clearTimeout(timeout); - func.apply(this, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -}; diff --git a/packages/invoice-dashboard/src/utils/formatAddress.ts b/packages/invoice-dashboard/src/utils/formatAddress.ts deleted file mode 100644 index 9b91118e..00000000 --- a/packages/invoice-dashboard/src/utils/formatAddress.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getAddress } from "viem"; - -export const formatAddress = ( - address: string, - first: number = 6, - last: number = 4 -): string | '-' => { - if (!address || address.length === 0) { - // Consider using a proper logging service - console.warn("[formatAddress] No address provided"); - return '-'; - } else { - try { - const checksumAddress = getAddress(address); - return `${checksumAddress.slice(0, first)}...${checksumAddress.slice(-last)}`; - } catch (error) { - console.error("Invalid address: ", error); - return '-'; - } - } -}; diff --git a/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts b/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts deleted file mode 100644 index f9d306d9..00000000 --- a/packages/invoice-dashboard/src/utils/getConversionPaymentValues.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { CurrencyManager } from "@requestnetwork/currency"; -import { CurrencyTypes } from "@requestnetwork/types"; -import { getAnyErc20Balance } from "@requestnetwork/payment-processor"; -import { BigNumber, BigNumberish, providers, utils } from "ethers"; - -import { getConversionRate, getSlippageMargin } from "./conversion"; - -export type SlippageLevel = "safe" | "risky"; - -interface ConversionPaymentValues { - conversion: { - currency: CurrencyTypes.ICurrency; - maxToSpend: string; - currencyManager: CurrencyManager; - }; - slippageLevel: SlippageLevel; - totalAmountInPaymentCurrency: { - value: number; - currency: CurrencyTypes.CurrencyDefinition; - }; - safeBalance: { - value: number; - currency: CurrencyTypes.CurrencyDefinition; - }; - rate: string; -} - -export const formatUnits = ( - amount: BigNumber, - token: CurrencyTypes.CurrencyDefinition -): string => { - return utils.formatUnits(amount, token.decimals); -}; - -export const toFixedDecimal = (numberToFormat: number, decimals?: number) => { - const MAX_DECIMALS = decimals !== undefined ? decimals : 5; - return Number(numberToFormat.toFixed(MAX_DECIMALS)); -}; - -export const amountToFixedDecimal = ( - amount: BigNumberish, - currency: CurrencyTypes.CurrencyDefinition, - decimals?: number -) => { - return toFixedDecimal( - Number.parseFloat(formatUnits(BigNumber.from(amount), currency)), - decimals - ); -}; - -/** - * Converts a number, even floating, to a BigNumber - * @param amount Number to convert, may float - * @param token Token - */ -export const bigAmountify = ( - amount: number, - token: Pick -): BigNumber => { - let [whole, decimals] = amount.toString().split("."); - let pow = "0"; - let powSign = true; - - if (decimals && decimals.includes("e")) { - powSign = !decimals.includes("e-"); - [decimals, pow] = powSign ? decimals.split("e") : decimals.split("e-"); - whole = whole; - } - - const wholeBn = utils.parseUnits(whole, token.decimals); - const power = BigNumber.from(10).pow(pow); - - if (decimals) { - const decimalsBn = utils - .parseUnits(decimals, token.decimals) - .div(BigNumber.from(10).pow(decimals.length)); - return powSign - ? wholeBn.add(decimalsBn).mul(power) - : wholeBn.add(decimalsBn).div(power); - } - return wholeBn; -}; - -/** - * Utility method to compute various settings associated to a payment involving conversion only - */ -export const getConversionPaymentValues = async ({ - baseAmount, - denominationCurrency, - selectedPaymentCurrency, - currencyManager, - provider, - fromAddress, -}: { - baseAmount: number; - denominationCurrency: CurrencyTypes.CurrencyDefinition; - selectedPaymentCurrency: CurrencyTypes.CurrencyDefinition; - currencyManager: CurrencyManager; - provider?: providers.Provider; - fromAddress?: string; -}): Promise => { - const conversionRate = await getConversionRate({ - from: denominationCurrency, - to: selectedPaymentCurrency, - currencyManager, - provider, - }); - - const minConversionAmount = bigAmountify( - baseAmount * Number(conversionRate), - selectedPaymentCurrency - ); - const safeConversionAmount = bigAmountify( - baseAmount * - Number(conversionRate) * - getSlippageMargin(selectedPaymentCurrency), - selectedPaymentCurrency - ); - - const userBalance = BigNumber.from( - fromAddress && - provider && - selectedPaymentCurrency.type === "ERC20" && - "address" in selectedPaymentCurrency && - selectedPaymentCurrency.address - ? await getAnyErc20Balance( - selectedPaymentCurrency.address, - fromAddress, - provider - ) - : safeConversionAmount - ); - - const hasEnoughForSlippage = userBalance.gte(safeConversionAmount); - const hasEnough = userBalance.gte(minConversionAmount); - const isRisky = hasEnough && !hasEnoughForSlippage; - - const slippageLevel = isRisky ? "risky" : ("safe" as SlippageLevel); - const conversionAmount = isRisky ? userBalance : safeConversionAmount; - - const conversion = { - currency: CurrencyManager.toStorageCurrency(selectedPaymentCurrency), - maxToSpend: conversionAmount.toString(), - currencyManager, - }; - - const totalAmountInPaymentCurrency = { - value: amountToFixedDecimal( - minConversionAmount, - selectedPaymentCurrency, - 4 - ), - currency: selectedPaymentCurrency, - }; - const safeBalance = { - value: amountToFixedDecimal( - safeConversionAmount, - selectedPaymentCurrency, - 4 - ), - currency: selectedPaymentCurrency, - }; - - return { - conversion, - slippageLevel, - totalAmountInPaymentCurrency, - safeBalance, - rate: conversionRate, - }; -}; diff --git a/packages/invoice-dashboard/src/utils/index.ts b/packages/invoice-dashboard/src/utils/index.ts deleted file mode 100644 index 97101620..00000000 --- a/packages/invoice-dashboard/src/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { debounce } from "./debounce"; -export { formatAddress } from "./formatAddress"; -export { publicClientToProvider, getEthersSigner } from "./wallet-utils"; -export { capitalize } from "./capitalize"; diff --git a/packages/invoice-dashboard/src/utils/wallet-utils.ts b/packages/invoice-dashboard/src/utils/wallet-utils.ts deleted file mode 100644 index e6aa563f..00000000 --- a/packages/invoice-dashboard/src/utils/wallet-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Config, getConnectorClient } from "@wagmi/core"; -import { providers } from "ethers"; -import type { Account, Chain, Client, Transport } from "viem"; - -export const publicClientToProvider = (publicClient: any) => { - const { chains, provider } = publicClient; - const network = { - chainId: parseInt(chains[0].id, 16), - name: chains[0].name, - }; - - return new providers.JsonRpcProvider(provider.url as string, network); -}; - -export function clientToSigner(client: Client) { - const { account, chain, transport } = client; - const network = { - chainId: chain.id, - name: chain.name, - ensAddress: chain.contracts?.ensRegistry?.address, - }; - - const provider = new providers.Web3Provider(transport, network); - const signer = provider.getSigner(account.address); - return signer; -} - -export async function getEthersSigner( - config: Config, - { chainId }: { chainId?: number } = {} -) { - try { - const client = await getConnectorClient(config, { chainId }); - return clientToSigner(client); - } catch (e) { - console.log("Failed to obtain client from getConnectorClient"); - } -} diff --git a/packages/payment-widget/src/lib/components/payment-complete.svelte b/packages/payment-widget/src/lib/components/payment-complete.svelte index 73e9fca3..d0d3b659 100644 --- a/packages/payment-widget/src/lib/components/payment-complete.svelte +++ b/packages/payment-widget/src/lib/components/payment-complete.svelte @@ -1,8 +1,11 @@ Date: Mon, 20 Jan 2025 18:47:29 +0100 Subject: [PATCH 17/18] fix: share svg viewbox --- shared/icons/share.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/icons/share.svelte b/shared/icons/share.svelte index 9d7a6cdd..e5bb19a6 100644 --- a/shared/icons/share.svelte +++ b/shared/icons/share.svelte @@ -5,7 +5,7 @@ Date: Mon, 20 Jan 2025 18:50:14 +0100 Subject: [PATCH 18/18] chore: update create invoice form declaration file --- .../create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts b/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts index de0404b9..bc5a1885 100644 --- a/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts +++ b/packages/create-invoice-form/src/lib/react/CreateInvoiceForm.d.ts @@ -8,7 +8,7 @@ export interface CreateInvoiceFormProps { config: IConfig; wagmiConfig: WagmiConfig; requestNetwork: RequestNetwork | null | undefined; - currencies: string[]; + currencies?: string[]; } /**