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
23 changes: 17 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { clearSession, getSession, setSession } from '@/lib/session'
import { setIntervalDebounced } from '@/lib/utils'
import { Logs } from './components/Logs'
import { Receive } from './components/receive/Receive'
import Send from './components/send/Send'
import { RescanChain } from './components/settings/RescanChain'
import { Settings } from './components/settings/Settings'

Expand Down Expand Up @@ -65,31 +66,41 @@ function App() {
}
/>
<Route
path="/settings"
path="/send"
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<Settings walletFileName={walletFileName} />
<Send />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/logs"
path="/settings/rescan"
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<Logs />
<RescanChain walletFileName={walletFileName} />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/settings/rescan"
path="/settings"
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<RescanChain walletFileName={walletFileName} />
<Settings walletFileName={walletFileName} />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/logs"
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<Logs />
</Layout>
</ProtectedRoute>
}
Expand Down
17 changes: 8 additions & 9 deletions src/components/DisplayLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ export function DisplayLogo({ displayMode, size = 'lg' }: DisplayLogoProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size === 'sm' ? '16px' : '30px'}
height={size === 'sm' ? '18px' : '40px'}
viewBox="0 0 24 24"
width={size === 'sm' ? '18px' : '30px'}
height={size === 'sm' ? '20px' : '40px'}
viewBox="0 0 24 28"
fill="none"
style={{
display: 'inline',
verticalAlign: 'middle',
}}
>
<path d="M7 7.90906H17" stroke="currentColor" />
<path d="M12 5.45454V3" stroke="currentColor" />
<path d="M12 20.9999V18.5454" stroke="currentColor" />
<path d="M7 12H17" stroke="currentColor" />
<path d="M7 16.0909H17" stroke="currentColor" />
<path d="M7 7.90906H17" stroke="currentColor" strokeWidth={2} />
<path d="M12 5.45454V3" stroke="currentColor" strokeWidth={2} />
<path d="M12 20.9999V18.5454" stroke="currentColor" strokeWidth={2} />
<path d="M7 12H17" stroke="currentColor" strokeWidth={2} />
<path d="M7 16.0909H17" stroke="currentColor" strokeWidth={2} />
</svg>
)
}
9 changes: 7 additions & 2 deletions src/components/receive/BitcoinAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ export const BitcoinAmountInput = ({
{...inputProps}
/>
</div>
<Button variant="outline" size="sm" className="py-4 whitespace-nowrap" onClick={toggleDisplayMode}>
{amountDisplayMode === 'sats' ? 'BTC' : 'Sats'}
<Button
variant="outline"
size="sm"
className="flex w-18 items-center justify-between py-4.5 whitespace-nowrap"
onClick={toggleDisplayMode}
>
<span>{amountDisplayMode === 'sats' ? 'BTC' : 'Sats'}</span>
<ArrowUpDown />
</Button>
</div>
Expand Down
148 changes: 148 additions & 0 deletions src/components/send/JarSelectorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useApiClient } from '../../hooks/useApiClient'
import { getaddressOptions } from '../../lib/jm-api/generated/client/@tanstack/react-query.gen'
import { getSession } from '../../lib/session'
import { SelectableJar } from '../ui/SelectableJar'
import { Button } from '../ui/button'

interface JarSelectorModalProps {
jars: Array<{ name: string; color: string; balance: number; account?: string }>
totalBalance: number
selectedSendingJar: string
onJarSelect: (address: string) => void
onClose: () => void
}

const JarSelectorModal = ({ jars, totalBalance, selectedSendingJar, onJarSelect, onClose }: JarSelectorModalProps) => {
const { t } = useTranslation()
const client = useApiClient()
const session = getSession()
const walletFileName = session?.walletFileName
const [selectedJarForAddress, setSelectedJarForAddress] = useState<string | null>(null)
const [localSelectedJar, setLocalSelectedJar] = useState<{ name: string; account?: string } | null>(null)

// Query to get address for selected jar (only when confirming)
const addressQuery = useQuery({
...getaddressOptions({
client,
path: {
walletname: walletFileName || '',
mixdepth: selectedJarForAddress || '0',
},
}),
enabled: !!walletFileName && !!selectedJarForAddress,
retry: false,
})

// When address is received, call onJarSelect
useEffect(() => {
if (addressQuery.data?.address) {
onJarSelect(addressQuery.data.address)
setSelectedJarForAddress(null) // Reset selection
setLocalSelectedJar(null) // Reset local selection
}
}, [addressQuery.data, onJarSelect])

// Handle query errors
useEffect(() => {
if (addressQuery.error) {
console.error('Failed to get address for jar:', addressQuery.error)
setSelectedJarForAddress(null) // Reset on error
}
}, [addressQuery.error])

const handleClose = () => {
setSelectedJarForAddress(null)
setLocalSelectedJar(null) // Reset local selection
onClose()
}

const handleJarClick = (jar: { name: string; account?: string }) => {
if (!walletFileName || !jar.account) return
if (jar.name === selectedSendingJar) return // Cannot select the sending jar

setLocalSelectedJar(jar) // Only set local selection, don't fetch address yet
}

const handleConfirm = () => {
if (localSelectedJar && localSelectedJar.account) {
setSelectedJarForAddress(localSelectedJar.account) // This triggers the address fetch
} else {
handleClose() // Close if no jar is selected
}
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
<div className="mx-4 w-full max-w-4xl rounded-lg bg-white p-6 dark:bg-[#2a2d35]">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-semibold text-black dark:text-white">
{t('send.select_jar_title', { defaultValue: 'Select a jar from your wallet to send the funds to.' })}
</h2>
<Button variant="ghost" size="icon" onClick={handleClose}>
<X className="h-4 w-4" />
</Button>
</div>

{/* Jars */}
<div className="mb-8 flex justify-center gap-8">
{jars.map((jar) => {
const isDisabled = jar.name === selectedSendingJar
const isLocallySelected = localSelectedJar?.name === jar.name
const isLoadingAddress = selectedJarForAddress === jar.account && addressQuery.isLoading

return (
<div key={jar.name} className="text-center">
<div>
<SelectableJar
name={jar.name}
color={jar.color}
balance={jar.balance}
totalBalance={totalBalance}
isSelected={false}
onClick={() => !isDisabled && handleJarClick(jar)}
disabled={isDisabled}
/>
</div>
<div className="mt-4">
<button
className={`h-6 w-6 rounded-full border-2 ${
isLoadingAddress
? 'border-blue-400 bg-blue-100'
: isLocallySelected
? 'border-blue-500 bg-blue-500'
: isDisabled
? 'cursor-not-allowed border-gray-300 bg-gray-200'
: 'cursor-pointer border-gray-400 bg-white hover:bg-gray-50'
} ${isLoadingAddress ? 'animate-pulse' : ''}`}
onClick={() => !isDisabled && handleJarClick(jar)}
disabled={isDisabled || isLoadingAddress}
/>
</div>
</div>
)
})}
</div>

{/* Buttons */}
<div className="flex gap-4">
<Button variant="outline" className="flex-1" onClick={handleClose}>
<X className="mr-2 h-4 w-4" />
{t('send.cancel', { defaultValue: 'Cancel' })}
</Button>
<Button className="flex-1" onClick={handleConfirm} disabled={!localSelectedJar || addressQuery.isLoading}>
{addressQuery.isLoading
? t('send.getting_address', { defaultValue: 'Getting address...' })
: t('send.confirm', { defaultValue: 'Confirm' })}
</Button>
</div>
</div>
</div>
)
}

export default JarSelectorModal
Loading