Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/sage/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions src/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
'flex items-center gap-2 px-4 py-1.5 text-xs bg-muted border-b text-muted-foreground',
className,
)}
role='status'
aria-label={ariaLabel}
aria-live='polite'
aria-atomic='true'
>
{icon ? icon : null}
<span>{message}</span>
</div>
);
}
10 changes: 8 additions & 2 deletions src/components/ClawbackCoinsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,6 +62,7 @@ export function ClawbackCoinsCard({
setSelectedCoins,
}: ClawbackCoinsCardProps) {
const walletState = useWalletState();
const { isTransactionDisabled } = useWallet();

const { addError } = useErrors();

Expand Down Expand Up @@ -315,7 +317,7 @@ export function ClawbackCoinsCard({
<>
<Button
variant='outline'
disabled={!canClawBack}
disabled={isTransactionDisabled || !canClawBack}
onClick={() => {
if (canClawBack) setClawBackOpen(true);
}}
Expand All @@ -326,7 +328,11 @@ export function ClawbackCoinsCard({

<Button
variant='outline'
disabled={selectedCoinIds.length === 0 || canClawBack}
disabled={
isTransactionDisabled ||
selectedCoinIds.length === 0 ||
canClawBack
}
onClick={() => {
setFinalizeOpen(true);
}}
Expand Down
129 changes: 82 additions & 47 deletions src/components/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { LoadingButton } from '@/components/ui/loading-button';
import { useBiometric } from '@/hooks/useBiometric';
import { useErrors } from '@/hooks/useErrors';
import { useWallet } from '@/contexts/WalletContext';
import { fromMojos } from '@/lib/utils';
import { useWalletState } from '@/state';
import { t } from '@lingui/core/macro';
Expand Down Expand Up @@ -38,9 +39,11 @@ import { formatNumber } from '../i18n';
import { calculateTransaction } from './AdvancedTransactionSummary';
import { CopyButton } from './CopyButton';
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
import { ReadOnlyButton } from './ReadOnlyButton';
import { Badge } from './ui/badge';
import { Separator } from './ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';

export interface ConfirmationDialogProps {
response: TransactionResponse | TakeOfferResponse | null;
Expand All @@ -65,6 +68,9 @@ export default function ConfirmationDialog({

const { addError } = useErrors();
const { promptIfEnabled } = useBiometric();
const { isReadOnly, allowUnsigned } = useWallet();

const isColdWalletUnsignedMode = isReadOnly && allowUnsigned;

const [pending, setPending] = useState(false);
const [signature, setSignature] = useState<string | null>(null);
Expand All @@ -77,6 +83,12 @@ export default function ConfirmationDialog({
}
}, [response]);

useEffect(() => {
if (response !== null && isColdWalletUnsignedMode) {
setActiveTab('json');
}
}, [response, isColdWalletUnsignedMode]);

const reset = () => {
setPending(false);
setSignature(null);
Expand Down Expand Up @@ -512,7 +524,7 @@ export default function ConfirmationDialog({
</Alert>

<div className='flex items-center gap-2 mt-4'>
<Button
<ReadOnlyButton
size='sm'
onClick={async () => {
if (await promptIfEnabled()) {
Expand All @@ -534,7 +546,7 @@ export default function ConfirmationDialog({
.catch(addError);
}
}}
disabled={!!signature}
disabled={!!signature || isReadOnly}
>
{signature ? (
<>
Expand All @@ -547,7 +559,7 @@ export default function ConfirmationDialog({
) : (
<Trans>Sign Transaction</Trans>
)}
</Button>
</ReadOnlyButton>

<Button
variant='outline'
Expand Down Expand Up @@ -606,57 +618,80 @@ export default function ConfirmationDialog({
<Button variant='ghost' onClick={reset}>
<Trans>Cancel</Trans>
</Button>
<LoadingButton
loading={pending}
loadingText={t`Submitting`}
onClick={() => {
setPending(true);

(async () => {
let finalSignature: string | null = signature;

if (
!finalSignature &&
response !== null &&
'coin_spends' in response
) {
if (!(await promptIfEnabled())) return;
{isColdWalletUnsignedMode ? (
<Tooltip>
<TooltipTrigger asChild>
<span className='inline-flex cursor-not-allowed'>
<LoadingButton
disabled
loading={false}
loadingText=''
className='pointer-events-none'
>
<Trans>Submit</Trans>
</LoadingButton>
</span>
</TooltipTrigger>
<TooltipContent>
<Trans>
Cold wallets cannot sign transactions. Copy the JSON and sign
externally.
</Trans>
</TooltipContent>
</Tooltip>
) : (
<LoadingButton
loading={pending}
loadingText={t`Submitting`}
onClick={() => {
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));
}}
>
<Trans>Submit</Trans>
</LoadingButton>
toast.success(t`Transaction submitted successfully`);
onConfirm?.();
reset();
})().finally(() => setPending(false));
}}
>
<Trans>Submit</Trans>
</LoadingButton>
)}
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
24 changes: 20 additions & 4 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ 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({
children,
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
Expand All @@ -43,7 +44,23 @@ function WalletTransitionWrapper({
: 'env(safe-area-inset-bottom)',
}}
>
{shouldShow ? children : null}
{shouldShow ? (
<>
{isReadOnly && (
<Banner
message={t`Read-only wallet — transactions require private keys`}
aria-label={t`Read-only wallet`}
icon={
<Snowflake
className='h-3 w-3 flex-shrink-0'
aria-hidden='true'
/>
}
/>
)}
{children}
</>
) : null}
</div>
);
}
Expand Down Expand Up @@ -99,7 +116,6 @@ export function FullLayout(props: LayoutProps) {
aria-label={
isCollapsed ? t`Expand sidebar` : t`Collapse sidebar`
}
aria-expanded={!isCollapsed}
>
{isCollapsed ? (
<PanelLeft className='h-5 w-5' aria-hidden='true' />
Expand Down
11 changes: 7 additions & 4 deletions src/components/NftCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { amount } from '@/lib/formTypes';
import { nftUri } from '@/lib/nftUri';
import { toMojos } from '@/lib/utils';
import { useWalletState } from '@/state';
import { useWallet } from '@/contexts/WalletContext';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
Expand Down Expand Up @@ -94,6 +95,7 @@ interface NftCardProps {

export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
const walletState = useWalletState();
const { isTransactionDisabled } = useWallet();
const [offerState, setOfferState] = useOfferStateWithDefault();
const navigate = useNavigate();

Expand Down Expand Up @@ -401,7 +403,7 @@ export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
e.stopPropagation();
setTransferOpen(true);
}}
disabled={!nft.created_height}
disabled={isTransactionDisabled || !nft.created_height}
aria-label={t`Transfer ${nftName}`}
>
<SendIcon className='mr-2 h-4 w-4' aria-hidden='true' />
Expand All @@ -416,7 +418,7 @@ export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
e.stopPropagation();
setAssignOpen(true);
}}
disabled={!nft.created_height}
disabled={isTransactionDisabled || !nft.created_height}
aria-label={
nft.owner_did === null ? t`Assign profile` : t`Edit profile`
}
Expand All @@ -438,7 +440,7 @@ export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
addUrlForm.reset();
setAddUrlOpen(true);
}}
disabled={!nft.created_height}
disabled={isTransactionDisabled || !nft.created_height}
aria-label={t`Add URL to ${nftName}`}
>
<LinkIcon className='mr-2 h-4 w-4' aria-hidden='true' />
Expand All @@ -453,7 +455,7 @@ export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
e.stopPropagation();
setBurnOpen(true);
}}
disabled={!nft.created_height}
disabled={isTransactionDisabled || !nft.created_height}
aria-label={t`Burn ${nftName}`}
>
<Flame className='mr-2 h-4 w-4' aria-hidden='true' />
Expand Down Expand Up @@ -484,6 +486,7 @@ export function NftCard({ nft, updateNfts, selectionState }: NftCardProps) {
});
}}
disabled={
isTransactionDisabled ||
!nft.created_height ||
offerState.offered.nfts.findIndex(
(nftId) => nftId === nft.launcher_id,
Expand Down
Loading
Loading