From 513851a1cb2de693edde3f94e5bbfef1fb27c97e Mon Sep 17 00:00:00 2001 From: mfbz Date: Tue, 19 Aug 2025 16:26:21 +0200 Subject: [PATCH 01/11] Implemented cross-vm-receive-nft hook and updated demo --- packages/demo/src/app.css | 63 +++++ packages/demo/src/app.tsx | 10 + .../cards/cross-vm-receive-nft-card.tsx | 147 ++++++++++++ .../cards/cross-vm-spend-nft-card.tsx | 151 ++++++++++++ packages/demo/src/components/container.tsx | 8 + packages/demo/src/components/navbar.tsx | 7 +- packages/demo/src/components/sidebar.tsx | 6 + packages/demo/src/constants.ts | 6 + packages/react-sdk/src/hooks/index.ts | 1 + .../src/hooks/useCrossVmReceiveNft.test.ts | 167 ++++++++++++++ .../src/hooks/useCrossVmReceiveNft.ts | 215 ++++++++++++++++++ 11 files changed, 779 insertions(+), 2 deletions(-) create mode 100644 packages/demo/src/components/cards/cross-vm-receive-nft-card.tsx create mode 100644 packages/demo/src/components/cards/cross-vm-spend-nft-card.tsx create mode 100644 packages/react-sdk/src/hooks/useCrossVmReceiveNft.test.ts create mode 100644 packages/react-sdk/src/hooks/useCrossVmReceiveNft.ts diff --git a/packages/demo/src/app.css b/packages/demo/src/app.css index 9337aa2a2..d5e3bf9bd 100644 --- a/packages/demo/src/app.css +++ b/packages/demo/src/app.css @@ -25,3 +25,66 @@ body { height: 100vh; background-color: #ffffff; } + +/* Custom scrollbar styles - Light mode (default) */ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: #ffffff; + border-radius: 8px; +} + +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 8px; + border: 2px solid #ffffff; + transition: background-color 0.2s ease-in-out; +} + +::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +::-webkit-scrollbar-thumb:active { + background: #6b7280; +} + +::-webkit-scrollbar-corner { + background: #ffffff; +} + +/* Firefox scrollbar styles - Light mode (default) */ +html { + scrollbar-width: thin; + scrollbar-color: #d1d5db #ffffff; +} + +/* Class-based dark mode support (theme toggle) */ +html.dark ::-webkit-scrollbar-track { + background: #374151 !important; +} + +html.dark ::-webkit-scrollbar-thumb { + background: #6b7280 !important; + border: 2px solid #374151 !important; +} + +html.dark ::-webkit-scrollbar-thumb:hover { + background: #9ca3af !important; +} + +html.dark ::-webkit-scrollbar-thumb:active { + background: #d1d5db !important; +} + +html.dark ::-webkit-scrollbar-corner { + background: #374151 !important; +} + +/* Firefox class-based dark mode (theme toggle) */ +html.dark { + scrollbar-color: #6b7280 #374151 !important; +} diff --git a/packages/demo/src/app.tsx b/packages/demo/src/app.tsx index 7efa037f4..3551c5520 100644 --- a/packages/demo/src/app.tsx +++ b/packages/demo/src/app.tsx @@ -5,10 +5,20 @@ import FlowProviderWrapper, { } from "./components/flow-provider-wrapper" import {Navbar} from "./components/navbar" import {Sidebar} from "./components/sidebar" +import {useEffect} from "react" function AppContent() { const {darkMode} = useDarkMode() + // Apply dark class to document element for scrollbar styling + useEffect(() => { + if (darkMode) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [darkMode]) + return (
{ + if (currentNetwork !== "testnet") return null + + const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork) + return { + name: "Example NFT", + nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`, + nftId: "1", + } + }, [currentNetwork]) + + // Set default NFT identifier when network changes + useMemo(() => { + if (exampleNftData && !nftIdentifier) { + setNftIdentifier(exampleNftData.nftIdentifier) + } + }, [exampleNftData, nftIdentifier]) + + const handleReceiveNft = () => { + receiveNft({ + nftIdentifier, + nftId, + }) + } + + if (!isNetworkSupported) { + return ( +
+

+ useCrossVmReceiveNft +

+
+

+ Network not supported: This feature is only + available on testnet. +

+
+
+ ) + } + + return ( +
+

+ useCrossVmReceiveNft +

+
+ + + setNftIdentifier(e.target.value)} + placeholder={ + exampleNftData + ? exampleNftData.nftIdentifier + : "e.g., A.dfc20aee650fcbdf.ExampleNFT.NFT" + } + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + + setNftId(e.target.value)} + placeholder="e.g., 1" + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + +
+ +
+

Transaction Status:

+ + {isPending && ( +

Receiving NFT from EVM...

+ )} + + {error && ( +
+ Error: {error.message} +
+ )} + + {transactionId && !isPending && !error && ( +
+

+ NFT received successfully! +

+

+ Transaction ID: {transactionId} +

+
+ )} + + {!transactionId && !isPending && !error && ( +
+

+ Click "Receive NFT" to bridge an NFT from EVM to Cadence +

+
+ )} +
+
+ ) +} diff --git a/packages/demo/src/components/cards/cross-vm-spend-nft-card.tsx b/packages/demo/src/components/cards/cross-vm-spend-nft-card.tsx new file mode 100644 index 000000000..41c630522 --- /dev/null +++ b/packages/demo/src/components/cards/cross-vm-spend-nft-card.tsx @@ -0,0 +1,151 @@ +import {useCrossVmSpendNft, useFlowConfig} from "@onflow/react-sdk" +import {useState, useMemo} from "react" +import {getContractAddress} from "../../constants" + +export function CrossVmSpendNftCard() { + const config = useFlowConfig() + const currentNetwork = config.flowNetwork || "emulator" + const [nftIdentifier, setNftIdentifier] = useState("") + const [nftIds, setNftIds] = useState("1") // NFT IDs to bridge (comma-separated) + + const { + spendNft, + isPending, + data: transactionId, + error, + } = useCrossVmSpendNft() + + const isNetworkSupported = currentNetwork === "testnet" + + const exampleNftData = useMemo(() => { + if (currentNetwork !== "testnet") return null + + const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork) + return { + name: "Example NFT", + nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`, + nftIds: "1", + } + }, [currentNetwork]) + + // Set default NFT identifier when network changes + useMemo(() => { + if (exampleNftData && !nftIdentifier) { + setNftIdentifier(exampleNftData.nftIdentifier) + setNftIds(exampleNftData.nftIds) + } + }, [exampleNftData, nftIdentifier]) + + const handleSpendNft = () => { + const nftIdArray = nftIds.split(",").map(id => id.trim()) + + spendNft({ + nftIdentifier, + nftIds: nftIdArray, + calls: [], // No EVM calls, just bridging + }) + } + + if (!isNetworkSupported) { + return ( +
+

+ useCrossVmSpendNft +

+
+

+ Network not supported: This feature is only + available on testnet. +

+
+
+ ) + } + + return ( +
+

+ useCrossVmSpendNft +

+
+ + + + setNftIdentifier(e.target.value)} + placeholder={ + exampleNftData + ? exampleNftData.nftIdentifier + : "e.g., A.012e4d204a60ac6f.ExampleNFT.NFT" + } + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + + setNftIds(e.target.value)} + placeholder="e.g., 1,2,3" + className="p-3 border-2 border-[#00EF8B] rounded-md text-sm text-black bg-white + outline-none transition-colors duration-200 ease-in-out w-full mb-4 font-mono" + /> + + +
+ +
+

Transaction Status:

+ + {isPending && ( +

Bridging NFTs to EVM...

+ )} + + {error && ( +
+ Error: {error.message} +
+ )} + + {transactionId && !isPending && !error && ( +
+

+ NFTs spent successfully! +

+

+ Transaction ID: {transactionId} +

+
+ )} + + {!transactionId && !isPending && !error && ( +
+

+ Click "Spend NFT" to bridge NFTs from Cadence to EVM +

+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/packages/demo/src/components/container.tsx b/packages/demo/src/components/container.tsx index 57930e7b8..2c84b0a35 100644 --- a/packages/demo/src/components/container.tsx +++ b/packages/demo/src/components/container.tsx @@ -9,6 +9,8 @@ import {FlowQueryCard} from "./cards/flow-query-card" import {FlowQueryRawCard} from "./cards/flow-query-raw-card" import {FlowRevertibleRandomCard} from "./cards/flow-revertible-random-card" import {FlowTransactionStatusCard} from "./cards/flow-transaction-status-card" +import {CrossVmReceiveNftCard} from "./cards/cross-vm-receive-nft-card" +import {CrossVmSpendNftCard} from "./cards/cross-vm-spend-nft-card" import {KitConnectCard} from "./kits/kit-connect-card" import {KitTransactionButtonCard} from "./kits/kit-transaction-button-card" import {KitTransactionDialogCard} from "./kits/kit-transaction-dialog-card" @@ -57,6 +59,12 @@ export function Container() {
+
+ +
+
+ +
diff --git a/packages/demo/src/components/navbar.tsx b/packages/demo/src/components/navbar.tsx index bddb895c6..927ffe726 100644 --- a/packages/demo/src/components/navbar.tsx +++ b/packages/demo/src/components/navbar.tsx @@ -10,8 +10,11 @@ export function Navbar() { return (