test(database): fix the id mix up on local deploy
5 files changed, +254 -58
+3-2scripts/deploy.js
| @@ -160,8 +160,9 @@ async function seedDatabase(wallets) { | ||
| 160 | 160 | const propertyIds = []; |
| 161 | 161 | for (let i = 0; i < SAMPLE_PROPERTIES.length; i++) { |
| 162 | 162 | const p = SAMPLE_PROPERTIES[i]; |
| 163 | - const cityId = cities[i % cities.length].id; | |
| 164 | - const stateId = states[i % states.length].id; | |
| 163 | + const city = cities[i % cities.length]; | |
| 164 | + const cityId = city.id; | |
| 165 | + const stateId = city.state_id; // Use the city's actual state, not a separate cycle | |
| 165 | 166 | const typeId = propertyTypes[i % propertyTypes.length].id; |
| 166 | 167 | const slug = p.title.toLowerCase().replace(/\s+/g, '-'); |
| 167 | 168 |
+49-14src/hooks/useWallet.js
| @@ -11,6 +11,7 @@ import { | ||
| 11 | 11 | walletErrorAtom, |
| 12 | 12 | shortAddressAtom, |
| 13 | 13 | isCorrectNetworkAtom, |
| 14 | + manuallyDisconnectedAtom, | |
| 14 | 15 | } from '../store/wallet'; |
| 15 | 16 | |
| 16 | 17 | export function useWallet() { |
| @@ -23,12 +24,13 @@ export function useWallet() { | ||
| 23 | 24 | const [error, setError] = useAtom(walletErrorAtom); |
| 24 | 25 | const [shortAddress] = useAtom(shortAddressAtom); |
| 25 | 26 | const [isCorrectNetwork] = useAtom(isCorrectNetworkAtom); |
| 27 | + const [manuallyDisconnected, setManuallyDisconnected] = useAtom(manuallyDisconnectedAtom); | |
| 26 | 28 | |
| 27 | 29 | // Check if MetaMask is installed |
| 28 | 30 | const hasMetaMask = typeof window !== 'undefined' && Boolean(window.ethereum); |
| 29 | 31 | |
| 30 | 32 | // Connect wallet |
| 31 | - const connect = useCallback(async () => { | |
| 33 | + const connect = useCallback(async (forceAccountPicker = false) => { | |
| 32 | 34 | if (!hasMetaMask) { |
| 33 | 35 | setError('Please install MetaMask to connect'); |
| 34 | 36 | return; |
| @@ -40,8 +42,18 @@ export function useWallet() { | ||
| 40 | 42 | try { |
| 41 | 43 | const web3Provider = new ethers.providers.Web3Provider(window.ethereum); |
| 42 | 44 | |
| 43 | - // Request account access | |
| 44 | - const accounts = await window.ethereum.request({ | |
| 45 | + let accounts; | |
| 46 | + | |
| 47 | + if (forceAccountPicker) { | |
| 48 | + // Force MetaMask to show account picker by requesting permissions | |
| 49 | + await window.ethereum.request({ | |
| 50 | + method: 'wallet_requestPermissions', | |
| 51 | + params: [{ eth_accounts: {} }], | |
| 52 | + }); | |
| 53 | + } | |
| 54 | + | |
| 55 | + // Get the selected account | |
| 56 | + accounts = await window.ethereum.request({ | |
| 45 | 57 | method: 'eth_requestAccounts', |
| 46 | 58 | }); |
| 47 | 59 | |
| @@ -52,6 +64,10 @@ export function useWallet() { | ||
| 52 | 64 | setSigner(web3Signer); |
| 53 | 65 | setAddress(accounts[0]); |
| 54 | 66 | setChainId(network.chainId); |
| 67 | + | |
| 68 | + // Clear the manually disconnected flag | |
| 69 | + setManuallyDisconnected(false); | |
| 70 | + sessionStorage.removeItem('walletDisconnected'); | |
| 55 | 71 | } catch (err) { |
| 56 | 72 | console.error('Failed to connect wallet:', err); |
| 57 | 73 | if (err.code === 4001) { |
| @@ -62,7 +78,10 @@ export function useWallet() { | ||
| 62 | 78 | } finally { |
| 63 | 79 | setIsConnecting(false); |
| 64 | 80 | } |
| 65 | - }, [hasMetaMask, setAddress, setChainId, setError, setIsConnecting, setProvider, setSigner]); | |
| 81 | + }, [hasMetaMask, setAddress, setChainId, setError, setIsConnecting, setProvider, setSigner, setManuallyDisconnected]); | |
| 82 | + | |
| 83 | + // Connect with account picker (for user-initiated connects) | |
| 84 | + const connectWithPicker = useCallback(() => connect(true), [connect]); | |
| 66 | 85 | |
| 67 | 86 | // Disconnect wallet |
| 68 | 87 | const disconnect = useCallback(() => { |
| @@ -71,7 +90,11 @@ export function useWallet() { | ||
| 71 | 90 | setSigner(null); |
| 72 | 91 | setChainId(null); |
| 73 | 92 | setError(null); |
| 74 | - }, [setAddress, setChainId, setError, setProvider, setSigner]); | |
| 93 | + | |
| 94 | + // Set flag to prevent auto-reconnect | |
| 95 | + setManuallyDisconnected(true); | |
| 96 | + sessionStorage.setItem('walletDisconnected', 'true'); | |
| 97 | + }, [setAddress, setChainId, setError, setProvider, setSigner, setManuallyDisconnected]); | |
| 75 | 98 | |
| 76 | 99 | // Switch to Hardhat local network |
| 77 | 100 | const switchToHardhat = useCallback(async () => { |
| @@ -106,17 +129,28 @@ export function useWallet() { | ||
| 106 | 129 | useEffect(() => { |
| 107 | 130 | if (!hasMetaMask) return; |
| 108 | 131 | |
| 109 | - const handleAccountsChanged = (accounts) => { | |
| 132 | + const handleAccountsChanged = async (accounts) => { | |
| 133 | + // If user manually disconnected, don't auto-reconnect | |
| 134 | + if (manuallyDisconnected) return; | |
| 135 | + | |
| 110 | 136 | if (accounts.length === 0) { |
| 111 | 137 | disconnect(); |
| 112 | - } else { | |
| 138 | + } else if (isConnected) { | |
| 139 | + // Only update if already connected (user switched accounts in MetaMask) | |
| 140 | + const web3Provider = new ethers.providers.Web3Provider(window.ethereum); | |
| 141 | + const web3Signer = web3Provider.getSigner(); | |
| 142 | + | |
| 143 | + setProvider(web3Provider); | |
| 144 | + setSigner(web3Signer); | |
| 113 | 145 | setAddress(accounts[0]); |
| 114 | 146 | } |
| 115 | 147 | }; |
| 116 | 148 | |
| 117 | - const handleChainChanged = (chainIdHex) => { | |
| 118 | - const newChainId = parseInt(chainIdHex, 16); | |
| 119 | - setChainId(newChainId); | |
| 149 | + const handleChainChanged = () => { | |
| 150 | + // Only reload if connected | |
| 151 | + if (isConnected) { | |
| 152 | + window.location.reload(); | |
| 153 | + } | |
| 120 | 154 | }; |
| 121 | 155 | |
| 122 | 156 | window.ethereum.on('accountsChanged', handleAccountsChanged); |
| @@ -126,11 +160,11 @@ export function useWallet() { | ||
| 126 | 160 | window.ethereum.removeListener('accountsChanged', handleAccountsChanged); |
| 127 | 161 | window.ethereum.removeListener('chainChanged', handleChainChanged); |
| 128 | 162 | }; |
| 129 | - }, [hasMetaMask, disconnect, setAddress, setChainId]); | |
| 163 | + }, [hasMetaMask, disconnect, setAddress, setProvider, setSigner, isConnected, manuallyDisconnected]); | |
| 130 | 164 | |
| 131 | - // Auto-connect if already authorized | |
| 165 | + // Auto-connect if already authorized (but not if manually disconnected) | |
| 132 | 166 | useEffect(() => { |
| 133 | - if (!hasMetaMask || isConnected) return; | |
| 167 | + if (!hasMetaMask || isConnected || manuallyDisconnected) return; | |
| 134 | 168 | |
| 135 | 169 | window.ethereum |
| 136 | 170 | .request({ method: 'eth_accounts' }) |
| @@ -140,7 +174,7 @@ export function useWallet() { | ||
| 140 | 174 | } |
| 141 | 175 | }) |
| 142 | 176 | .catch(console.error); |
| 143 | - }, [hasMetaMask, isConnected, connect]); | |
| 177 | + }, [hasMetaMask, isConnected, connect, manuallyDisconnected]); | |
| 144 | 178 | |
| 145 | 179 | return { |
| 146 | 180 | // State |
| @@ -156,6 +190,7 @@ export function useWallet() { | ||
| 156 | 190 | hasMetaMask, |
| 157 | 191 | // Actions |
| 158 | 192 | connect, |
| 193 | + connectWithPicker, | |
| 159 | 194 | disconnect, |
| 160 | 195 | switchToHardhat, |
| 161 | 196 | }; |
+87-27src/pages/PropertyDetail.jsx
| @@ -21,7 +21,7 @@ const ContractStates = { | ||
| 21 | 21 | |
| 22 | 22 | function PropertyDetail() { |
| 23 | 23 | const { id } = useParams(); // This is actually the slug from the URL |
| 24 | - const { connect, isConnected, isConnecting, shortAddress, address } = useWallet(); | |
| 24 | + const { connect, connectWithPicker, isConnected, isConnecting, shortAddress, address } = useWallet(); | |
| 25 | 25 | const { signAsBuyerWithDeposit, signAsSeller, loading: contractLoading, error: contractError } = useContracts(); |
| 26 | 26 | |
| 27 | 27 | const [property, setProperty] = useState(null); |
| @@ -312,43 +312,91 @@ function PropertyDetail() { | ||
| 312 | 312 | </div> |
| 313 | 313 | </div> |
| 314 | 314 | |
| 315 | - {/* Investment Input */} | |
| 316 | - {contractStateInfo?.canInvest && ( | |
| 315 | + {/* Investment Section - Always visible with state-appropriate messaging */} | |
| 316 | + {property.contractState && ( | |
| 317 | 317 | <div className="mb-4"> |
| 318 | - <label className="block text-sm font-medium text-secondary-700 mb-2"> | |
| 319 | - Investment Amount (ETH) | |
| 320 | - </label> | |
| 321 | - <div className="flex space-x-2"> | |
| 322 | - <input | |
| 323 | - type="number" | |
| 324 | - step="0.01" | |
| 325 | - min={minDeposit} | |
| 326 | - max={maxDeposit} | |
| 327 | - value={investAmount} | |
| 328 | - onChange={(e) => setInvestAmount(e.target.value)} | |
| 329 | - placeholder={`Min: ${minDeposit.toFixed(2)} ETH`} | |
| 330 | - className="input flex-1" | |
| 331 | - /> | |
| 332 | - </div> | |
| 333 | - <p className="text-xs text-secondary-500 mt-1"> | |
| 334 | - Deposit range: {minDeposit.toFixed(2)} - {maxDeposit.toFixed(2)} ETH (10-100% of price) | |
| 335 | - </p> | |
| 318 | + {/* State 0: Waiting for Seller */} | |
| 319 | + {property.contractState.state === 0 && ( | |
| 320 | + <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4"> | |
| 321 | + <p className="text-sm font-medium text-yellow-800 mb-2">Awaiting Seller Signature</p> | |
| 322 | + <p className="text-xs text-yellow-700"> | |
| 323 | + The seller must sign this contract before investments can be made. | |
| 324 | + </p> | |
| 325 | + <p className="text-xs text-yellow-600 mt-2 font-mono"> | |
| 326 | + Seller: {property.contractState.seller?.slice(0, 10)}...{property.contractState.seller?.slice(-8)} | |
| 327 | + </p> | |
| 328 | + </div> | |
| 329 | + )} | |
| 330 | + | |
| 331 | + {/* State 1: Can Invest */} | |
| 332 | + {property.contractState.state === 1 && ( | |
| 333 | + <> | |
| 334 | + <label className="block text-sm font-medium text-secondary-700 mb-2"> | |
| 335 | + Investment Amount (ETH) | |
| 336 | + </label> | |
| 337 | + <div className="flex space-x-2"> | |
| 338 | + <input | |
| 339 | + type="number" | |
| 340 | + step="0.01" | |
| 341 | + min={minDeposit} | |
| 342 | + max={maxDeposit} | |
| 343 | + value={investAmount} | |
| 344 | + onChange={(e) => setInvestAmount(e.target.value)} | |
| 345 | + placeholder={`Min: ${minDeposit.toFixed(2)} ETH`} | |
| 346 | + className="input flex-1" | |
| 347 | + /> | |
| 348 | + </div> | |
| 349 | + <p className="text-xs text-secondary-500 mt-1"> | |
| 350 | + Deposit range: {minDeposit.toFixed(2)} - {maxDeposit.toFixed(2)} ETH (10-100% of price) | |
| 351 | + </p> | |
| 352 | + </> | |
| 353 | + )} | |
| 354 | + | |
| 355 | + {/* State 2+: Transaction in progress */} | |
| 356 | + {property.contractState.state >= 2 && property.contractState.state <= 3 && ( | |
| 357 | + <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4"> | |
| 358 | + <p className="text-sm font-medium text-blue-800 mb-2">{contractStateInfo?.label}</p> | |
| 359 | + <p className="text-xs text-blue-700"> | |
| 360 | + This transaction is in progress. No new investments can be made. | |
| 361 | + </p> | |
| 362 | + </div> | |
| 363 | + )} | |
| 364 | + | |
| 365 | + {/* State 4: Completed */} | |
| 366 | + {property.contractState.state === 4 && ( | |
| 367 | + <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4"> | |
| 368 | + <p className="text-sm font-medium text-green-800 mb-2">Transaction Complete</p> | |
| 369 | + <p className="text-xs text-green-700"> | |
| 370 | + This property has been successfully sold. | |
| 371 | + </p> | |
| 372 | + </div> | |
| 373 | + )} | |
| 374 | + | |
| 375 | + {/* State 5: Rejected */} | |
| 376 | + {property.contractState.state === 5 && ( | |
| 377 | + <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4"> | |
| 378 | + <p className="text-sm font-medium text-red-800 mb-2">Transaction Rejected</p> | |
| 379 | + <p className="text-xs text-red-700"> | |
| 380 | + This transaction was cancelled or rejected. | |
| 381 | + </p> | |
| 382 | + </div> | |
| 383 | + )} | |
| 336 | 384 | </div> |
| 337 | 385 | )} |
| 338 | 386 | |
| 339 | 387 | {/* Action Buttons based on state and role */} |
| 340 | 388 | {!isConnected ? ( |
| 341 | 389 | <button |
| 342 | - onClick={connect} | |
| 390 | + onClick={connectWithPicker} | |
| 343 | 391 | disabled={isConnecting} |
| 344 | 392 | className="btn w-full mb-4 flex items-center justify-center" |
| 345 | 393 | > |
| 346 | 394 | <FaWallet className="mr-2" /> |
| 347 | - {isConnecting ? 'Connecting...' : 'Connect Wallet'} | |
| 395 | + {isConnecting ? 'Connecting...' : 'Connect Wallet to Invest'} | |
| 348 | 396 | </button> |
| 349 | 397 | ) : ( |
| 350 | 398 | <> |
| 351 | - {/* Seller Sign Button */} | |
| 399 | + {/* Seller Sign Button - Show to seller in state 0 */} | |
| 352 | 400 | {userRole === 'seller' && property.contractState?.state === 0 && ( |
| 353 | 401 | <button |
| 354 | 402 | onClick={handleSellerSign} |
| @@ -360,8 +408,17 @@ function PropertyDetail() { | ||
| 360 | 408 | </button> |
| 361 | 409 | )} |
| 362 | 410 | |
| 363 | - {/* Buyer Invest Button */} | |
| 364 | - {contractStateInfo?.canInvest && ( | |
| 411 | + {/* Show info for non-sellers in state 0 */} | |
| 412 | + {userRole !== 'seller' && property.contractState?.state === 0 && ( | |
| 413 | + <div className="bg-secondary-100 rounded-lg p-3 mb-4 text-center"> | |
| 414 | + <p className="text-sm text-secondary-600"> | |
| 415 | + Waiting for seller to sign before you can invest | |
| 416 | + </p> | |
| 417 | + </div> | |
| 418 | + )} | |
| 419 | + | |
| 420 | + {/* Buyer Invest Button - Only in state 1 */} | |
| 421 | + {property.contractState?.state === 1 && ( | |
| 365 | 422 | <button |
| 366 | 423 | onClick={handleInvest} |
| 367 | 424 | disabled={contractLoading || !investAmount} |
| @@ -379,10 +436,13 @@ function PropertyDetail() { | ||
| 379 | 436 | </> |
| 380 | 437 | )} |
| 381 | 438 | |
| 382 | - {/* Contract State Info */} | |
| 439 | + {/* Contract State Badge */} | |
| 383 | 440 | {contractStateInfo && ( |
| 384 | 441 | <div className={`p-3 rounded-lg bg-${contractStateInfo.color}-50 border border-${contractStateInfo.color}-200 mb-4`}> |
| 385 | 442 | <p className="text-sm font-medium text-center">{contractStateInfo.label}</p> |
| 443 | + {userRole && ( | |
| 444 | + <p className="text-xs text-center text-secondary-600 mt-1">You are the {userRole}</p> | |
| 445 | + )} | |
| 386 | 446 | </div> |
| 387 | 447 | )} |
| 388 | 448 |
+5-0src/store/wallet.js
| @@ -10,6 +10,11 @@ export const chainIdAtom = atom(null); | ||
| 10 | 10 | export const isConnectingAtom = atom(false); |
| 11 | 11 | export const isConnectedAtom = atom((get) => get(addressAtom) !== null); |
| 12 | 12 | |
| 13 | +// Track if user manually disconnected (persists to prevent auto-reconnect) | |
| 14 | +export const manuallyDisconnectedAtom = atom( | |
| 15 | + sessionStorage.getItem('walletDisconnected') === 'true' | |
| 16 | +); | |
| 17 | + | |
| 13 | 18 | // Error state |
| 14 | 19 | export const walletErrorAtom = atom(null); |
| 15 | 20 |