Skip to content
Draft
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
56 changes: 42 additions & 14 deletions src/components/layout/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { useContext, useEffect, useState } from 'react'
import { FilecoinPinContext } from '../../context/filecoin-pin-provider.tsx'
import { useDatasetPieces } from '../../hooks/use-dataset-pieces.ts'
import { useFilecoinUpload } from '../../hooks/use-filecoin-upload.ts'
import { useWallet } from '../../hooks/use-wallet.ts'
import { formatFileSize } from '../../utils/format-file-size.ts'
import { ButtonBase } from '../ui/button/button-base.tsx'
import { Heading } from '../ui/heading.tsx'
import { LoadingState } from '../ui/loading-state.tsx'
import { PageTitle } from '../ui/page-title.tsx'
Expand All @@ -27,6 +29,7 @@ export default function Content() {
const [dragDropKey, setDragDropKey] = useState(0) // Key to force DragNDrop remount
const { uploadState, uploadFile, resetUpload } = useFilecoinUpload()
const { pieces: uploadHistory, refreshPieces, isLoading: isLoadingPieces } = useDatasetPieces()
const { connect: connectWallet, isUsingWallet } = useWallet()
const context = useContext(FilecoinPinContext)
if (!context) {
throw new Error('Content must be used within FilecoinPinProvider')
Expand All @@ -36,11 +39,11 @@ export default function Content() {

// Determine if we're still initializing (wallet, synapse, provider)
// Note: We don't block on isLoadingPieces - users can upload while history loads
const isInitializing = wallet.status === 'loading' || wallet.status === 'idle'
const isInitializing = wallet.status === 'loading'

// Get loading message based on current state
const getLoadingMessage = () => {
if (wallet.status === 'loading' || wallet.status === 'idle') {
if (wallet.status === 'loading') {
return 'Connecting to Filecoin network...'
}
if (!synapse) {
Expand All @@ -52,18 +55,6 @@ export default function Content() {
return 'Preparing upload interface...'
}

// If wallet failed to load, show error instead of spinner
if (wallet.status === 'error') {
return (
<div className="content">
<PageTitle />
<div className="error-message">
<p>Failed to connect to Filecoin network: {wallet.error}</p>
</div>
</div>
)
}

const handleUpload = (file: File) => {
// Set uploadedFile immediately to switch to progress view
setUploadedFile({ file, cid: '' })
Expand Down Expand Up @@ -111,6 +102,43 @@ export default function Content() {
}
}, [uploadState.error, resetUpload])

// If wallet is disconnected (idle) and using wallet mode (no private key), show connect button
if (wallet.status === 'idle' && isUsingWallet) {
return (
<div className="content">
<PageTitle />
<div className="space-y-6 max-w-md mx-auto text-center">
<div className="space-y-2">
<Heading tag="h2">Connect Your Wallet</Heading>
<p className="text-zinc-400">Connect your wallet to start uploading files to Filecoin</p>
</div>
<ButtonBase onClick={connectWallet} variant="primary">
Connect Wallet
</ButtonBase>
</div>
</div>
)
}

// If wallet failed to load, show error instead of spinner
if (wallet.status === 'error') {
return (
<div className="content">
<PageTitle />
<div className="space-y-6 max-w-md mx-auto text-center">
<div className="p-4 rounded-lg bg-zinc-900 border border-zinc-700">
<p className="text-red-400">Failed to connect to Filecoin network: {wallet.error}</p>
</div>
{isUsingWallet && (
<ButtonBase onClick={connectWallet} variant="secondary">
Try Again
</ButtonBase>
)}
</div>
</div>
)
}

return (
<div className="space-y-10">
<PageTitle />
Expand Down
8 changes: 6 additions & 2 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PillBalance } from '../ui/pill/pill-balance.tsx'
import { PillWallet } from '../ui/pill/pill-wallet.tsx'

export default function Header() {
const { status, balances, address, network } = useWallet()
const { status, balances, address, network, disconnect, isUsingWallet } = useWallet()

const isCalibration = network === 'calibration'
const filLabel: FilLabel = isCalibration ? CALIBRATION_LABEL_FIL : MAINNET_LABEL_FIL
Expand Down Expand Up @@ -45,7 +45,11 @@ export default function Header() {
]}
/>
)}
<PillWallet address={addressDisplay} href={`https://filscan.io/en/address/${address}`} />
<PillWallet
address={addressDisplay}
href={`https://filscan.io/en/address/${address}`}
onDisconnect={isUsingWallet ? disconnect : undefined}
/>
</div>
</header>
)
Expand Down
23 changes: 21 additions & 2 deletions src/components/ui/pill/pill-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { LogOut } from 'lucide-react'
import { PillWrapper } from './pill-wrapper.tsx'

type PillWalletProps = {
address: string
href: string
onDisconnect?: () => void
}

function PillWallet({ address, href }: PillWalletProps) {
function PillWallet({ address, href, onDisconnect }: PillWalletProps) {
return (
<PillWrapper ariaLabel={`Wallet address: ${address}`} href={href}>
<span>{address}</span>
<div className="flex items-center gap-2">
<span>{address}</span>
{onDisconnect && (
<button
aria-label="Disconnect wallet"
className="relative z-10 ml-1 text-zinc-400 hover:text-zinc-100 transition-colors"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onDisconnect()
}}
title="Disconnect wallet"
type="button"
>
<LogOut size={14} />
</button>
)}
</div>
</PillWrapper>
)
}
Expand Down
64 changes: 50 additions & 14 deletions src/context/filecoin-pin-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { SynapseService } from 'filecoin-pin/core/synapse'
import { createContext, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { type DataSetState, useDataSetManager } from '../hooks/use-data-set-manager.ts'
import { filecoinPinConfig } from '../lib/filecoin-pin/config.ts'
import { getSynapseClient } from '../lib/filecoin-pin/synapse.ts'
import { disconnectWallet as disconnectSynapseWallet, getSynapseClient } from '../lib/filecoin-pin/synapse.ts'
import { fetchWalletSnapshot, type WalletSnapshot } from '../lib/filecoin-pin/wallet.ts'

type ProviderInfo = NonNullable<ReturnType<typeof useDataSetManager>['providerInfo']>
Expand All @@ -17,6 +17,9 @@ type WalletState =
export interface FilecoinPinContextValue {
wallet: WalletState
refreshWallet: () => Promise<void>
connectWallet: () => Promise<void>
disconnectWallet: () => void
isUsingWallet: boolean // true if using browser wallet, false if using private key
synapse: SynapseService['synapse'] | null
dataSet: DataSetState
ensureDataSet: () => Promise<number | null>
Expand All @@ -38,45 +41,65 @@ export const FilecoinPinProvider = ({ children }: { children: ReactNode }) => {
const synapseRef = useRef<SynapseService['synapse'] | null>(null)
const config = filecoinPinConfig

// Check if using wallet connection (no private key) vs private key
const isUsingWallet = !config.privateKey

// Use the data set manager hook
const { dataSet, ensureDataSet, storageContext, providerInfo } = useDataSetManager({
synapse: synapseRef.current,
walletAddress: wallet.status === 'ready' ? wallet.data.address : null,
})

const refreshWallet = useCallback(async () => {
if (!config.privateKey) {
setWallet((prev) => ({
status: 'error',
error: 'Missing VITE_FILECOIN_PRIVATE_KEY environment variable. Wallet data unavailable.',
data: prev.data,
}))
return
}

const connectWallet = useCallback(async () => {
setWallet((prev) => ({
status: 'loading',
data: prev.status === 'ready' ? prev.data : undefined,
}))

try {
// getSynapseClient now handles the fallback logic:
// 1. Try private key if VITE_FILECOIN_PRIVATE_KEY is set
// 2. Otherwise, attempt to connect to browser wallet (MetaMask, etc.)
const synapse = await getSynapseClient(config)
synapseRef.current = synapse
const snapshot = await fetchWalletSnapshot(synapse)

setWallet({
status: 'ready',
data: snapshot,
})
} catch (error) {
console.error('Failed to load wallet balances', error)
console.error('Failed to load wallet', error)
const errorMessage = error instanceof Error ? error.message : 'Unable to load wallet. See console for details.'
setWallet((prev) => ({
status: 'error',
error: error instanceof Error ? error.message : 'Unable to load wallet balances. See console for details.',
error: errorMessage,
data: prev.data,
}))
}
}, [config])

const refreshWallet = useCallback(async () => {
// Only auto-connect if using private key
// For wallet mode, user must explicitly click "Connect Wallet"
if (isUsingWallet) {
console.info('Wallet mode detected, waiting for user to click Connect Wallet button')
setWallet({
status: 'idle',
})
return
}

// Auto-connect with private key
await connectWallet()
}, [connectWallet, isUsingWallet])

const disconnectWallet = useCallback(async () => {
await disconnectSynapseWallet()
synapseRef.current = null
setWallet({ status: 'idle' })
}, [])

useEffect(() => {
void refreshWallet()
}, [refreshWallet])
Expand Down Expand Up @@ -104,13 +127,26 @@ export const FilecoinPinProvider = ({ children }: { children: ReactNode }) => {
() => ({
wallet,
refreshWallet,
connectWallet,
disconnectWallet,
isUsingWallet,
synapse: synapseRef.current,
dataSet,
ensureDataSet,
storageContext,
providerInfo,
}),
[wallet, refreshWallet, dataSet, ensureDataSet, storageContext, providerInfo]
[
wallet,
refreshWallet,
connectWallet,
disconnectWallet,
isUsingWallet,
dataSet,
ensureDataSet,
storageContext,
providerInfo,
]
)

return <FilecoinPinContext.Provider value={value}>{children}</FilecoinPinContext.Provider>
Expand Down
14 changes: 14 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ interface ImportMetaEnv {
readonly VITE_WARM_STORAGE_ADDRESS?: string
}

/**
* Ethereum provider types for MetaMask and other Web3 wallets
*/
interface EthereumProvider {
request: (args: { method: string; params?: any[] }) => Promise<any>
on: (event: string, handler: (...args: any[]) => void) => void
removeListener: (event: string, handler: (...args: any[]) => void) => void
isMetaMask?: boolean
}

/**
* We are delcaring a name on the global interface because this repo should not be consumed.
* Don't do this for libs, export proper types instead.
Expand All @@ -19,4 +29,8 @@ declare global {
interface ImportMeta {
readonly env: ImportMetaEnv
}

interface Window {
ethereum?: EthereumProvider
}
}
7 changes: 5 additions & 2 deletions src/hooks/use-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const useWallet = () => {
throw new Error('useWallet must be used within FilecoinPinProvider')
}

const { wallet, refreshWallet } = context
const { wallet, refreshWallet, connectWallet, disconnectWallet, isUsingWallet } = context

return useMemo(
() => ({
Expand All @@ -18,7 +18,10 @@ export const useWallet = () => {
raw: wallet.data?.raw,
error: wallet.status === 'error' ? wallet.error : undefined,
refresh: refreshWallet,
connect: connectWallet,
disconnect: disconnectWallet,
isUsingWallet, // true if using browser wallet, false if using private key
}),
[wallet, refreshWallet]
[wallet, refreshWallet, connectWallet, disconnectWallet, isUsingWallet]
)
}
9 changes: 3 additions & 6 deletions src/lib/filecoin-pin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ const normalizeEnvValue = (value: string | boolean | number | undefined) => {
return trimmed.length === 0 ? undefined : trimmed
}

if (!import.meta.env.VITE_FILECOIN_PRIVATE_KEY) {
throw new Error('Missing VITE_FILECOIN_PRIVATE_KEY; unable to initialize Synapse')
}

export const filecoinPinConfig: SynapseSetupConfig = {
privateKey: import.meta.env.VITE_FILECOIN_PRIVATE_KEY,
// Private key is now optional - will fall back to wallet connection if not provided
export const filecoinPinConfig: Partial<SynapseSetupConfig> = {
privateKey: normalizeEnvValue(import.meta.env.VITE_FILECOIN_PRIVATE_KEY),
rpcUrl: normalizeEnvValue(import.meta.env.VITE_FILECOIN_RPC_URL),
warmStorageAddress: normalizeEnvValue(import.meta.env.VITE_WARM_STORAGE_ADDRESS),
}
Loading