From e124a2747933947450726665b5e797d17673c7df Mon Sep 17 00:00:00 2001 From: Van Minh Date: Thu, 4 Sep 2025 18:46:58 +0700 Subject: [PATCH 1/3] 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; +} From 3d0183918731186b72dc32aa7d8513e0ede410bd Mon Sep 17 00:00:00 2001 From: Van Minh Date: Thu, 4 Sep 2025 19:06:53 +0700 Subject: [PATCH 2/3] feat: add txn-retry-button component with preview and documentation --- components/txn-retry-button-preview.tsx | 84 ++++++++ .../murphy/Txn-Feedback/txn-retry-button.tsx | 75 +++++++ components/ui/murphy/index.tsx | 2 + .../Txn-Feedback/txn-retry-button.mdx | 190 ++++++++++++++++++ public/r/txn-retry-button.json | 18 ++ registry.json | 16 ++ registry/components/txn-retry-button.json | 14 ++ 7 files changed, 399 insertions(+) create mode 100644 components/txn-retry-button-preview.tsx create mode 100644 components/ui/murphy/Txn-Feedback/txn-retry-button.tsx create mode 100644 content/docs/onchainkit/Txn-Feedback/txn-retry-button.mdx create mode 100644 public/r/txn-retry-button.json create mode 100644 registry/components/txn-retry-button.json diff --git a/components/txn-retry-button-preview.tsx b/components/txn-retry-button-preview.tsx new file mode 100644 index 0000000..5ab345d --- /dev/null +++ b/components/txn-retry-button-preview.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { TxnFeedbackToast } from "./ui/murphy"; +import type { TransactionStatus } from "@/types/transaction"; +import { TxnRetryButton } from "./ui/murphy/Txn-Feedback/txn-retry-button"; + +export default function TxnRetryButtonPreview() { + const [toastStatus, setToastStatus] = useState({ + status: "idle", + }); + + const simulateTransaction = async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const success = Math.random() < 0.6; + + if (success) { + setToastStatus({ + status: "success", + }); + } else { + setToastStatus({ + status: "error", + }); + throw new Error("Simulated transaction failure"); + } + }; + + const closeToast = () => { + setToastStatus({ status: "idle" }); + }; + + return ( +
+
+
+

+ Standard Retry (3 attempts) +

+ + Retry Transaction + +
+ +
+

+ Quick Retry (5 attempts, 500ms delay) +

+ + Quick Retry + +
+ +
+

+ Single Retry (1 attempt) +

+ + Single Retry + +
+
+ + +
+ ); +} diff --git a/components/ui/murphy/Txn-Feedback/txn-retry-button.tsx b/components/ui/murphy/Txn-Feedback/txn-retry-button.tsx new file mode 100644 index 0000000..61a1d74 --- /dev/null +++ b/components/ui/murphy/Txn-Feedback/txn-retry-button.tsx @@ -0,0 +1,75 @@ +"use client"; + +import type React from "react"; +import { useState } from "react"; +import { RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface TxnRetryButtonProps { + onRetry: () => Promise | void; + disabled?: boolean; + maxRetries?: number; + retryDelay?: number; + children?: React.ReactNode; + className?: string; + variant?: + | "default" + | "destructive" + | "outline" + | "secondary" + | "ghost" + | "link"; + size?: "default" | "sm" | "lg" | "icon"; +} + +export function TxnRetryButton({ + onRetry, + disabled = false, + maxRetries = 3, + retryDelay = 1000, + children = "Retry", + className, + variant = "default", + size = "default", +}: TxnRetryButtonProps) { + const [isRetrying, setIsRetrying] = useState(false); + const [retryCount, setRetryCount] = useState(0); + + const handleRetry = async () => { + if (retryCount >= maxRetries || isRetrying) return; + + setIsRetrying(true); + + try { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + await onRetry(); + setRetryCount(0); + } catch (error) { + setRetryCount((prev) => prev + 1); + console.error("Retry failed:", error); + } finally { + setIsRetrying(false); + } + }; + + const isDisabled = disabled || isRetrying || retryCount >= maxRetries; + + return ( + + ); +} diff --git a/components/ui/murphy/index.tsx b/components/ui/murphy/index.tsx index bc79e79..cf7cebd 100644 --- a/components/ui/murphy/index.tsx +++ b/components/ui/murphy/index.tsx @@ -38,6 +38,7 @@ 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"; +import { TxnRetryButton } from "./Txn-Feedback/txn-retry-button"; export { ConnectWalletButton, @@ -81,4 +82,5 @@ export { MPLHybridForm, TokenMetadataViewer, TxnFeedbackToast, + TxnRetryButton, }; diff --git a/content/docs/onchainkit/Txn-Feedback/txn-retry-button.mdx b/content/docs/onchainkit/Txn-Feedback/txn-retry-button.mdx new file mode 100644 index 0000000..18c4511 --- /dev/null +++ b/content/docs/onchainkit/Txn-Feedback/txn-retry-button.mdx @@ -0,0 +1,190 @@ +--- +title: Transaction Retry Button +description: Smart retry functionality with configurable limits and delays +icon: RefreshCw +--- + +import TxnRetryButtonPreview from "@/components/txn-retry-button-preview"; + + +
+ +
+
+ +## Installation + + + + + Install Transaction Retry Button + + + + + +## Basic Usage + +```tsx +"use client"; + +import { TxnRetryButton } from "@/components/ui/murphy/Txn-Feedback/txn-retry-button"; + +export default function MyPage() { + const executeTransaction = async () => { + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // 60% chance of failure to demonstrate retry logic + if (Math.random() < 0.6) { + throw new Error("Transaction failed: Network timeout"); + } + + console.log("Transaction successful!"); + }; + + return ( +
+

Retry Button Demo

+ + + Execute Transaction + +
+ ); +} +``` + +## Props + + Promise | void", + default: "required", + }, + disabled: { + description: "Whether the button is disabled", + type: "boolean", + default: "false", + }, + maxRetries: { + description: "Maximum number of retry attempts", + type: "number", + default: "3", + }, + retryDelay: { + description: "Delay between retries in milliseconds", + type: "number", + default: "1000", + }, + children: { + description: "Button content/label", + type: "React.ReactNode", + default: "'Retry'", + }, + className: { + description: "Additional CSS classes", + type: "string", + default: "undefined", + }, + variant: { + description: "Button variant style", + type: "'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'", + default: "'default'", + }, + size: { + description: "Button size", + type: "'default' | 'sm' | 'lg' | 'icon'", + default: "'default'", + }, + }} +/> + +## Retry Strategies + +### Network Errors + +For network-related failures, use longer delays and more retries: + +```tsx + + Network Transaction + +``` + +### Program Errors + +For smart contract errors, use fewer retries: + +```tsx + + Program Transaction + +``` + +### Timeout Errors + +For timeout errors, use moderate retries with increasing delays: + +```tsx + + Timeout-Prone Transaction + +``` + +## Button States + +### Loading State + +Shows spinning icon and "Retrying..." text during retry attempts. + +### Disabled State + +Button becomes disabled after reaching maximum retry attempts. + +### Retry Counter + +Displays current retry attempt count: "(2/3)" + +### Success Reset + +Retry counter resets to 0 after successful execution. + +## Customization + +### Custom Labels + +```tsx + + {(isRetrying) => (isRetrying ? "Processing..." : "Try Again")} + +``` + +### Custom Styling + +```tsx + + Custom Styled Retry + +``` diff --git a/public/r/txn-retry-button.json b/public/r/txn-retry-button.json new file mode 100644 index 0000000..151fe9a --- /dev/null +++ b/public/r/txn-retry-button.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "txn-retry-button", + "type": "registry:block", + "title": "A button component with built-in retry handling for transactions.", + "dependencies": [], + "registryDependencies": [ + "button" + ], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx", + "content": "\"use client\";\r\n\r\nimport type React from \"react\";\r\nimport { useState } from \"react\";\r\nimport { RefreshCw } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface TxnRetryButtonProps {\r\n onRetry: () => Promise | void;\r\n disabled?: boolean;\r\n maxRetries?: number;\r\n retryDelay?: number;\r\n children?: React.ReactNode;\r\n className?: string;\r\n variant?:\r\n | \"default\"\r\n | \"destructive\"\r\n | \"outline\"\r\n | \"secondary\"\r\n | \"ghost\"\r\n | \"link\";\r\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\r\n}\r\n\r\nexport function TxnRetryButton({\r\n onRetry,\r\n disabled = false,\r\n maxRetries = 3,\r\n retryDelay = 1000,\r\n children = \"Retry\",\r\n className,\r\n variant = \"default\",\r\n size = \"default\",\r\n}: TxnRetryButtonProps) {\r\n const [isRetrying, setIsRetrying] = useState(false);\r\n const [retryCount, setRetryCount] = useState(0);\r\n\r\n const handleRetry = async () => {\r\n if (retryCount >= maxRetries || isRetrying) return;\r\n\r\n setIsRetrying(true);\r\n\r\n try {\r\n await new Promise((resolve) => setTimeout(resolve, retryDelay));\r\n await onRetry();\r\n setRetryCount(0);\r\n } catch (error) {\r\n setRetryCount((prev) => prev + 1);\r\n console.error(\"Retry failed:\", error);\r\n } finally {\r\n setIsRetrying(false);\r\n }\r\n };\r\n\r\n const isDisabled = disabled || isRetrying || retryCount >= maxRetries;\r\n\r\n return (\r\n \r\n \r\n {isRetrying ? \"Retrying...\" : children}\r\n {retryCount > 0 && (\r\n \r\n ({retryCount}/{maxRetries})\r\n \r\n )}\r\n \r\n );\r\n}\r\n", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index f33e319..ddbac2e 100644 --- a/registry.json +++ b/registry.json @@ -1430,6 +1430,22 @@ } ] }, + { + "name": "txn-retry-button", + "type": "registry:block", + "title": "A button component with built-in retry handling for transactions.", + "registryDependencies": [ + "button" + ], + "dependencies": [], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx" + } + ] + }, { "name": "txn-settings", "type": "registry:block", diff --git a/registry/components/txn-retry-button.json b/registry/components/txn-retry-button.json new file mode 100644 index 0000000..61e4e42 --- /dev/null +++ b/registry/components/txn-retry-button.json @@ -0,0 +1,14 @@ +{ + "name": "txn-retry-button", + "type": "registry:block", + "title": "A button component with built-in retry handling for transactions.", + "registryDependencies": ["button"], + "dependencies": [], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-retry-button.tsx" + } + ] +} From 8bc8063098ee515948d56e0ef94f664840ba3547 Mon Sep 17 00:00:00 2001 From: Van Minh Date: Thu, 4 Sep 2025 21:27:14 +0700 Subject: [PATCH 3/3] feat: add txn-error-fallback component with retry and logging features, including documentation and registry entries --- components/txn-error-fallback-preview.tsx | 129 ++++++++++++ .../Txn-Feedback/txn-error-fallback.tsx | 139 +++++++++++++ components/ui/murphy/index.tsx | 2 + .../Txn-Feedback/txn-error-fallback.mdx | 183 ++++++++++++++++++ public/r/txn-error-fallback.json | 18 ++ registry.json | 16 ++ registry/components/txn-error-fallback.json | 14 ++ 7 files changed, 501 insertions(+) create mode 100644 components/txn-error-fallback-preview.tsx create mode 100644 components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx create mode 100644 content/docs/onchainkit/Txn-Feedback/txn-error-fallback.mdx create mode 100644 public/r/txn-error-fallback.json create mode 100644 registry/components/txn-error-fallback.json diff --git a/components/txn-error-fallback-preview.tsx b/components/txn-error-fallback-preview.tsx new file mode 100644 index 0000000..d9fe87c --- /dev/null +++ b/components/txn-error-fallback-preview.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { TxnErrorFallback } from "./ui/murphy/Txn-Feedback/txn-error-fallback"; +import { TxnRetryButton } from "./ui/murphy/Txn-Feedback/txn-retry-button"; +import { TxnFeedbackToast } from "./ui/murphy"; +import type { TransactionStatus } from "@/types/transaction"; + +// ===== Types ===== +type ErrorType = "simple" | "with-signature" | "with-logs"; + +type ErrorExample = { + error: string; + signature?: string; + showLogs?: boolean; + logs?: string[]; +}; + +// ===== Data ===== +const errorExamples: Record = { + simple: { + error: "Transaction failed: Insufficient funds for transaction fees", + }, + "with-signature": { + error: "Transaction failed: Program error occurred during execution", + signature: + "5VfYmGC9L8ty3D4HutfxndoKXGBwXJWKKvxgF7qQzqK8xMjU9v7Rw2sP3nT6hL4jK9mN8bC1dF2eG3hI5jK6lM7n", + }, + "with-logs": { + error: "Transaction failed: Custom program error: 0x1771", + signature: + "2B5VfYmGC9L8ty3D4HutfxndoKXGBwXJWKKvxgF7qQzqK8xMjU9v7Rw2sP3nT6hL4jK9mN8bC1dF2eG3hI5jK6lM", + showLogs: true, + logs: [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program log: Instruction: Transfer", + "Program log: Error: custom program error: 0x1771", + "Program 11111111111111111111111111111111 consumed 200000 of 200000 compute units", + "Program 11111111111111111111111111111111 failed: custom program error: 0x1771", + ], + }, +}; + +export default function TxnErrorFallbackPreview() { + const [showError, setShowError] = useState(false); + const [errorType, setErrorType] = useState("simple"); + + const [currentErrorMsg, setCurrentErrorMsg] = useState(""); + + const [toastStatus, setToastStatus] = useState({ + status: "idle", + }); + + const retryTxn = async () => { + await new Promise((r) => setTimeout(r, 800)); + const ok = Math.random() > 0.5; + + if (ok) { + const sig = errorExamples[errorType].signature; + setShowError(false); + setToastStatus({ status: "success", signature: sig }); + return; + } + + setToastStatus({ + status: "error", + error: currentErrorMsg || errorExamples[errorType].error, + }); + throw new Error("Simulated transaction failure"); + }; + + const openWithType = (type: ErrorType) => { + setErrorType(type); + setCurrentErrorMsg(errorExamples[type].error); + setShowError(true); + }; + + const closeToast = () => setToastStatus({ status: "idle" }); + + return ( +
+
+ + + +
+ + {showError && ( +
+
+ setShowError(false)} + /> + + Try Again + +
+
+ )} + + +
+ ); +} diff --git a/components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx b/components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx new file mode 100644 index 0000000..3a7289e --- /dev/null +++ b/components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { AlertTriangle, RefreshCw, Copy, ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { useState } from "react"; + +interface TxnErrorFallbackProps { + error: string; + signature?: string; + onRetry?: () => void; + onClose?: () => void; + showLogs?: boolean; + logs?: string[]; +} + +export function TxnErrorFallback({ + error, + signature, + onRetry, + onClose, + showLogs = false, + logs = [], +}: TxnErrorFallbackProps) { + const [copied, setCopied] = useState(false); + + const copyError = async () => { + await navigator.clipboard.writeText(error); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + +
+ +
+ + Transaction Failed + + + Your transaction could not be completed + +
+ + +
+
+

+ {error} +

+ +
+ {copied && ( + + Copied to clipboard + + )} +
+ + {signature && ( +
+
+

+ Transaction Signature +

+

+ {signature.slice(0, 8)}...{signature.slice(-8)} +

+
+ +
+ )} + + {showLogs && logs.length > 0 && ( +
+ + View Transaction Logs + +
+ {logs.map((log, index) => ( +

+ {log} +

+ ))} +
+
+ )} +
+ + + {onRetry && ( + + )} + + +
+ ); +} diff --git a/components/ui/murphy/index.tsx b/components/ui/murphy/index.tsx index cf7cebd..75bd18a 100644 --- a/components/ui/murphy/index.tsx +++ b/components/ui/murphy/index.tsx @@ -39,6 +39,7 @@ import { MPLHybridForm } from "./mpl-hybrid-form"; import { TokenMetadataViewer } from "./token-metadata-viewer"; import { TxnFeedbackToast } from "./Txn-Feedback/txn-feedback-toast"; import { TxnRetryButton } from "./Txn-Feedback/txn-retry-button"; +import { TxnErrorFallback } from "./Txn-Feedback/txn-error-fallback"; export { ConnectWalletButton, @@ -83,4 +84,5 @@ export { TokenMetadataViewer, TxnFeedbackToast, TxnRetryButton, + TxnErrorFallback, }; diff --git a/content/docs/onchainkit/Txn-Feedback/txn-error-fallback.mdx b/content/docs/onchainkit/Txn-Feedback/txn-error-fallback.mdx new file mode 100644 index 0000000..8e95e39 --- /dev/null +++ b/content/docs/onchainkit/Txn-Feedback/txn-error-fallback.mdx @@ -0,0 +1,183 @@ +--- +title: Transaction Error Fallback +description: Comprehensive error handling UI with retry and debugging features +icon: Ban +--- + +import TxnErrorFallbackPreiew from "@/components/txn-error-fallback-preview"; + + +
+ +
+
+ +## Installation + + + + + Install Transaction Error Fallback + + + + + +## Basic Usage + +```tsx +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { TxnnErrorFallback } from "@/components/ui/murphy/Txn-Feedback/txn-error-fallback"; + +export default function MyPage() { + const [showError, setShowError] = useState(false); + + const handleRetry = () => { + console.log("Retrying transaction..."); + setShowError(false); + // Your retry logic here + }; + + return ( +
+

Error Handling Demo

+ + + + {showError && ( +
+ setShowError(false)} + /> +
+ )} +
+ ); +} +``` + +## Props + + void", + default: "undefined", + }, + onClose: { + description: "Callback function when close button is clicked", + type: "() => void", + default: "undefined", + }, + showLogs: { + description: "Whether to show expandable transaction logs", + type: "boolean", + default: "false", + }, + logs: { + description: "Array of log messages to display", + type: "string[]", + default: "[]", + }, + }} +/> + +## Examples + +### Simple Error + +```tsx + retryTransaction()} + onClose={() => closeError()} +/> +``` + +### Error with Transaction Signature + +```tsx + retryTransaction()} + onClose={() => closeError()} +/> +``` + +### Error with Transaction Logs + +```tsx + retryTransaction()} + onClose={() => closeError()} + showLogs={true} + logs={[ + "Program 11111111111111111111111111111111 invoke [1]", + "Program log: Instruction: Transfer", + "Program log: Error: custom program error: 0x1771", + "Program 11111111111111111111111111111111 consumed 200000 of 200000 compute units", + "Program 11111111111111111111111111111111 failed: custom program error: 0x1771", + ]} +/> +``` + +## Customization + +### Custom Error Messages + +```tsx +const getErrorMessage = (error: string) => { + const errorMap = { + "insufficient funds": "You need more SOL to complete this transaction", + "network error": "Connection problem - please try again", + timeout: "Transaction is taking longer than expected", + "program error": "Smart contract issue - please contact support", + }; + + for (const [key, message] of Object.entries(errorMap)) { + if (error.toLowerCase().includes(key)) { + return message; + } + } + + return error; +}; +``` + +### Custom Styling + +```tsx + +``` diff --git a/public/r/txn-error-fallback.json b/public/r/txn-error-fallback.json new file mode 100644 index 0000000..2851eb1 --- /dev/null +++ b/public/r/txn-error-fallback.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "txn-error-fallback", + "type": "registry:block", + "title": "Fallback UI for failed transactions with retry and log support.", + "dependencies": [], + "registryDependencies": [ + "button" + ], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx", + "content": "\"use client\";\r\n\r\nimport { AlertTriangle, RefreshCw, Copy, ExternalLink } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardDescription,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { useState } from \"react\";\r\n\r\ninterface TxnErrorFallbackProps {\r\n error: string;\r\n signature?: string;\r\n onRetry?: () => void;\r\n onClose?: () => void;\r\n showLogs?: boolean;\r\n logs?: string[];\r\n}\r\n\r\nexport function TxnErrorFallback({\r\n error,\r\n signature,\r\n onRetry,\r\n onClose,\r\n showLogs = false,\r\n logs = [],\r\n}: TxnErrorFallbackProps) {\r\n const [copied, setCopied] = useState(false);\r\n\r\n const copyError = async () => {\r\n await navigator.clipboard.writeText(error);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n };\r\n\r\n return (\r\n \r\n \r\n
\r\n \r\n
\r\n \r\n Transaction Failed\r\n \r\n \r\n Your transaction could not be completed\r\n \r\n
\r\n\r\n \r\n
\r\n
\r\n

\r\n {error}\r\n

\r\n \r\n \r\n \r\n
\r\n {copied && (\r\n \r\n Copied to clipboard\r\n \r\n )}\r\n
\r\n\r\n {signature && (\r\n
\r\n
\r\n

\r\n Transaction Signature\r\n

\r\n

\r\n {signature.slice(0, 8)}...{signature.slice(-8)}\r\n

\r\n
\r\n \r\n window.open(\r\n `https://explorer.solana.com/tx/${signature}`,\r\n \"_blank\"\r\n )\r\n }\r\n className=\"dark:border-gray-700\"\r\n >\r\n \r\n \r\n
\r\n )}\r\n\r\n {showLogs && logs.length > 0 && (\r\n
\r\n \r\n View Transaction Logs\r\n \r\n
\r\n {logs.map((log, index) => (\r\n \r\n {log}\r\n

\r\n ))}\r\n
\r\n
\r\n )}\r\n
\r\n\r\n \r\n {onRetry && (\r\n \r\n )}\r\n \r\n Close\r\n \r\n \r\n
\r\n );\r\n}\r\n", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index ddbac2e..e88e46c 100644 --- a/registry.json +++ b/registry.json @@ -1386,6 +1386,22 @@ } ] }, + { + "name": "txn-error-fallback", + "type": "registry:block", + "title": "Fallback UI for failed transactions with retry and log support.", + "registryDependencies": [ + "button" + ], + "dependencies": [], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx" + } + ] + }, { "name": "txn-feedback-toast", "type": "registry:block", diff --git a/registry/components/txn-error-fallback.json b/registry/components/txn-error-fallback.json new file mode 100644 index 0000000..e481352 --- /dev/null +++ b/registry/components/txn-error-fallback.json @@ -0,0 +1,14 @@ +{ + "name": "txn-error-fallback", + "type": "registry:block", + "title": "Fallback UI for failed transactions with retry and log support.", + "registryDependencies": ["button"], + "dependencies": [], + "files": [ + { + "path": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx", + "type": "registry:file", + "target": "components/ui/murphy/Txn-Feedback/txn-error-fallback.tsx" + } + ] +}