diff --git a/crates/sage/src/error.rs b/crates/sage/src/error.rs index 043f7e0eb..db927ae83 100644 --- a/crates/sage/src/error.rs +++ b/crates/sage/src/error.rs @@ -238,7 +238,8 @@ impl Error { pub fn kind(&self) -> ErrorKind { match self { Self::Wallet(..) => ErrorKind::Wallet, - Self::NotLoggedIn | Self::NoSigningKey => ErrorKind::Unauthorized, + Self::NotLoggedIn => ErrorKind::Unauthorized, + Self::NoSigningKey => ErrorKind::Wallet, Self::Keychain(error) => match error { KeychainError::Decrypt => ErrorKind::Unauthorized, KeychainError::KeyExists diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx new file mode 100644 index 000000000..0590168a6 --- /dev/null +++ b/src/components/Banner.tsx @@ -0,0 +1,32 @@ +import { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +interface BannerProps { + message: string; + icon?: ReactNode; + 'aria-label'?: string; + className?: string; +} + +export function Banner({ + message, + icon, + className, + 'aria-label': ariaLabel, +}: BannerProps) { + return ( +
+ {icon ? icon : null} + {message} +
+ ); +} diff --git a/src/components/ClawbackCoinsCard.tsx b/src/components/ClawbackCoinsCard.tsx index 4bfdfd269..7ca34f9b3 100644 --- a/src/components/ClawbackCoinsCard.tsx +++ b/src/components/ClawbackCoinsCard.tsx @@ -20,6 +20,7 @@ import { import { useErrors } from '@/hooks/useErrors'; import { amount } from '@/lib/formTypes'; import { toMojos } from '@/lib/utils'; +import { useWallet } from '@/contexts/WalletContext'; import { useWalletState } from '@/state'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from '@lingui/core/macro'; @@ -61,6 +62,7 @@ export function ClawbackCoinsCard({ setSelectedCoins, }: ClawbackCoinsCardProps) { const walletState = useWalletState(); + const { isTransactionDisabled } = useWallet(); const { addError } = useErrors(); @@ -315,7 +317,7 @@ export function ClawbackCoinsCard({ <> + - { - setPending(true); - - (async () => { - let finalSignature: string | null = signature; - - if ( - !finalSignature && - response !== null && - 'coin_spends' in response - ) { - if (!(await promptIfEnabled())) return; + {isColdWalletUnsignedMode ? ( + + + + + Submit + + + + + + Cold wallets cannot sign transactions. Copy the JSON and sign + externally. + + + + ) : ( + { + setPending(true); + + (async () => { + let finalSignature: string | null = signature; + + if ( + !finalSignature && + response !== null && + 'coin_spends' in response + ) { + if (!(await promptIfEnabled())) return; + + const data = await commands + .signCoinSpends({ + coin_spends: response.coin_spends, + }) + .catch(addError); + + if (!data) return reset(); + + finalSignature = data.spend_bundle.aggregated_signature; + } const data = await commands - .signCoinSpends({ - coin_spends: response.coin_spends, + .submitTransaction({ + spend_bundle: { + coin_spends: + response === null + ? [] + : 'coin_spends' in response + ? response.coin_spends + : response.spend_bundle.coin_spends, + aggregated_signature: finalSignature ?? '', + }, }) .catch(addError); if (!data) return reset(); - finalSignature = data.spend_bundle.aggregated_signature; - } - - const data = await commands - .submitTransaction({ - spend_bundle: { - coin_spends: - response === null - ? [] - : 'coin_spends' in response - ? response.coin_spends - : response.spend_bundle.coin_spends, - aggregated_signature: finalSignature ?? '', - }, - }) - .catch(addError); - - if (!data) return reset(); - - toast.success(t`Transaction submitted successfully`); - onConfirm?.(); - reset(); - })().finally(() => setPending(false)); - }} - > - Submit - + toast.success(t`Transaction submitted successfully`); + onConfirm?.(); + reset(); + })().finally(() => setPending(false)); + }} + > + Submit + + )} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 9298f620f..d80346b81 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -8,13 +8,14 @@ import { import { useInsets } from '@/contexts/SafeAreaContext'; import { useWallet } from '@/contexts/WalletContext'; import { t } from '@lingui/core/macro'; -import { PanelLeft, PanelLeftClose } from 'lucide-react'; +import { PanelLeft, PanelLeftClose, Snowflake } from 'lucide-react'; import { PropsWithChildren } from 'react'; import { useLocation } from 'react-router-dom'; import { Insets } from 'tauri-plugin-safe-area-insets'; import { useTheme } from 'theme-o-rama'; import { useLocalStorage } from 'usehooks-ts'; import { BottomNav, TopNav } from './Nav'; +import { Banner } from './Banner'; import { WalletSwitcher } from './WalletSwitcher'; function WalletTransitionWrapper({ @@ -22,7 +23,7 @@ function WalletTransitionWrapper({ props, insets, }: PropsWithChildren<{ props: LayoutProps; insets: Insets }>) { - const { isSwitching, wallet } = useWallet(); + const { isSwitching, wallet, isReadOnly } = useWallet(); // Only show content if we have a wallet or we're not switching // This prevents old wallet data from showing during transition @@ -43,7 +44,23 @@ function WalletTransitionWrapper({ : 'env(safe-area-inset-bottom)', }} > - {shouldShow ? children : null} + {shouldShow ? ( + <> + {isReadOnly && ( +