import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { StyleSheet, ActivityIndicator, TouchableOpacity, ScrollView, Platform, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { View, Text, useTheme, toast } from '@bettoredge/styles'; import { useCalcuttaAuction } from '../../hooks/useCalcuttaAuction'; import { useCalcuttaEscrow } from '../../hooks/useCalcuttaEscrow'; import { useCalcuttaBid } from '../../hooks/useCalcuttaBid'; import { useCalcuttaSocket } from '../../hooks/useCalcuttaSocket'; import { useCalcuttaCompetition } from '../../hooks/useCalcuttaCompetition'; import { useCalcuttaPlayers } from '../../hooks/useCalcuttaPlayers'; import { useCalcuttaItemImages } from '../../hooks/useCalcuttaItemImages'; import { deriveLifecycleState, canBid as canBidCheck, type CalcuttaLifecycleState } from '../../helpers/lifecycleState'; import { formatCurrency } from '../../helpers/formatting'; import { SealedBidHeader } from './SealedBidHeader'; import { SealedBidStatusBar } from './SealedBidStatusBar'; import { SealedBidTabBar, SealedBidTab } from './SealedBidTabBar'; import { SealedBidItemsTab } from './SealedBidItemsTab'; import { SealedBidMyBidsTab } from './SealedBidMyBidsTab'; import { SealedBidPlayersTab } from './SealedBidPlayersTab'; import { SealedBidInfoTab } from './SealedBidInfoTab'; import { EscrowBottomSheet } from './EscrowBottomSheet'; import { AuctionResultsModal } from './AuctionResultsModal'; import { CalcuttaRoundResults } from '../CalcuttaRoundResults'; import { CalcuttaLeaderboard } from '../CalcuttaLeaderboard'; interface SealedBidAuctionProps { calcutta_competition_id: string; player_id?: string; onClose?: () => void; access_token?: string; device_id?: string; getSocket?: () => WebSocket | null; getAccessToken?: () => string | undefined; getDeviceId?: () => string | undefined; player_data?: { username: string; profile_pic?: string }; player_balance?: number; onDepositFunds?: (amount: number) => void; onManage?: () => void; onJoin?: () => void | Promise; onLeave?: () => void | Promise; } /** Poll interval per lifecycle state */ function getPollInterval(state: CalcuttaLifecycleState, auctionExpired: boolean): number { // When timer expired but server hasn't confirmed close, poll aggressively if (auctionExpired && state === 'auctioning') return 3000; switch (state) { case 'auctioning': return 10000; case 'tournament': return 60000; default: return 0; } } export const SealedBidAuction: React.FC = ({ calcutta_competition_id, player_id, onClose, access_token, device_id, player_data, player_balance, onDepositFunds, onManage, onJoin, onLeave, }) => { const { theme } = useTheme(); const { width: windowWidth } = useWindowDimensions(); const [containerWidth, setContainerWidth] = useState(0); const measuredWidth = containerWidth || windowWidth; const isDesktop = Platform.OS === 'web' && measuredWidth >= 700; const [activeTab, setActiveTab] = useState('items'); const [escrowSheetVisible, setEscrowSheetVisible] = useState(false); const [refreshing, setRefreshing] = useState(false); const [showResultsModal, setShowResultsModal] = useState(false); // Client-side flag: auction end time has passed. // Does NOT change lifecycle state — only disables bid actions. // Lifecycle only changes when the SERVER confirms auction_status='closed'. const [auctionExpired, setAuctionExpired] = useState(false); // Track previous lifecycle state to detect transitions const prevLifecycleRef = useRef(undefined); const hasShownResultsRef = useRef(false); // Competition detail (rounds, participants, payout rules, item_results) const { rounds, participants, payout_rules, item_results, refresh: refreshCompetition, } = useCalcuttaCompetition(calcutta_competition_id); // Auction data (items, bids) const { loading, competition, items, my_bids, refresh: refreshAuction, } = useCalcuttaAuction(calcutta_competition_id, 10000, player_id); // Client-side check: has the auction end time passed? useEffect(() => { if (!competition?.auction_end_datetime) return; const end = new Date(competition.auction_end_datetime).getTime(); const now = Date.now(); if (now >= end) { setAuctionExpired(true); return; } // Set a timer to flip the flag the instant the deadline passes const delay = end - now; const timer = setTimeout(() => { setAuctionExpired(true); // Immediately poll the server for fresh status refreshAuction(); refreshCompetition(); }, delay); return () => clearTimeout(timer); }, [competition?.auction_end_datetime, refreshAuction, refreshCompetition]); // Derive lifecycle state — only from server data, no client override const lifecycleState = deriveLifecycleState( competition?.status, competition?.auction_status, ); // Bidding is disabled when lifecycle says no OR when the timer has expired const biddingDisabled = !canBidCheck(lifecycleState) || auctionExpired; // Escrow const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id); // Bid actions const { placeBid, cancelBid, loading: bidLoading } = useCalcuttaBid(); // Player enrichment const participantIds = useMemo( () => participants.map(p => p.player_id), [participants], ); const { players } = useCalcuttaPlayers(participantIds); // Item images (team logos / athlete photos) const { images: itemImages } = useCalcuttaItemImages(items); // Socket for presence const { connected: socketConnected, presence } = useCalcuttaSocket( calcutta_competition_id, access_token, device_id, player_data || { username: '' }, ); // Adaptive polling based on lifecycle state const pollInterval = getPollInterval(lifecycleState, auctionExpired); useEffect(() => { if (pollInterval <= 0) return; const id = setInterval(() => { refreshAuction(); refreshCompetition(); }, pollInterval); return () => clearInterval(id); }, [pollInterval, refreshAuction, refreshCompetition]); // Detect auctioning → tournament transition → show results modal useEffect(() => { if (!competition || hasShownResultsRef.current) return; const prev = prevLifecycleRef.current; const curr = lifecycleState; const justTransitioned = prev === 'auctioning' && (curr === 'tournament' || curr === 'completed'); if (justTransitioned) { setShowResultsModal(true); hasShownResultsRef.current = true; } prevLifecycleRef.current = curr; }, [lifecycleState, competition]); // Auto-switch tab when state changes useEffect(() => { const prev = prevLifecycleRef.current; if (!prev) return; if (lifecycleState === 'tournament' || lifecycleState === 'completed') { setActiveTab('my_items'); } else if (lifecycleState === 'auctioning') { setActiveTab('items'); } }, [lifecycleState]); // On first load, set the correct default tab for the current state const hasSetInitialTab = useRef(false); useEffect(() => { if (hasSetInitialTab.current || !competition) return; hasSetInitialTab.current = true; if (lifecycleState === 'tournament' || lifecycleState === 'completed') { setActiveTab('my_items'); } }, [lifecycleState, competition]); useEffect(() => { fetchEscrow(); }, [fetchEscrow]); const handlePlaceBid = useCallback(async (calcutta_auction_item_id: string, amount: number) => { if (biddingDisabled) return; try { await placeBid(calcutta_auction_item_id, amount); fetchEscrow(); refreshAuction(); toast.success('Bid placed'); } catch (e: any) { toast.error(e?.message || 'Failed to place bid'); } }, [placeBid, fetchEscrow, refreshAuction, biddingDisabled]); const handleCancelBid = useCallback(async (calcutta_bid_id: string) => { if (biddingDisabled) return; try { await cancelBid(calcutta_bid_id); fetchEscrow(); refreshAuction(); toast.success('Bid cancelled'); } catch (e: any) { toast.error(e?.message || 'Failed to cancel bid'); } }, [cancelBid, fetchEscrow, refreshAuction, biddingDisabled]); const handleUpdateBid = useCallback((calcutta_auction_item_id: string) => { if (biddingDisabled) return; setActiveTab('items'); }, [biddingDisabled]); const handleRefresh = useCallback(async () => { setRefreshing(true); await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]); setRefreshing(false); }, [refreshAuction, refreshCompetition, fetchEscrow]); const handleEscrowUpdate = useCallback(() => { fetchEscrow(); }, [fetchEscrow]); const handleResultsDismiss = useCallback(() => { setShowResultsModal(false); setActiveTab('my_items'); }, []); const [joining, setJoining] = useState(false); const handleJoin = useCallback(async () => { if (!onJoin) return; setJoining(true); try { await onJoin(); await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]); toast.success('Joined auction'); } catch (e: any) { toast.error(e?.message || 'Failed to join auction'); } setJoining(false); }, [onJoin, refreshAuction, refreshCompetition, fetchEscrow]); const [leaving, setLeaving] = useState(false); const handleLeave = useCallback(async () => { if (!onLeave) return; setLeaving(true); try { await onLeave(); await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]); toast.success('Left auction'); } catch (e: any) { toast.error(e?.message || 'Failed to leave auction'); } setLeaving(false); }, [onLeave, refreshAuction, refreshCompetition, fetchEscrow]); if (loading && !competition) { return ( ); } if (!competition) { return ( Auction not found ); } const isAdmin = player_id != null && competition.admin_id === player_id; const isParticipant = player_id != null && participants.some(p => p.player_id === player_id); // All items I purchased at auction (includes active + eliminated) const myPurchasedItems = (lifecycleState === 'tournament' || lifecycleState === 'completed') ? items.filter(i => i.winning_player_id === player_id) : []; // Only active (non-eliminated) for the results modal const wonItems = myPurchasedItems.filter(i => i.status === 'sold'); const myBidTabCount = (lifecycleState === 'tournament' || lifecycleState === 'completed') ? myPurchasedItems.length : my_bids.filter(b => b.bid_status === 'active').length; const statusBar = ( setEscrowSheetVisible(true)} onLeave={isParticipant && onLeave && lifecycleState === 'auctioning' ? handleLeave : undefined} leaving={leaving} /> ); return ( setContainerWidth(e.nativeEvent.layout.width)}> {isDesktop ? ( /* ═══ DESKTOP: all panels visible ═══ */ {statusBar} {/* Join banner */} {!isParticipant && onJoin && ( Join this auction {competition.entry_fee > 0 ? `${formatCurrency(competition.entry_fee, competition.market_type)} entry fee` : 'Free to enter'} {joining ? : Join} )} {/* Row 1: Items + My Bids */} {/* Row 2: Players + Info */} ) : ( /* ═══ MOBILE: tab system ═══ */ <> {!isParticipant && onJoin && ( Join this auction {competition.entry_fee > 0 ? `${formatCurrency(competition.entry_fee, competition.market_type)} entry fee` : 'Free to enter'} {joining ? : Join} )} {activeTab === 'items' && ( )} {activeTab === 'my_bids' && ( )} {activeTab === 'my_items' && ( )} {activeTab === 'players' && ( )} {activeTab === 'results' && ( )} {activeTab === 'leaderboard' && ( )} {activeTab === 'info' && ( )} )} setEscrowSheetVisible(false)} calcutta_competition_id={calcutta_competition_id} escrow={escrow} market_type={competition.market_type} max_escrow={competition.max_escrow} player_balance={player_balance} onDepositFunds={onDepositFunds} lifecycleState={lifecycleState} onUpdate={() => { handleEscrowUpdate(); setEscrowSheetVisible(false); }} /> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 40, }, joinBanner: { marginHorizontal: 12, marginTop: 10, padding: 14, borderRadius: 12, borderWidth: 1, }, joinInfo: { flexDirection: 'row', alignItems: 'center', }, joinBtn: { marginTop: 12, height: 44, borderRadius: 10, alignItems: 'center', justifyContent: 'center', }, tabContent: { flex: 1, }, desktopPanel: { borderRadius: 12, borderWidth: 1, padding: 8, }, });