From 76718b00766b4ceccc92b0f909409ff0cdd72a96 Mon Sep 17 00:00:00 2001
From: Divine Osehotue Omoiyobe
Date: Tue, 28 Jul 2026 23:43:18 +0000
Subject: [PATCH] issue: 349, 354, 355, 356
---
src/components/ErrorBoundary.tsx | 31 +++++++++++++++--
src/components/FeeEstimator.tsx | 55 +++++++++++++++++++-----------
src/components/index.ts | 2 +-
src/components/ui/Skeleton.tsx | 43 +++++++++++++++++------
src/screens/SorobanScreen.tsx | 9 +++--
src/screens/TransactionsScreen.tsx | 5 ++-
6 files changed, 107 insertions(+), 38 deletions(-)
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
index 4649f24..0b4c729 100644
--- a/src/components/ErrorBoundary.tsx
+++ b/src/components/ErrorBoundary.tsx
@@ -12,21 +12,26 @@ interface Props {
onError?: (error: Error, info: ErrorInfo) => void;
/** Render fallback content as an in-page scoped panel instead of a full-page state. */
isolate?: boolean;
+ /** Optional support URL shown in the default fallback. */
+ supportUrl?: string;
}
interface State {
error: Error | null;
resetKey: number;
+ componentStack: string | null;
}
export class ErrorBoundary extends Component {
- state: State = { error: null, resetKey: 0 };
+ state: State = { error: null, resetKey: 0, componentStack: null };
static getDerivedStateFromError(error: Error): Partial {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
+ this.setState({ error, componentStack: info.componentStack ?? null });
+
if (this.props.onError) {
this.props.onError(error, info);
return;
@@ -44,8 +49,8 @@ export class ErrorBoundary extends Component {
}));
render() {
- const { error, resetKey } = this.state;
- const { children, fallback, isolate } = this.props;
+ const { error, resetKey, componentStack } = this.state;
+ const { children, fallback, isolate, supportUrl } = this.props;
if (!error) return {children}
;
@@ -101,6 +106,26 @@ export class ErrorBoundary extends Component {
: "Details are hidden in production."}
+ {supportUrl && (
+
+ Report this issue
+
+ )}
+ {import.meta.env.DEV && componentStack && (
+
+
+ Show component stack
+
+
+ {componentStack}
+
+
+ )}
);
}
-function FeeCell({
+export function FeeCell({
label,
value,
unit,
diff --git a/src/components/index.ts b/src/components/index.ts
index a75f4c3..36cf68a 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -65,7 +65,7 @@ export { AllowanceManager } from "./AllowanceManager";
// Transactions
export { ActivityTimeline } from "./ActivityTimeline";
export { ClaimableBalanceCard } from "./ClaimableBalanceCard";
-export { FeeEstimator } from "./FeeEstimator";
+export { FeeEstimator, FeeCell } from "./FeeEstimator";
export { GasOptimizer } from "./GasOptimizer";
export type {
TransactionConfirmModalProps,
diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx
index b64d8ec..8c5e53d 100644
--- a/src/components/ui/Skeleton.tsx
+++ b/src/components/ui/Skeleton.tsx
@@ -5,6 +5,8 @@ import { cn } from "@/lib/utils";
interface SkeletonProps extends React.HTMLAttributes {
/** Render as a circle (for avatars/icons) */
circle?: boolean;
+ /** Shape variant for non-circular placeholders. */
+ shape?: "rounded" | "circle" | "square";
/**
* Animation variant.
* - "pulse" — opacity pulsing via Tailwind's animate-pulse (default)
@@ -13,13 +15,18 @@ interface SkeletonProps extends React.HTMLAttributes {
variant?: "pulse" | "shimmer";
}
-export function Skeleton({ circle, variant = "pulse", className, ...props }: SkeletonProps) {
+export function Skeleton({ circle, shape = "rounded", variant = "pulse", className, ...props }: SkeletonProps) {
+ const resolvedShape = circle ? "circle" : shape;
return (
{
+ count?: number;
+}
+
/** Pre-composed row skeleton: icon + two lines of text */
-export function SkeletonRow({ className, ...props }: React.HTMLAttributes
) {
+export function SkeletonRow({ className, count = 1, ...props }: SkeletonRowProps) {
return (
1 ? "flex flex-col gap-3" : undefined, className)}
{...props}
>
-
-
-
-
-
+ {Array.from({ length: count }).map((_, index) => (
+
+ ))}
);
}
@@ -50,6 +65,7 @@ interface SkeletonCardProps extends React.HTMLAttributes {
rows?: number;
structure?: React.ReactNode;
children?: React.ReactNode;
+ header?: React.ReactNode;
}
/**
@@ -82,6 +98,7 @@ export function SkeletonCard({
rows = 3,
structure,
children,
+ header,
className,
...props
}: SkeletonCardProps) {
@@ -98,8 +115,12 @@ export function SkeletonCard({
) : (
<>
-
-
+ {header ?? (
+ <>
+
+
+ >
+ )}
{Array.from({ length: rows }).map((_, i) => (
diff --git a/src/screens/SorobanScreen.tsx b/src/screens/SorobanScreen.tsx
index c2b5045..d9f14d6 100644
--- a/src/screens/SorobanScreen.tsx
+++ b/src/screens/SorobanScreen.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { ContractEventFeed } from "@/components/ContractEventFeed";
+import { ErrorBoundary } from "@/components/ErrorBoundary";
import { SorobanPanel } from "@/components/SorobanPanel";
import { useSorokit } from "@/context/useSorokit";
import { SCREEN_LABELS } from "@/lib/nav-labels";
@@ -94,9 +95,13 @@ export function SorobanScreen() {
)}
-
+
+
+
{contractId.trim() !== "" && (
-
+
+
+
)}
);
diff --git a/src/screens/TransactionsScreen.tsx b/src/screens/TransactionsScreen.tsx
index 61ecf74..baec39a 100644
--- a/src/screens/TransactionsScreen.tsx
+++ b/src/screens/TransactionsScreen.tsx
@@ -1,4 +1,5 @@
import { ActivityTimeline } from "@/components/ActivityTimeline";
+import { ErrorBoundary } from "@/components/ErrorBoundary";
import { FeeEstimator } from "@/components/FeeEstimator";
import { GasOptimizer } from "@/components/GasOptimizer";
import { MultiSigTransactionBuilder } from "@/components/MultiSigTransactionBuilder";
@@ -19,7 +20,9 @@ export function TransactionsScreen() {
-
+
+
+
);