diff --git a/src/components/common/CreatorOnboardingForm.tsx b/src/components/common/CreatorOnboardingForm.tsx index 77023c7b..a7cb78f1 100644 --- a/src/components/common/CreatorOnboardingForm.tsx +++ b/src/components/common/CreatorOnboardingForm.tsx @@ -109,6 +109,8 @@ export const CreatorOnboardingForm: React.FC< placeholder="Tell us about yourself..." touched={touched.bio} rows={4} + maxLength={200} + showCharacterCount={true} /> = ({ className, }) => { const [copied, setCopied] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 20); + }; + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); // Display-normalised handle; raw `handle` is preserved for any equality / // URL construction the caller might do via the prop. @@ -76,77 +85,99 @@ const CreatorProfileHeader: React.FC = ({ return (
-
-
- -
-
-
-

- {name} -

- {isVerified && ( -
- +
+
+ + + +
+
+ + {name} + + {isVerified && ( +
+ +
+ )} +
+ {!isScrolled ? ( +
+

+ {displayHandle || `@${handle}`} +

+
+ ) : ( +

+ {displayHandle || `@${handle}`} +

)}
-

+ +

+
- -
- -
); }; diff --git a/src/components/common/FormInput.tsx b/src/components/common/FormInput.tsx index a6ffe25f..2c4826ce 100644 --- a/src/components/common/FormInput.tsx +++ b/src/components/common/FormInput.tsx @@ -22,6 +22,7 @@ interface FormInputProps { suffix?: React.ReactNode; // Optional wrapper className for the input container wrapperClassName?: string; + showCharacterCount?: boolean; } export const FormInput: React.FC = ({ @@ -42,6 +43,7 @@ export const FormInput: React.FC = ({ prefix, suffix, wrapperClassName = '', + showCharacterCount = false, }) => { // Local display state is used only for number inputs so we can // show formatted (comma separated) values while keeping the @@ -198,16 +200,33 @@ export const FormInput: React.FC = ({ {renderInputWithElements()} - - {hasError && ( - - )} + +
+
+ {hasError && ( + + )} +
+ {showCharacterCount && maxLength && ( +
+ {String(value).length} / {maxLength} +
+ )} +
); }; diff --git a/src/components/common/PendingTxModal.tsx b/src/components/common/PendingTxModal.tsx index 93039149..865d3e4f 100644 --- a/src/components/common/PendingTxModal.tsx +++ b/src/components/common/PendingTxModal.tsx @@ -11,6 +11,17 @@ import { Button } from '@/components/ui/button'; import CircularSpinner from '@/components/common/CircularSpinnerProps'; import { cn } from '@/lib/utils'; import TransactionHashRow from '@/components/common/TransactionHashRow'; +import { + getConfirmationStatus, + getConfirmationTone, +} from '@/utils/transaction.utils'; +import { Tooltip } from '@/components/ui/tooltip'; + +const confirmationToneClasses = { + neutral: 'border-slate-500/30 bg-slate-500/20 text-slate-400', + warning: 'border-amber-500/30 bg-amber-500/20 text-amber-300', + success: 'border-emerald-500/30 bg-emerald-500/20 text-emerald-400', +} as const; export interface PendingTxModalProps { open: boolean; @@ -30,6 +41,8 @@ export interface PendingTxModalProps { label: string; onClick: () => void; }; + /** Optional block confirmation count */ + confirmations?: number; } const PendingTxModal: React.FC = ({ @@ -42,12 +55,20 @@ const PendingTxModal: React.FC = ({ explorerUrl, blockDismissal = false, action, + confirmations, }) => { const handleOpenChange = (next: boolean) => { if (!next && blockDismissal && isLoading) return; onOpenChange?.(next); }; + const confirmationStatus = + confirmations !== undefined ? getConfirmationStatus(confirmations) : undefined; + const confirmationTone = + confirmationStatus !== undefined + ? getConfirmationTone(confirmationStatus) + : undefined; + return ( = ({ {description} + + {confirmationStatus !== undefined && confirmationTone !== undefined && ( +
+ + + {confirmationStatus} + + +

+ {confirmations} confirmations +

+
+ )} {txHash && ( diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 89be5b2e..dec93076 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -31,6 +31,7 @@ import NetworkMismatchBanner from '@/components/common/NetworkMismatchBanner'; import StellarConnectionQualityBadge from '@/components/common/StellarConnectionQualityBadge'; import { useNetworkMismatch } from '@/hooks/useNetworkMismatch'; import showToast from '@/utils/toast.util'; +import { getSignatureErrorMessage } from '@/utils/errorHandling.utils'; import { formatCompactNumber, formatNumber } from '@/utils/numberFormat.utils'; import PrecisionModeToggle, { type PrecisionMode, @@ -518,9 +519,9 @@ function LandingPage() { : `Holdings refreshed: -${formatNumber(amount)} keys.` ); setTradeDialogOpen(false); - } catch { + } catch (error) { setFeaturedHoldings(previousHoldings); - showToast.error('Trade failed. Holdings have been restored.'); + showToast.error(getSignatureErrorMessage(error)); } finally { setTradeSubmitting(false); } diff --git a/src/utils/errorHandling.utils.ts b/src/utils/errorHandling.utils.ts new file mode 100644 index 00000000..fd254cb2 --- /dev/null +++ b/src/utils/errorHandling.utils.ts @@ -0,0 +1,55 @@ +/** + * Wallet and Signature error handling utilities + */ + +/** + * Standard wallet error types and their corresponding user-friendly messages + */ +export const WALLET_ERROR_COPY = { + SIGNATURE_REJECTED: "Signature request was declined. Please try again when you're ready to confirm.", + SIGNATURE_FAILED: "The signature request failed. Please ensure your wallet is unlocked and try again.", + GENERIC_TRANSACTION_FAILED: "Transaction failed. Please check your balance or connection and try again.", +}; + +interface WalletError { + message?: string; + code?: number | string; +} + +/** + * Determines if an error is a user rejection of a signature or transaction + * + * @param error - The error object to check + * @returns boolean + */ +export function isUserRejection(error: unknown): boolean { + if (!error || typeof error !== 'object') return false; + + const err = error as WalletError; + // Check common error codes and names across different wallets/libraries + const errorMessage = (err.message || '').toLowerCase(); + const errorCode = err.code; + + return ( + errorCode === 4001 || // EIP-1193 userRejectedRequest + errorCode === 'ACTION_REJECTED' || // ethers.js + errorMessage.includes('user rejected') || + errorMessage.includes('declined') || + errorMessage.includes('cancelled') + ); +} + +/** + * Gets a human-readable error message for a wallet signature failure + * + * @param error - The error object + * @returns Targeted error message + */ +export function getSignatureErrorMessage(error: unknown): string { + if (isUserRejection(error)) { + return WALLET_ERROR_COPY.SIGNATURE_REJECTED; + } + + // For other failures that aren't explicit rejections + return WALLET_ERROR_COPY.SIGNATURE_FAILED; +} diff --git a/src/utils/transaction.utils.ts b/src/utils/transaction.utils.ts new file mode 100644 index 00000000..3d998be7 --- /dev/null +++ b/src/utils/transaction.utils.ts @@ -0,0 +1,43 @@ +/** + * Transaction confirmation status thresholds + */ +export const CONFIRMATION_THRESHOLDS = { + PENDING: 0, + CONFIRMING_START: 1, + CONFIRMED: 6, +} as const; + +export type ConfirmationStatus = 'Pending' | 'Confirming' | 'Confirmed'; + +/** + * Maps a raw block confirmation count to a human-readable status label + * + * @param confirmations - The number of block confirmations + * @returns Human-readable confirmation status + */ +export function getConfirmationStatus(confirmations: number): ConfirmationStatus { + if (confirmations <= CONFIRMATION_THRESHOLDS.PENDING) { + return 'Pending'; + } + if (confirmations < CONFIRMATION_THRESHOLDS.CONFIRMED) { + return 'Confirming'; + } + return 'Confirmed'; +} + +/** + * Maps a confirmation status to a UI tone/color + * + * @param status - The confirmation status + * @returns UI tone string + */ +export function getConfirmationTone(status: ConfirmationStatus): 'neutral' | 'warning' | 'success' { + switch (status) { + case 'Pending': + return 'neutral'; + case 'Confirming': + return 'warning'; + case 'Confirmed': + return 'success'; + } +}