From e124a2747933947450726665b5e797d17673c7df Mon Sep 17 00:00:00 2001 From: Van Minh Date: Thu, 4 Sep 2025 18:46:58 +0700 Subject: [PATCH] feat: add transaction feedback toast component with simulation and documentation --- components/txn-feedback-toast-preview.tsx | 112 +++++++++++ .../Txn-Feedback/txn-feedback-toast.tsx | 185 ++++++++++++++++++ components/ui/murphy/index.tsx | 6 +- .../Txn-Feedback/txn-feedback-toast.mdx | 145 ++++++++++++++ content/docs/onchainkit/meta.json | 3 +- public/r/txn-feedback-toast.json | 20 ++ registry.json | 18 ++ registry/components/txn-feedback-toast.json | 14 ++ types/transaction/index.tsx | 27 +++ 9 files changed, 527 insertions(+), 3 deletions(-) create mode 100644 components/txn-feedback-toast-preview.tsx create mode 100644 components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx create mode 100644 content/docs/onchainkit/Txn-Feedback/txn-feedback-toast.mdx create mode 100644 public/r/txn-feedback-toast.json create mode 100644 registry/components/txn-feedback-toast.json create mode 100644 types/transaction/index.tsx diff --git a/components/txn-feedback-toast-preview.tsx b/components/txn-feedback-toast-preview.tsx new file mode 100644 index 0000000..d0d8db7 --- /dev/null +++ b/components/txn-feedback-toast-preview.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import type { TransactionStatus } from "@/types/transaction"; +import { TxnFeedbackToast } from "@/components/ui/murphy"; + +export default function TxnFeedbackToastPreview() { + const [txStatus, setTxStatus] = useState({ + status: "idle", + }); + + const showToast = ( + status: TransactionStatus["status"], + error?: string, + signature?: string + ) => { + setTxStatus({ status, error, signature }); + }; + + const simulateTransaction = async () => { + const statuses: TransactionStatus["status"][] = [ + "preparing", + "signing", + "sending", + "confirming", + ]; + + for (const status of statuses) { + setTxStatus({ status }); + await new Promise((resolve) => setTimeout(resolve, 1500)); + } + + if (Math.random() > 0.3) { + setTxStatus({ + status: "success", + signature: + "5VfYmGC9L8ty3D4HutfxndoKXGBwXJWKKvxgF7qQzqK8xMjU9v7Rw2sP3nT6hL4jK9mN8bC1dF2eG3hI5jK6lM7n", + }); + } else { + setTxStatus({ + status: "error", + error: "Transaction failed: Insufficient funds for transaction fees", + }); + } + }; + + return ( +
+ {/* Controls */} +
+
+ {["preparing", "signing", "sending", "confirming"].map((status) => ( + + ))} +
+ +
+ + +
+ + + + +
+ + {/* Toast */} + setTxStatus({ status: "idle" })} + /> +
+ ); +} diff --git a/components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx b/components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx new file mode 100644 index 0000000..a06a31f --- /dev/null +++ b/components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + X, + CheckCircle, + AlertCircle, + Loader2, + Send, + Clock, + FileSignature, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { TransactionStatus } from "@/types/transaction"; + +interface TxnFeedbackToastProps { + status: TransactionStatus; + onRetry?: () => void; + onClose: () => void; +} + +export function TxnFeedbackToast({ + status, + onRetry, + onClose, +}: TxnFeedbackToastProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (status.status !== "idle") { + setIsVisible(true); + } else { + setIsVisible(false); + } + }, [status.status]); + + if (!isVisible || status.status === "idle") { + return null; + } + + const getStatusConfig = () => { + switch (status.status) { + case "preparing": + return { + icon: , + title: "Preparing Transaction", + description: "Setting up your transaction...", + bgColor: "bg-blue-50 dark:bg-blue-950", + borderColor: "border-blue-200 dark:border-blue-800", + textColor: "text-blue-900 dark:text-blue-100", + }; + case "signing": + return { + icon: , + title: "Signing Transaction", + description: "Please sign the transaction in your wallet...", + bgColor: "bg-yellow-50 dark:bg-yellow-950", + borderColor: "border-yellow-200 dark:border-yellow-800", + textColor: "text-yellow-900 dark:text-yellow-100", + }; + case "sending": + return { + icon: , + title: "Sending Transaction", + description: "Broadcasting to the network...", + bgColor: "bg-purple-50 dark:bg-purple-950", + borderColor: "border-purple-200 dark:border-purple-800", + textColor: "text-purple-900 dark:text-purple-100", + }; + case "confirming": + return { + icon: , + title: "Confirming Transaction", + description: "Waiting for network confirmation...", + bgColor: "bg-orange-50 dark:bg-orange-950", + borderColor: "border-orange-200 dark:border-orange-800", + textColor: "text-orange-900 dark:text-orange-100", + }; + case "success": + return { + icon: ( + + ), + title: "Transaction Successful", + description: status.signature + ? `Signature: ${status.signature.slice( + 0, + 8 + )}...${status.signature.slice(-8)}` + : "Your transaction has been completed successfully.", + bgColor: "bg-green-50 dark:bg-green-950", + borderColor: "border-green-200 dark:border-green-800", + textColor: "text-green-900 dark:text-green-100", + }; + case "error": + return { + icon: , + title: "Transaction Failed", + description: + status.error || + "An error occurred while processing your transaction.", + bgColor: "bg-red-50 dark:bg-red-950", + borderColor: "border-red-200 dark:border-red-800", + textColor: "text-red-900 dark:text-red-100", + }; + default: + return null; + } + }; + + const config = getStatusConfig(); + if (!config) return null; + + return ( +
+
+
+ {/* Icon container with proper alignment */} +
+ {config.icon} +
+ + {/* Content container */} +
+
+ {config.title} +
+
+ {config.description} +
+ + {/* Action buttons */} + {status.status === "error" && onRetry && ( +
+ +
+ )} + + {status.status === "success" && status.signature && ( +
+ +
+ )} +
+ + {/* Close button with proper alignment */} +
+ +
+
+
+
+ ); +} diff --git a/components/ui/murphy/index.tsx b/components/ui/murphy/index.tsx index 987b191..bc79e79 100644 --- a/components/ui/murphy/index.tsx +++ b/components/ui/murphy/index.tsx @@ -14,7 +14,7 @@ import CandyMachineForm from "./candy-machine-form"; import CoreCandyMachineForm from "./core-candy-machine-form"; import BubblegumLegacyForm from "./bubblegum-legacy-form"; import ImprovedCNFTManager from "./improved-cnft-manager"; -import CompressedNFTViewer from "./compressed-nft-viewer" +import CompressedNFTViewer from "./compressed-nft-viewer"; import { CreateMerkleTree } from "./create-merkleTree-form"; import { TokenList } from "./token-list"; import { StakeForm } from "./stake-token-form"; @@ -37,6 +37,7 @@ import { CoreAssetLaunchpad } from "./core-asset-launchpad"; import { HydraFanoutForm } from "./hydra-fanout-form"; import { MPLHybridForm } from "./mpl-hybrid-form"; import { TokenMetadataViewer } from "./token-metadata-viewer"; +import { TxnFeedbackToast } from "./Txn-Feedback/txn-feedback-toast"; export { ConnectWalletButton, @@ -78,5 +79,6 @@ export { TMLaunchpadForm, HydraFanoutForm, MPLHybridForm, - TokenMetadataViewer + TokenMetadataViewer, + TxnFeedbackToast, }; diff --git a/content/docs/onchainkit/Txn-Feedback/txn-feedback-toast.mdx b/content/docs/onchainkit/Txn-Feedback/txn-feedback-toast.mdx new file mode 100644 index 0000000..cdcd48c --- /dev/null +++ b/content/docs/onchainkit/Txn-Feedback/txn-feedback-toast.mdx @@ -0,0 +1,145 @@ +--- +title: Transaction Feedback Toast +description: Real-time toast notifications for transaction status updates +icon: Bell +--- + +import TxnFeedbackToastPreview from "@/components/txn-feedback-toast-preview"; + + +
+ +

+ Click buttons above to see toast notifications in the top-right corner +

+
+
+ +## Installation + + + + + Install Transaction Feedback Toast + + + + + +## Basic Usage + +```tsx +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { TxnFeedbackToast } from "@/components/ui/murphy/Txn-Feedback/txn-feedback-toast"; +import type { TransactionStatus } from "@/types/transaction"; + +export default function MyPage() { + const [txnStatus, setTxnStatus] = useState({ + status: "idle", + }); + + const simulateTransaction = async () => { + const statuses: TransactionStatus["status"][] = [ + "preparing", + "signing", + "sending", + "confirming", + ]; + + for (const status of statuses) { + setTxnStatus({ status }); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + // Simulate success or error + if (Math.random() > 0.3) { + setTxnStatus({ + status: "success", + signature: + "5VfYmGC9L8ty3D4HutfxndoKXGBwXJWKKvxgF7qQzqK8xMjU9v7Rw2sP3nT6hL4jK9mN8bC1dF2eG3hI5jK6lM7n", + }); + } else { + setTxnStatus({ + status: "error", + error: "Transaction failed: Insufficient funds for transaction fees", + }); + } + }; + + return ( +
+

Transaction Demo

+ + + + simulateTransaction()} + onClose={() => setTxnStatus({ status: "idle" })} + /> +
+ ); +} +``` + +## Props + + void", + default: "undefined", + }, + onClose: { + description: "Callback function when toast is closed", + type: "() => void", + default: "undefined", + }, + }} +/> + +### TransactionStatus Interface + + diff --git a/content/docs/onchainkit/meta.json b/content/docs/onchainkit/meta.json index d2b1c3b..329e6c2 100644 --- a/content/docs/onchainkit/meta.json +++ b/content/docs/onchainkit/meta.json @@ -31,6 +31,7 @@ "Metaplex", "Meteora-DBC", "ZK-Compression", - "Jupiter-Recurring" + "Jupiter-Recurring", + "Txn-Feedback" ] } diff --git a/public/r/txn-feedback-toast.json b/public/r/txn-feedback-toast.json new file mode 100644 index 0000000..bd8bae5 --- /dev/null +++ b/public/r/txn-feedback-toast.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "txn-feedback-toast", + "type": "registry:block", + "title": "Toast component to display transaction status (loading, success, error).", + "dependencies": [ + "sonner" + ], + "registryDependencies": [ + "toast" + ], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx", + "content": "\"use client\";\r\n\r\nimport { useEffect, useState } from \"react\";\r\nimport {\r\n X,\r\n CheckCircle,\r\n AlertCircle,\r\n Loader2,\r\n Send,\r\n Clock,\r\n FileSignature,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { TransactionStatus } from \"@/types/transaction\";\r\n\r\ninterface TxnFeedbackToastProps {\r\n status: TransactionStatus;\r\n onRetry?: () => void;\r\n onClose: () => void;\r\n}\r\n\r\nexport function TxnFeedbackToast({\r\n status,\r\n onRetry,\r\n onClose,\r\n}: TxnFeedbackToastProps) {\r\n const [isVisible, setIsVisible] = useState(false);\r\n\r\n useEffect(() => {\r\n if (status.status !== \"idle\") {\r\n setIsVisible(true);\r\n } else {\r\n setIsVisible(false);\r\n }\r\n }, [status.status]);\r\n\r\n if (!isVisible || status.status === \"idle\") {\r\n return null;\r\n }\r\n\r\n const getStatusConfig = () => {\r\n switch (status.status) {\r\n case \"preparing\":\r\n return {\r\n icon: ,\r\n title: \"Preparing Transaction\",\r\n description: \"Setting up your transaction...\",\r\n bgColor: \"bg-blue-50 dark:bg-blue-950\",\r\n borderColor: \"border-blue-200 dark:border-blue-800\",\r\n textColor: \"text-blue-900 dark:text-blue-100\",\r\n };\r\n case \"signing\":\r\n return {\r\n icon: ,\r\n title: \"Signing Transaction\",\r\n description: \"Please sign the transaction in your wallet...\",\r\n bgColor: \"bg-yellow-50 dark:bg-yellow-950\",\r\n borderColor: \"border-yellow-200 dark:border-yellow-800\",\r\n textColor: \"text-yellow-900 dark:text-yellow-100\",\r\n };\r\n case \"sending\":\r\n return {\r\n icon: ,\r\n title: \"Sending Transaction\",\r\n description: \"Broadcasting to the network...\",\r\n bgColor: \"bg-purple-50 dark:bg-purple-950\",\r\n borderColor: \"border-purple-200 dark:border-purple-800\",\r\n textColor: \"text-purple-900 dark:text-purple-100\",\r\n };\r\n case \"confirming\":\r\n return {\r\n icon: ,\r\n title: \"Confirming Transaction\",\r\n description: \"Waiting for network confirmation...\",\r\n bgColor: \"bg-orange-50 dark:bg-orange-950\",\r\n borderColor: \"border-orange-200 dark:border-orange-800\",\r\n textColor: \"text-orange-900 dark:text-orange-100\",\r\n };\r\n case \"success\":\r\n return {\r\n icon: (\r\n \r\n ),\r\n title: \"Transaction Successful\",\r\n description: status.signature\r\n ? `Signature: ${status.signature.slice(\r\n 0,\r\n 8\r\n )}...${status.signature.slice(-8)}`\r\n : \"Your transaction has been completed successfully.\",\r\n bgColor: \"bg-green-50 dark:bg-green-950\",\r\n borderColor: \"border-green-200 dark:border-green-800\",\r\n textColor: \"text-green-900 dark:text-green-100\",\r\n };\r\n case \"error\":\r\n return {\r\n icon: ,\r\n title: \"Transaction Failed\",\r\n description:\r\n status.error ||\r\n \"An error occurred while processing your transaction.\",\r\n bgColor: \"bg-red-50 dark:bg-red-950\",\r\n borderColor: \"border-red-200 dark:border-red-800\",\r\n textColor: \"text-red-900 dark:text-red-100\",\r\n };\r\n default:\r\n return null;\r\n }\r\n };\r\n\r\n const config = getStatusConfig();\r\n if (!config) return null;\r\n\r\n return (\r\n
\r\n \r\n
\r\n {/* Icon container with proper alignment */}\r\n
\r\n {config.icon}\r\n
\r\n\r\n {/* Content container */}\r\n
\r\n
\r\n {config.title}\r\n
\r\n \r\n {config.description}\r\n
\r\n\r\n {/* Action buttons */}\r\n {status.status === \"error\" && onRetry && (\r\n
\r\n \r\n Retry\r\n \r\n
\r\n )}\r\n\r\n {status.status === \"success\" && status.signature && (\r\n
\r\n {\r\n navigator.clipboard.writeText(status.signature!);\r\n }}\r\n size=\"sm\"\r\n variant=\"outline\"\r\n className=\"h-7 px-2 text-xs\"\r\n >\r\n Copy Signature\r\n \r\n
\r\n )}\r\n
\r\n\r\n {/* Close button with proper alignment */}\r\n
\r\n \r\n \r\n Close\r\n \r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index bac943b..f33e319 100644 --- a/registry.json +++ b/registry.json @@ -1386,6 +1386,24 @@ } ] }, + { + "name": "txn-feedback-toast", + "type": "registry:block", + "title": "Toast component to display transaction status (loading, success, error).", + "registryDependencies": [ + "toast" + ], + "dependencies": [ + "sonner" + ], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx" + } + ] + }, { "name": "txn-list", "type": "registry:block", diff --git a/registry/components/txn-feedback-toast.json b/registry/components/txn-feedback-toast.json new file mode 100644 index 0000000..f769f5d --- /dev/null +++ b/registry/components/txn-feedback-toast.json @@ -0,0 +1,14 @@ +{ + "name": "txn-feedback-toast", + "type": "registry:block", + "title": "Toast component to display transaction status (loading, success, error).", + "registryDependencies": ["toast"], + "dependencies": ["sonner"], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-feedback-toast.tsx" + } + ] +} diff --git a/types/transaction/index.tsx b/types/transaction/index.tsx new file mode 100644 index 0000000..5be4a81 --- /dev/null +++ b/types/transaction/index.tsx @@ -0,0 +1,27 @@ +export interface TransactionStatus { + status: + | "idle" + | "preparing" + | "signing" + | "sending" + | "confirming" + | "success" + | "error"; + signature?: string; + error?: string; + step?: number; + totalSteps?: number; +} + +export interface TxnStep { + id: string; + title: string; + description?: string; + status: "pending" | "active" | "completed" | "error"; +} + +export interface TxnFeedbackProps { + status: TransactionStatus; + onRetry?: () => void; + onClose?: () => void; +}