diff --git a/examples/nft-checkout/.env b/examples/nft-checkout/.env new file mode 100644 index 000000000..15cb40c49 --- /dev/null +++ b/examples/nft-checkout/.env @@ -0,0 +1,3 @@ +VITE_OPENSEA_API_KEY=ee7460014fda4f58804f25c29a27df35 +VITE_WALLET_CONNECT=5432e3507d41270bee46b7b85bbc2ef8 + diff --git a/examples/nft-checkout/README.md b/examples/nft-checkout/README.md new file mode 100644 index 000000000..51e546b23 --- /dev/null +++ b/examples/nft-checkout/README.md @@ -0,0 +1,35 @@ +# LI.FI Widget NFT Checkout + +The demo of the LI.FI Widget NFT Checkout based on the OpenSea API. + +### How to run? + +``` +pnpm dev +``` + +### How to test? + +1. Find an NFT on the [OpenSea](https://opensea.io/). Please make sure it has an active listing and the test wallet has enough tokens to buy it. While we will be able to pay with any token in the process, the OpenSea SDK checks for the token in which the NFT is listed to generate transaction data. +2. Let's say we found this NFT https://opensea.io/assets/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 +3. We need to replace the `opensea.io/assets` part with `localhost:3000` or `widget.li.fi`, depending on the testing environment, so the final URL should look like this +http://localhost:3000/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 or this https://widget.li.fi/base/0x9e81df5258908dbeef4f841d0ab3816b10850426/2578 +4. Open the URL and make sure the test wallet is switched to the chain the NFT is on so OpenSea SDK can generate transaction data. +5. Select any token on any chain and pay for NFT. + +### Live Demo + +https://github.com/lifinance/widget/assets/18644653/af360181-3856-4276-b309-f923f476f40b + +#### Demo Transactions + +https://optimistic.etherscan.io/tx/0xa9f4e4304822cfe01808555b66e047761361c9e54b2387f93e23e9ffb92ba151 +https://polygonscan.com/tx/0x370682cbbc544e0ea258da774220b529a086c4b22941b924587cd2e0105579f6 + +### What does it look like? + + + +### Questions? + +Please don't hesitate to open an issue or contact us if you have any questions. diff --git a/examples/nft-checkout/index.html b/examples/nft-checkout/index.html new file mode 100644 index 000000000..59d64ff5b --- /dev/null +++ b/examples/nft-checkout/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + LI.FI Widget + + + + +
+ + + diff --git a/examples/nft-checkout/package.json b/examples/nft-checkout/package.json new file mode 100644 index 000000000..d2d223caf --- /dev/null +++ b/examples/nft-checkout/package.json @@ -0,0 +1,61 @@ +{ + "name": "nft-checkout", + "version": "1.0.0", + "type": "module", + "scripts": { + "analyze": "source-map-explorer 'dist/assets/*.js' --no-border-checks", + "dev": "vite", + "build": "tsc && vite build", + "check:types": "tsc --noEmit", + "clean": "rm -rf dist tsconfig.tsbuildinfo", + "preview": "vite preview", + "pre-push:validate": "pnpm check:types" + }, + "author": "Eugene Chybisov ", + "dependencies": { + "@lifi/sdk": "^4.0.0-alpha.13", + "@lifi/wallet-management": "workspace:*", + "@lifi/widget": "workspace:*", + "@lifi/widget-provider-bitcoin": "workspace:*", + "@lifi/widget-provider-ethereum": "workspace:*", + "@lifi/widget-provider-solana": "workspace:*", + "@lifi/widget-provider-sui": "workspace:*", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", + "@mui/system": "^7.3.6", + "@opensea/seaport-js": "4.0.6", + "@tanstack/react-query": "^5.90.20", + "bignumber.js": "^9.3.0", + "ethers": "^6.16.0", + "events": "^3.3.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.0", + "viem": "^2.45.1", + "wagmi": "^3.1.0" + }, + "devDependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@vitejs/plugin-react-swc": "^4.2.3", + "source-map-explorer": "^2.5.3", + "typescript": "^5.9.3", + "vite": "^7.3.0", + "vite-plugin-node-polyfills": "0.25.0", + "web-vitals": "^5.1.0" + }, + "browserslist": { + "production": [ + "> 1%", + "last 2 versions", + "not dead", + "not IE > 0", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "private": true +} diff --git a/examples/nft-checkout/public/favicon.ico b/examples/nft-checkout/public/favicon.ico new file mode 100644 index 000000000..073f01ec8 Binary files /dev/null and b/examples/nft-checkout/public/favicon.ico differ diff --git a/examples/nft-checkout/public/favicon.png b/examples/nft-checkout/public/favicon.png new file mode 100644 index 000000000..73889a74f Binary files /dev/null and b/examples/nft-checkout/public/favicon.png differ diff --git a/examples/nft-checkout/public/logo192.png b/examples/nft-checkout/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/examples/nft-checkout/public/logo192.png differ diff --git a/examples/nft-checkout/public/logo512.png b/examples/nft-checkout/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/examples/nft-checkout/public/logo512.png differ diff --git a/examples/nft-checkout/public/manifest.json b/examples/nft-checkout/public/manifest.json new file mode 100644 index 000000000..41431693d --- /dev/null +++ b/examples/nft-checkout/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "LI.FI Widget", + "name": "LI.FI Widget", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/nft-checkout/public/robots.txt b/examples/nft-checkout/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/nft-checkout/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/nft-checkout/src/App.tsx b/examples/nft-checkout/src/App.tsx new file mode 100644 index 000000000..dafe26d38 --- /dev/null +++ b/examples/nft-checkout/src/App.tsx @@ -0,0 +1,59 @@ +import { LiFiWidget } from '@lifi/widget' +import { Box, CssBaseline } from '@mui/material' +import type { NFTNetwork } from './components/NFTOpenSea/index.js' +import { + NFTOpenSea, + NFTOpenSeaSecondary, + openSeaContractTool, +} from './components/NFTOpenSea/index.js' +import { widgetConfig } from './config.js' +import './index.css' + +export const App = () => { + const pathnameParams = window.location.pathname.substring(1).split('/') + + return ( + + + + + } + contractSecondaryComponent={ + + } + contractCompactComponent={ + + } + contractTool={openSeaContractTool} + config={widgetConfig} + integrator={widgetConfig.integrator} + open + /> + + + ) +} diff --git a/packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSea.tsx b/examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSea.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSea.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSea.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx b/examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/NFTOpenSeaSecondary.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/getEthersSigner.ts b/examples/nft-checkout/src/components/NFTOpenSea/getEthersSigner.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/getEthersSigner.ts rename to examples/nft-checkout/src/components/NFTOpenSea/getEthersSigner.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/index.ts b/examples/nft-checkout/src/components/NFTOpenSea/index.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/index.ts rename to examples/nft-checkout/src/components/NFTOpenSea/index.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/types.ts b/examples/nft-checkout/src/components/NFTOpenSea/types.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/types.ts rename to examples/nft-checkout/src/components/NFTOpenSea/types.ts diff --git a/packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx b/examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaFulfillment.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaOrder.tsx b/examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaOrder.tsx similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/useOpenSeaOrder.tsx rename to examples/nft-checkout/src/components/NFTOpenSea/useOpenSeaOrder.tsx diff --git a/packages/widget-embedded/src/components/NFTOpenSea/utils.ts b/examples/nft-checkout/src/components/NFTOpenSea/utils.ts similarity index 100% rename from packages/widget-embedded/src/components/NFTOpenSea/utils.ts rename to examples/nft-checkout/src/components/NFTOpenSea/utils.ts diff --git a/packages/widget-embedded/src/config.ts b/examples/nft-checkout/src/config.ts similarity index 100% rename from packages/widget-embedded/src/config.ts rename to examples/nft-checkout/src/config.ts diff --git a/packages/widget-embedded/src/index.css b/examples/nft-checkout/src/index.css similarity index 100% rename from packages/widget-embedded/src/index.css rename to examples/nft-checkout/src/index.css diff --git a/packages/widget-embedded/src/index.tsx b/examples/nft-checkout/src/main.tsx similarity index 95% rename from packages/widget-embedded/src/index.tsx rename to examples/nft-checkout/src/main.tsx index 34da85d6c..60611f65a 100644 --- a/packages/widget-embedded/src/index.tsx +++ b/examples/nft-checkout/src/main.tsx @@ -45,6 +45,5 @@ root.render( // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals if (import.meta.env.DEV) { - // biome-ignore lint/suspicious/noConsole: allowed in dev reportWebVitals(console.log) } diff --git a/examples/nft-checkout/src/reportWebVitals.ts b/examples/nft-checkout/src/reportWebVitals.ts new file mode 100644 index 000000000..0cf1bb720 --- /dev/null +++ b/examples/nft-checkout/src/reportWebVitals.ts @@ -0,0 +1,13 @@ +import type { MetricType } from 'web-vitals' + +export const reportWebVitals = (onPerfEntry?: (metric: MetricType) => void) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { + onCLS(onPerfEntry) + onINP(onPerfEntry) + onFCP(onPerfEntry) + onLCP(onPerfEntry) + onTTFB(onPerfEntry) + }) + } +} diff --git a/examples/nft-checkout/src/vite-env.d.ts b/examples/nft-checkout/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/examples/nft-checkout/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/nft-checkout/tsconfig.json b/examples/nft-checkout/tsconfig.json new file mode 100644 index 000000000..bd4ab8766 --- /dev/null +++ b/examples/nft-checkout/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", + "outDir": "dist", + "rootDir": "./src", + "composite": true + }, + "include": ["src"] +} diff --git a/examples/nft-checkout/tsconfig.node.json b/examples/nft-checkout/tsconfig.node.json new file mode 100644 index 000000000..9617166fd --- /dev/null +++ b/examples/nft-checkout/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/nft-checkout/vite.config.ts b/examples/nft-checkout/vite.config.ts new file mode 100644 index 000000000..1dfef3f6a --- /dev/null +++ b/examples/nft-checkout/vite.config.ts @@ -0,0 +1,32 @@ +import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' +import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' +import { nodePolyfills } from 'vite-plugin-node-polyfills' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [nodePolyfills(), react()], + esbuild: { + target: 'esnext', + }, + build: { + sourcemap: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis', + }, + plugins: [ + NodeGlobalsPolyfillPlugin({ + process: true, + buffer: true, + }), + ], + }, + }, + server: { + port: 3000, + open: true, + }, +}) diff --git a/examples/vite-iframe-wagmi/.env b/examples/vite-iframe-wagmi/.env new file mode 100644 index 000000000..147bb3c95 --- /dev/null +++ b/examples/vite-iframe-wagmi/.env @@ -0,0 +1 @@ +VITE_WIDGET_URL=https://widget.li.fi diff --git a/examples/vite-iframe-wagmi/.env.localhost b/examples/vite-iframe-wagmi/.env.localhost new file mode 100644 index 000000000..daedfcbba --- /dev/null +++ b/examples/vite-iframe-wagmi/.env.localhost @@ -0,0 +1 @@ +VITE_WIDGET_URL=http://localhost:3000 diff --git a/examples/vite-iframe-wagmi/index.html b/examples/vite-iframe-wagmi/index.html new file mode 100644 index 000000000..88b1599e0 --- /dev/null +++ b/examples/vite-iframe-wagmi/index.html @@ -0,0 +1,12 @@ + + + + + + Widget Light — Host + + +
+ + + diff --git a/examples/vite-iframe-wagmi/package.json b/examples/vite-iframe-wagmi/package.json new file mode 100644 index 000000000..1b9fc55ac --- /dev/null +++ b/examples/vite-iframe-wagmi/package.json @@ -0,0 +1,26 @@ +{ + "name": "vite-iframe-wagmi", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@lifi/widget-light": "^4.0.0-alpha.2", + "@tanstack/react-query": "^5.90.20", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "viem": "^2.45.1", + "wagmi": "^3.4.2" + }, + "devDependencies": { + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.3", + "typescript": "^5.9.3", + "vite": "^7.3.0" + } +} diff --git a/examples/vite-iframe-wagmi/src/App.tsx b/examples/vite-iframe-wagmi/src/App.tsx new file mode 100644 index 000000000..464ae8d29 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/App.tsx @@ -0,0 +1,31 @@ +import { LiFiWidgetLight } from '@lifi/widget-light' +import { useEthereumIframeHandler } from '@lifi/widget-light/ethereum' +import { useMemo } from 'react' +import { WalletHeader } from './components/WalletHeader' +import { widgetConfig } from './widgetConfig' + +const WIDGET_URL = import.meta.env.VITE_WIDGET_URL || 'https://widget.li.fi' +const WIDGET_ORIGIN = new URL(WIDGET_URL).origin + +export function HostApp() { + const ethHandler = useEthereumIframeHandler() + const handlers = useMemo(() => [ethHandler], [ethHandler]) + + return ( +
+ + +
+ +
+
+ ) +} diff --git a/examples/vite-iframe-wagmi/src/components/WalletHeader.tsx b/examples/vite-iframe-wagmi/src/components/WalletHeader.tsx new file mode 100644 index 000000000..1c1217b12 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/components/WalletHeader.tsx @@ -0,0 +1,53 @@ +import { useConnect, useConnection, useDisconnect } from 'wagmi' +import { injected } from 'wagmi/connectors' + +function shortenAddress(address?: string) { + if (!address) { + return '' + } + return `${address.slice(0, 6)}…${address.slice(-4)}` +} + +export function WalletHeader() { + const { address, isConnected, chain } = useConnection() + const { mutate: connect } = useConnect() + const { mutate: disconnect } = useDisconnect() + + return ( +
+
+ Widget Light — Host + + Connect your wallet — all widget transactions will be signed through + your connected wallet. + +
+ +
+ {isConnected && ( +
+ {shortenAddress(address)} + {chain && {chain.name}} + +
+ )} + + +
+
+ ) +} diff --git a/examples/vite-iframe-wagmi/src/main.css b/examples/vite-iframe-wagmi/src/main.css new file mode 100644 index 000000000..41fa8beb8 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/main.css @@ -0,0 +1,122 @@ +* { + margin: 0; + box-sizing: border-box; +} + +body { + font-family: + Helvetica, "Helvetica Neue", Arial, "Nimbus Sans", "Liberation Sans", + sans-serif; +} + +/* App */ + +.app { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.app-content { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + min-height: 0; + padding-top: 48px; + gap: 16px; +} + +.widget-iframe { + border: 0; + width: 100%; + flex: 1; + min-height: 0; +} + +/* WalletHeader */ + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 24px; + border-bottom: 1px solid #eee; + background-color: #fafafa; +} + +.header-left { + display: flex; + flex-direction: column; + gap: 4px; +} + +.header-title { + font-weight: 700; + font-size: 20px; +} + +.header-hint { + color: #747474; + font-size: 13px; +} + +.header-hint-hidden { + visibility: hidden; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.account-chip { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border: 1px solid #ddd; + border-radius: 20px; + font-size: 13px; +} + +.account-address { + font-weight: 500; +} + +.account-chain { + color: #747474; + font-size: 11px; +} + +.btn-disconnect { + background: none; + border: none; + cursor: pointer; + padding: 0; + font-size: 13px; + color: #999; + line-height: 1; +} + +.btn { + padding: 8px 20px; + border-radius: 12px; + font-weight: 600; + font-size: 14px; + cursor: pointer; +} + +.btn-primary { + border: none; + background-color: #5c67ff; + color: #fff; +} + +.btn-secondary { + border: 1px solid #5c67ff; + background-color: transparent; + color: #5c67ff; +} diff --git a/examples/vite-iframe-wagmi/src/main.tsx b/examples/vite-iframe-wagmi/src/main.tsx new file mode 100644 index 000000000..95e0beda2 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/main.tsx @@ -0,0 +1,18 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import ReactDOM from 'react-dom/client' +import { HostApp } from './App' +import './main.css' +import { WalletProvider } from './providers/WalletProvider' + +const queryClient = new QueryClient() + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + +) diff --git a/examples/vite-iframe-wagmi/src/providers/WalletProvider.tsx b/examples/vite-iframe-wagmi/src/providers/WalletProvider.tsx new file mode 100644 index 000000000..1e21aaf70 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/providers/WalletProvider.tsx @@ -0,0 +1,19 @@ +import type { FC, PropsWithChildren } from 'react' +import { createClient, http } from 'viem' +import { arbitrum, base, mainnet, optimism, polygon } from 'viem/chains' +import { createConfig, WagmiProvider } from 'wagmi' +import { injected } from 'wagmi/connectors' + +const config = createConfig({ + chains: [mainnet, arbitrum, optimism, base, polygon], + connectors: [injected()], + client({ chain }) { + return createClient({ chain, transport: http() }) + }, + multiInjectedProviderDiscovery: true, + ssr: false, +}) + +export const WalletProvider: FC = ({ children }) => ( + {children} +) diff --git a/examples/vite-iframe-wagmi/src/vite-env.d.ts b/examples/vite-iframe-wagmi/src/vite-env.d.ts new file mode 100644 index 000000000..10c8089ad --- /dev/null +++ b/examples/vite-iframe-wagmi/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +declare global { + interface ImportMetaEnv { + readonly VITE_WIDGET_URL: string + readonly VITE_WALLET_CONNECT_PROJECT_ID?: string + } + + interface ImportMeta { + readonly env: ImportMetaEnv + } +} + +export {} diff --git a/examples/vite-iframe-wagmi/src/widgetConfig.ts b/examples/vite-iframe-wagmi/src/widgetConfig.ts new file mode 100644 index 000000000..6e7996482 --- /dev/null +++ b/examples/vite-iframe-wagmi/src/widgetConfig.ts @@ -0,0 +1,18 @@ +import type { WidgetLightConfig } from '@lifi/widget-light' + +export const widgetConfig: WidgetLightConfig = { + integrator: 'vite-iframe-example', + variant: 'wide', + theme: { + container: { + border: '1px solid rgb(234, 234, 234)', + borderRadius: '16px', + height: 'fit-content', + }, + }, + sdkConfig: { + routeOptions: { + maxPriceImpact: 0.4, + }, + }, +} diff --git a/examples/vite-iframe-wagmi/tsconfig.json b/examples/vite-iframe-wagmi/tsconfig.json new file mode 100644 index 000000000..bb0095e8e --- /dev/null +++ b/examples/vite-iframe-wagmi/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "types": ["vite/client"], + "useDefineForClassFields": true + }, + "include": ["src", "vite.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/vite-iframe-wagmi/tsconfig.node.json b/examples/vite-iframe-wagmi/tsconfig.node.json new file mode 100644 index 000000000..16dfedc6a --- /dev/null +++ b/examples/vite-iframe-wagmi/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vite-iframe-wagmi/vite.config.ts b/examples/vite-iframe-wagmi/vite.config.ts new file mode 100644 index 000000000..99fab386e --- /dev/null +++ b/examples/vite-iframe-wagmi/vite.config.ts @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [react()], + esbuild: { + target: 'esnext', + }, + server: { + port: 4000, + open: true, + }, +}) diff --git a/examples/vite-iframe/.env b/examples/vite-iframe/.env new file mode 100644 index 000000000..147bb3c95 --- /dev/null +++ b/examples/vite-iframe/.env @@ -0,0 +1 @@ +VITE_WIDGET_URL=https://widget.li.fi diff --git a/examples/vite-iframe/.env.example b/examples/vite-iframe/.env.example new file mode 100644 index 000000000..40097a9b0 --- /dev/null +++ b/examples/vite-iframe/.env.example @@ -0,0 +1,8 @@ +# Copy this file to .env and fill in your values. + +# Widget iframe URL (defaults to https://widget.li.fi) +VITE_WIDGET_URL=https://widget.li.fi + +# WalletConnect project ID (optional — injected wallet works without it) +# Get one at https://cloud.walletconnect.com/ +VITE_WALLET_CONNECT_PROJECT_ID= diff --git a/examples/vite-iframe/.env.localhost b/examples/vite-iframe/.env.localhost new file mode 100644 index 000000000..daedfcbba --- /dev/null +++ b/examples/vite-iframe/.env.localhost @@ -0,0 +1 @@ +VITE_WIDGET_URL=http://localhost:3000 diff --git a/examples/vite-iframe/index.html b/examples/vite-iframe/index.html new file mode 100644 index 000000000..b0ef8f513 --- /dev/null +++ b/examples/vite-iframe/index.html @@ -0,0 +1,12 @@ + + + + + + Widget Light — Host + + +
+ + + diff --git a/examples/vite-iframe/package.json b/examples/vite-iframe/package.json new file mode 100644 index 000000000..94b1f3fa8 --- /dev/null +++ b/examples/vite-iframe/package.json @@ -0,0 +1,44 @@ +{ + "name": "vite-iframe", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "analyze": "source-map-explorer 'dist/assets/*.js' --no-border-checks", + "dev": "vite", + "dev:local": "vite --mode localhost", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@bigmi/react": "0.7.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@lifi/sdk": "^4.0.0-alpha.13", + "@lifi/wallet-management": "workspace:*", + "@lifi/widget-light": "workspace:*", + "@lifi/widget-provider": "workspace:*", + "@lifi/widget-provider-bitcoin": "workspace:*", + "@lifi/widget-provider-ethereum": "workspace:*", + "@lifi/widget-provider-solana": "workspace:*", + "@lifi/widget-provider-sui": "workspace:*", + "@mui/material": "^7.3.6", + "@mysten/dapp-kit": "^0.19.11", + "@tanstack/react-query": "^5.90.20", + "@wagmi/connectors": "^7.1.6", + "bs58": ">=4.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "viem": "^2.45.1", + "wagmi": "^3.4.2" + }, + "devDependencies": { + "@types/node": "^25.2.1", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.3", + "typescript": "^5.9.3", + "vite": "^7.3.0", + "vite-plugin-node-polyfills": "^0.25.0" + } +} diff --git a/examples/vite-iframe/src/App.tsx b/examples/vite-iframe/src/App.tsx new file mode 100644 index 000000000..be6582ce3 --- /dev/null +++ b/examples/vite-iframe/src/App.tsx @@ -0,0 +1,71 @@ +import { useAccount } from '@lifi/wallet-management' +import { LiFiWidgetLight } from '@lifi/widget-light' +import { useBitcoinIframeHandler } from '@lifi/widget-light/bitcoin' +import { useEthereumIframeHandler } from '@lifi/widget-light/ethereum' +import { useSolanaIframeHandler } from '@lifi/widget-light/solana' +import { useSuiIframeHandler } from '@lifi/widget-light/sui' +import { + useSolanaWalletStandard, + useWalletAccount, +} from '@lifi/widget-provider-solana' +import { Box, Typography } from '@mui/material' +import { useMemo } from 'react' +import { WalletHeader } from './components/WalletHeader' +import { widgetConfig } from './widgetConfig' + +const WIDGET_URL = import.meta.env.VITE_WIDGET_URL || 'https://widget.li.fi' +const WIDGET_ORIGIN = new URL(WIDGET_URL).origin + +export function HostApp() { + const { account } = useAccount() + + const ethHandler = useEthereumIframeHandler() + + const { selectedWallet, connected } = useSolanaWalletStandard() + const { address: solAddress } = useWalletAccount() + const solHandler = useSolanaIframeHandler({ + address: solAddress, + connected, + wallet: selectedWallet, + }) + + const btcHandler = useBitcoinIframeHandler() + const suiHandler = useSuiIframeHandler() + const handlers = useMemo( + () => [ethHandler, solHandler, btcHandler, suiHandler], + [ethHandler, solHandler, btcHandler, suiHandler] + ) + + return ( + + + + + {!account.isConnected && ( + + Connect your wallet above — all widget transactions will be signed + through your connected wallet. + + )} + + + + + ) +} diff --git a/examples/vite-iframe/src/components/WalletHeader.tsx b/examples/vite-iframe/src/components/WalletHeader.tsx new file mode 100644 index 000000000..a57792dcc --- /dev/null +++ b/examples/vite-iframe/src/components/WalletHeader.tsx @@ -0,0 +1,79 @@ +import { + getConnectorIcon, + useAccount, + useAccountDisconnect, + useWalletMenu, +} from '@lifi/wallet-management' +import type { Account } from '@lifi/widget-provider' +import { Avatar, Box, Button, Chip, Typography } from '@mui/material' + +function shortenAddress(address?: string) { + if (!address) { + return '' + } + return `${address.slice(0, 6)}…${address.slice(-4)}` +} + +export function WalletHeader() { + const { account, accounts } = useAccount() + const { openWalletMenu } = useWalletMenu() + + return ( + + + Widget Light — Host + + + + {accounts.map((acc) => ( + + ))} + + + + ) +} + +function ConnectedAccount({ account }: { account: Account }) { + const disconnect = useAccountDisconnect() + + return ( + + } + label={ + + + {shortenAddress(account.address)} + + + ({account.chainType}) + + + } + onDelete={() => disconnect(account)} + variant="outlined" + size="medium" + /> + ) +} diff --git a/examples/vite-iframe/src/hooks/useChains.ts b/examples/vite-iframe/src/hooks/useChains.ts new file mode 100644 index 000000000..fa504c827 --- /dev/null +++ b/examples/vite-iframe/src/hooks/useChains.ts @@ -0,0 +1,46 @@ +import { + ChainType, + createClient, + type ExtendedChain, + getChains, +} from '@lifi/sdk' +import { convertExtendedChain } from '@lifi/widget-provider-ethereum' +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' + +const client = createClient({ integrator: 'vite-iframe-example' }) + +const chainTypes = [ChainType.EVM, ChainType.SVM, ChainType.UTXO, ChainType.MVM] + +/** + * Fetches all chains from the LI.FI API via the SDK. + * + * Returns both the raw `ExtendedChain[]` (for ecosystem providers) and + * viem `Chain[]` derived from the EVM subset (for wagmi). + */ +export function useChains() { + const { data, isLoading } = useQuery({ + queryKey: ['lifi-chains'], + queryFn: async () => { + const chains = await getChains(client, { chainTypes }) + client.setChains(chains) + return chains + }, + refetchInterval: 300_000, + staleTime: 300_000, + }) + + const evmChains = useMemo( + () => + data + ?.filter((c: ExtendedChain) => c.chainType === 'EVM') + .map(convertExtendedChain), + [data] + ) + + return { + chains: data, + evmChains, + isLoading, + } +} diff --git a/examples/vite-iframe/src/main.tsx b/examples/vite-iframe/src/main.tsx new file mode 100644 index 000000000..88ee662b6 --- /dev/null +++ b/examples/vite-iframe/src/main.tsx @@ -0,0 +1,24 @@ +import { ThemeProvider } from '@mui/material' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import ReactDOM from 'react-dom/client' +import { HostApp } from './App' +import { EcosystemProviders } from './providers/EcosystemProviders' +import { HostWalletProvider } from './providers/HostWalletProvider' +import { theme } from './theme' + +const queryClient = new QueryClient() + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + +) diff --git a/examples/vite-iframe/src/providers/EcosystemProviders.tsx b/examples/vite-iframe/src/providers/EcosystemProviders.tsx new file mode 100644 index 000000000..b17fbedba --- /dev/null +++ b/examples/vite-iframe/src/providers/EcosystemProviders.tsx @@ -0,0 +1,28 @@ +import { WalletManagementProvider } from '@lifi/wallet-management' +import { BitcoinProvider } from '@lifi/widget-provider-bitcoin' +import { EthereumProvider } from '@lifi/widget-provider-ethereum' +import { SolanaProvider } from '@lifi/widget-provider-solana' +import { SuiProvider } from '@lifi/widget-provider-sui' +import type { FC, PropsWithChildren } from 'react' +import { useChains } from '../hooks/useChains' + +const EthereumProviderInner = EthereumProvider() +const SolanaProviderInner = SolanaProvider() +const BitcoinProviderInner = BitcoinProvider() +const SuiProviderInner = SuiProvider() + +export const EcosystemProviders: FC = ({ children }) => { + const { chains } = useChains() + + return ( + + + + + {children} + + + + + ) +} diff --git a/examples/vite-iframe/src/providers/HostWalletProvider.tsx b/examples/vite-iframe/src/providers/HostWalletProvider.tsx new file mode 100644 index 000000000..445135439 --- /dev/null +++ b/examples/vite-iframe/src/providers/HostWalletProvider.tsx @@ -0,0 +1,67 @@ +import { injected, walletConnect } from '@wagmi/connectors' +import { type FC, type PropsWithChildren, useEffect, useRef } from 'react' +import { createClient, http } from 'viem' +import { mainnet } from 'viem/chains' +import type { Config } from 'wagmi' +import { createConfig, WagmiProvider } from 'wagmi' +import { reconnect } from 'wagmi/actions' +import { useChains } from '../hooks/useChains' + +const projectId = import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID as + | string + | undefined + +const connectors = [ + injected(), + ...(projectId ? [walletConnect({ projectId })] : []), +] + +/** + * Host-side WalletProvider. + * + * Uses standard injected + WalletConnect connectors — the parent window owns + * the real wallet. EVM chains are fetched directly from the LI.FI API so that + * wallet_switchEthereumChain requests forwarded by the guest can succeed. + */ +export const HostWalletProvider: FC = ({ children }) => { + const { evmChains } = useChains() + const wagmi = useRef(null) + + if (!wagmi.current) { + wagmi.current = createConfig({ + chains: [mainnet], + client({ chain }) { + return createClient({ chain, transport: http() }) + }, + multiInjectedProviderDiscovery: true, + ssr: false, + }) + } + + useEffect(() => { + if (!evmChains?.length || !wagmi.current) { + return + } + const typed = evmChains as unknown as readonly [ + (typeof evmChains)[0], + ...(typeof evmChains)[number][], + ] + wagmi.current._internal.chains.setState(typed) + wagmi.current._internal.connectors.setState(() => + [ + ...connectors, + ...(wagmi + .current!._internal.mipd?.getProviders() + .map(wagmi.current!._internal.connectors.providerDetailToConnector) ?? + []), + ].map(wagmi.current!._internal.connectors.setup) + ) + reconnect(wagmi.current) + }, [evmChains]) + + return ( + + {children} + + ) +} diff --git a/examples/vite-iframe/src/theme.ts b/examples/vite-iframe/src/theme.ts new file mode 100644 index 000000000..5f59865aa --- /dev/null +++ b/examples/vite-iframe/src/theme.ts @@ -0,0 +1,157 @@ +import { alpha, createTheme, darken, lighten } from '@mui/material' + +const primaryMain = '#5C67FF' +const secondaryMain = '#F7C2FF' + +export const theme = createTheme({ + cssVariables: true, + colorSchemes: { + light: { + palette: { + primary: { + main: primaryMain, + light: lighten(primaryMain, 0.84), + dark: darken(primaryMain, 0.2), + }, + secondary: { + main: secondaryMain, + light: lighten(secondaryMain, 0.84), + dark: darken(secondaryMain, 0.2), + }, + background: { + paper: '#ffffff', + default: '#ffffff', + }, + text: { + primary: '#000000', + secondary: '#747474', + }, + success: { main: '#0AA65B' }, + warning: { main: '#FFCC00' }, + error: { main: '#E5452F' }, + info: { main: '#297EFF' }, + grey: { + 200: '#eeeeee', + 300: '#e0e0e0', + 700: '#616161', + 800: '#424242', + }, + }, + }, + dark: { + palette: { + primary: { + main: primaryMain, + light: lighten(primaryMain, 0.84), + dark: darken(primaryMain, 0.2), + }, + secondary: { + main: secondaryMain, + light: lighten(secondaryMain, 0.84), + dark: darken(secondaryMain, 0.2), + }, + background: { + paper: '#212121', + default: '#121212', + }, + text: { + primary: '#ffffff', + secondary: '#bbbbbb', + }, + success: { main: '#0AA65B' }, + warning: { main: '#FFCC00' }, + error: { main: '#E5452F' }, + info: { main: '#297EFF' }, + grey: { + 200: '#eeeeee', + 300: '#e0e0e0', + 700: '#616161', + 800: '#424242', + }, + }, + }, + }, + shape: { + borderRadius: 12, + }, + typography: { + fontFamily: 'Inter, sans-serif', + }, + components: { + MuiCard: { + defaultProps: { + variant: 'outlined', + }, + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: theme.vars.shape.borderRadius, + backgroundImage: 'none', + transition: theme.transitions.create(['background-color', 'filter'], { + duration: theme.transitions.duration.enteringScreen, + easing: theme.transitions.easing.easeOut, + }), + cursor: 'pointer', + '&:hover': { + backgroundColor: `color-mix(in srgb, ${theme.vars.palette.background.paper} 98%, black)`, + ...theme.applyStyles('dark', { + backgroundColor: `color-mix(in srgb, ${theme.vars.palette.background.paper} 98%, white)`, + }), + }, + }), + }, + }, + MuiButton: { + defaultProps: { + disableElevation: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: theme.vars.shape.borderRadius, + textTransform: 'none' as const, + fontWeight: 600, + }), + text: ({ theme }) => ({ + backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.08)`, + color: theme.vars.palette.primary.main, + '&:hover': { + backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.12)`, + }, + ...theme.applyStyles('dark', { + backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.42)`, + color: theme.palette.getContrastText( + alpha(theme.palette.primary.main, 0.08) + ), + '&:hover': { + backgroundColor: `rgba(${theme.vars.palette.primary.mainChannel} / 0.56)`, + }, + }), + }), + }, + }, + MuiAvatar: { + styleOverrides: { + root: { + height: 40, + width: 40, + }, + img: { + objectFit: 'contain' as const, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiDrawer: { + styleOverrides: { + paper: { + backgroundImage: 'none', + }, + }, + }, + }, +}) diff --git a/examples/vite-iframe/src/vite-env.d.ts b/examples/vite-iframe/src/vite-env.d.ts new file mode 100644 index 000000000..10c8089ad --- /dev/null +++ b/examples/vite-iframe/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +declare global { + interface ImportMetaEnv { + readonly VITE_WIDGET_URL: string + readonly VITE_WALLET_CONNECT_PROJECT_ID?: string + } + + interface ImportMeta { + readonly env: ImportMetaEnv + } +} + +export {} diff --git a/examples/vite-iframe/src/widgetConfig.ts b/examples/vite-iframe/src/widgetConfig.ts new file mode 100644 index 000000000..e1fb14e18 --- /dev/null +++ b/examples/vite-iframe/src/widgetConfig.ts @@ -0,0 +1,30 @@ +import { ChainId } from '@lifi/sdk' +import type { WidgetLightConfig } from '@lifi/widget-light' + +/** + * Widget configuration sent to the iframe. + * Must be JSON-serialisable — no React nodes or callback functions. + */ +export const widgetConfig: WidgetLightConfig = { + integrator: 'vite-iframe-example', + variant: 'wide', + theme: { + container: { + border: '1px solid rgb(234, 234, 234)', + borderRadius: '16px', + height: 'fit-content', + }, + }, + sdkConfig: { + rpcUrls: { + [ChainId.SOL]: [ + 'https://wild-winter-frog.solana-mainnet.quiknode.pro/2370a45ff891f6dc9e5b1753460290fe0f1ef103/', + 'https://dacey-pp61jd-fast-mainnet.helius-rpc.com/', + ], + }, + routeOptions: { + maxPriceImpact: 0.4, + jitoBundle: true, + }, + }, +} diff --git a/examples/vite-iframe/tsconfig.json b/examples/vite-iframe/tsconfig.json new file mode 100644 index 000000000..bb0095e8e --- /dev/null +++ b/examples/vite-iframe/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "types": ["vite/client"], + "useDefineForClassFields": true + }, + "include": ["src", "vite.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/vite-iframe/tsconfig.node.json b/examples/vite-iframe/tsconfig.node.json new file mode 100644 index 000000000..16dfedc6a --- /dev/null +++ b/examples/vite-iframe/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vite-iframe/vite.config.ts b/examples/vite-iframe/vite.config.ts new file mode 100644 index 000000000..28ce9a2df --- /dev/null +++ b/examples/vite-iframe/vite.config.ts @@ -0,0 +1,16 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import { nodePolyfills } from 'vite-plugin-node-polyfills' + +// Host-only Vite app. +// The guest (iframe) page is served from VITE_WIDGET_URL (default: https://widget.li.fi). +export default defineConfig({ + plugins: [nodePolyfills(), react()], + esbuild: { + target: 'esnext', + }, + server: { + port: 4000, + open: true, + }, +}) diff --git a/knip.json b/knip.json index 95b28b3a9..d51617671 100644 --- a/knip.json +++ b/knip.json @@ -2,6 +2,17 @@ "ignore": ["examples/**", "packages/widget-embedded/**", "scripts/**"], "ignoreDependencies": ["@mui/system", "csstype"], "workspaces": { + "packages/widget-light": { + "ignore": ["src/config/version.ts"], + "ignoreDependencies": [ + "wagmi", + "viem", + "@bigmi/client", + "@bigmi/react", + "@mysten/dapp-kit", + "@wallet-standard/base" + ] + }, "packages/widget-playground": { "ignoreDependencies": [ "@base-org/account", diff --git a/package.json b/package.json index 4ff5ee948..ae311babf 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "3.38.0", + "version": "4.0.0-alpha.2", "private": true, "sideEffects": false, "type": "module", @@ -91,5 +91,5 @@ "zod": ">=4.1.11" } }, - "packageManager": "pnpm@10.30.3" + "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017" } diff --git a/packages/wallet-management/package.json b/packages/wallet-management/package.json index c4fe9ace3..9cbbaf389 100644 --- a/packages/wallet-management/package.json +++ b/packages/wallet-management/package.json @@ -1,6 +1,6 @@ { "name": "@lifi/wallet-management", - "version": "4.0.0-alpha.0", + "version": "4.0.0-alpha.2", "description": "LI.FI Wallet Management solution.", "type": "module", "main": "./src/index.ts", @@ -10,8 +10,8 @@ "watch": "tsc -w -p ./tsconfig.json", "build": "pnpm clean && pnpm build:esm && pnpm build:clean", "build:esm": "tsc --build", - "build:prerelease": "node ../../scripts/prerelease.js && cpy '../../*.md' .", - "build:postrelease": "node ../../scripts/postrelease.js && rm -rf *.md", + "build:prerelease": "node ../../scripts/prerelease.js && cpy '../../README.md' .", + "build:postrelease": "node ../../scripts/postrelease.js && rm -rf README.md", "build:clean": "rm -rf tsconfig.tsbuildinfo ./dist/tsconfig.tsbuildinfo", "release:build": "pnpm build", "clean": "pnpm build:clean && rm -rf dist", @@ -67,11 +67,5 @@ "@tanstack/react-query": ">=5.90.0", "react": ">=18", "react-dom": ">=18" - }, - "files": [ - "dist/**", - "src/**", - "!tsconfig.json", - "!*.tmp" - ] + } } diff --git a/packages/widget-embedded/.env b/packages/widget-embedded/.env index 15cb40c49..285f81c83 100644 --- a/packages/widget-embedded/.env +++ b/packages/widget-embedded/.env @@ -1,3 +1,2 @@ -VITE_OPENSEA_API_KEY=ee7460014fda4f58804f25c29a27df35 VITE_WALLET_CONNECT=5432e3507d41270bee46b7b85bbc2ef8 diff --git a/packages/widget-embedded/index.html b/packages/widget-embedded/index.html index 043627a50..7a10a8c33 100644 --- a/packages/widget-embedded/index.html +++ b/packages/widget-embedded/index.html @@ -1,4 +1,4 @@ - + @@ -6,6 +6,12 @@ + + +