import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Modal, Animated, StyleSheet, KeyboardAvoidingView, Platform, } from 'react-native'; import { useDubsTheme } from '../theme'; import { useDubs } from '../../provider'; import { useCreateCustomGame } from '../../hooks/useCreateCustomGame'; import type { CreateCustomGameMutationResult } from '../../hooks/useCreateCustomGame'; export interface CreateCustomGameSheetProps { visible: boolean; onDismiss: () => void; title?: string; maxPlayers?: number; fee?: number; presetAmounts?: number[]; defaultAmount?: number; metadata?: Record; onAmountChange?: (amount: number | null) => void; onSuccess?: (result: CreateCustomGameMutationResult) => void; onError?: (error: Error) => void; /** Pool mode: hides buy-in selection, uses defaultAmount, auto-assigns team, shows "Create Pool" labels */ isPoolModeEnabled?: boolean; } const STATUS_LABELS: Record = { building: 'Building transaction...', signing: 'Approve in wallet...', confirming: 'Confirming...', success: 'Game Created!', }; export function CreateCustomGameSheet({ visible, onDismiss, title, maxPlayers = 2, fee = 5, presetAmounts = [0.01, 0.1, 0.5, 1], defaultAmount = 0.01, metadata, onAmountChange, onSuccess, onError, isPoolModeEnabled = false, }: CreateCustomGameSheetProps) { const t = useDubsTheme(); const { wallet } = useDubs(); const mutation = useCreateCustomGame(); const [selectedAmount, setSelectedAmount] = useState(null); const [customAmount, setCustomAmount] = useState(''); const [isCustom, setIsCustom] = useState(false); const overlayOpacity = useRef(new Animated.Value(0)).current; // Animate overlay on visibility change useEffect(() => { Animated.timing(overlayOpacity, { toValue: visible ? 1 : 0, duration: 250, useNativeDriver: true, }).start(); }, [visible, overlayOpacity]); // Reset state when sheet opens useEffect(() => { if (visible) { setSelectedAmount(defaultAmount ?? null); setCustomAmount(''); setIsCustom(false); mutation.reset(); } }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps // Auto-dismiss on success useEffect(() => { if (mutation.status === 'success' && mutation.data) { onSuccess?.(mutation.data); const timer = setTimeout(() => { onDismiss(); }, 1500); return () => clearTimeout(timer); } }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps // Report errors useEffect(() => { if (mutation.status === 'error' && mutation.error) { onError?.(mutation.error); } }, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps const handlePresetSelect = useCallback((amount: number) => { setSelectedAmount(amount); setIsCustom(false); setCustomAmount(''); onAmountChange?.(amount); }, [onAmountChange]); const handleCustomSelect = useCallback(() => { setIsCustom(true); setSelectedAmount(null); onAmountChange?.(null); }, [onAmountChange]); const handleCustomAmountChange = useCallback((text: string) => { // Allow only numbers and single decimal point const cleaned = text.replace(/[^0-9.]/g, '').replace(/(\..*?)\..*/g, '$1'); setCustomAmount(cleaned); const parsed = parseFloat(cleaned); const amount = parsed > 0 ? parsed : null; setSelectedAmount(amount); onAmountChange?.(amount); }, [onAmountChange]); const effectiveAmount = selectedAmount; const playerCount = maxPlayers || 2; // In pool mode, always use defaultAmount const finalAmount = isPoolModeEnabled ? (defaultAmount ?? 0.1) : effectiveAmount; const pot = finalAmount ? finalAmount * playerCount : 0; const winnerTakes = pot * (1 - fee / 100); const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error'; const canCreate = finalAmount !== null && finalAmount > 0 && !isMutating && mutation.status !== 'success'; const handleCreate = useCallback(async () => { if (!finalAmount || !wallet.publicKey) return; try { await mutation.execute({ playerWallet: wallet.publicKey.toBase58(), teamChoice: 'home', wagerAmount: finalAmount, title, maxPlayers, metadata, }); } catch { // Error is already captured in mutation state } }, [finalAmount, wallet.publicKey, mutation.execute, title, maxPlayers, metadata]); // eslint-disable-line react-hooks/exhaustive-deps const statusLabel = STATUS_LABELS[mutation.status] || ''; const playersLabel = playerCount === 2 ? '2 (1v1)' : `${playerCount} players`; return ( {/* Drag handle */} {/* Header */} {isPoolModeEnabled ? 'Create Pool' : 'New Game'} {'\u2715'} {/* Buy-In Section — hidden in pool mode (uses defaultAmount) */} {!isPoolModeEnabled && ( Buy-In Amount {presetAmounts.map((amount) => { const active = !isCustom && selectedAmount === amount; return ( handlePresetSelect(amount)} activeOpacity={0.8} > {amount} SOL ); })} Custom {isCustom && ( )} )} {/* Summary Card */} Buy-in {finalAmount ? `${finalAmount} SOL` : '—'} {isPoolModeEnabled ? 'Max players' : 'Players'} {isPoolModeEnabled ? playerCount : playersLabel} {isPoolModeEnabled ? 'Max pot' : 'Winner Takes'} {finalAmount ? `${(finalAmount * playerCount * (1 - fee / 100)).toFixed(4)} SOL` : '—'} {finalAmount ? ( {fee}% platform fee ) : null} {/* Error Display */} {mutation.error && ( {mutation.error.message} )} {/* CTA Button */} {isMutating ? ( {statusLabel} ) : mutation.status === 'success' ? ( {isPoolModeEnabled ? 'Pool Created!' : STATUS_LABELS.success} ) : ( {isPoolModeEnabled ? `Create Pool \u2014 ${finalAmount} SOL` : effectiveAmount ? `Create Game \u2014 ${effectiveAmount} SOL` : 'Select buy-in amount'} )} ); } const styles = StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', }, overlayTap: { flex: 1, }, keyboardView: { flex: 1, justifyContent: 'flex-end', }, sheetPositioner: { justifyContent: 'flex-end', }, sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingHorizontal: 20, paddingBottom: 40, }, handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 8, }, handle: { width: 36, height: 4, borderRadius: 2, opacity: 0.4, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, }, headerTitle: { fontSize: 20, fontWeight: '700', }, closeButton: { fontSize: 20, padding: 4, }, section: { gap: 12, paddingTop: 8, }, sectionLabel: { fontSize: 15, fontWeight: '600', }, chipsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, }, chip: { paddingHorizontal: 18, paddingVertical: 10, borderRadius: 14, borderWidth: 1.5, }, chipText: { fontSize: 15, fontWeight: '600', }, input: { height: 56, borderRadius: 16, borderWidth: 1.5, paddingHorizontal: 16, fontSize: 16, }, summaryCard: { marginTop: 20, borderRadius: 16, borderWidth: 1, overflow: 'hidden', }, summaryRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 14, }, summaryLabel: { fontSize: 14, }, summaryValue: { fontSize: 15, fontWeight: '700', }, summarySep: { height: 1, marginHorizontal: 16, }, winnerCol: { alignItems: 'flex-end', gap: 2, }, feeNote: { fontSize: 11, }, errorBox: { marginTop: 16, borderRadius: 12, borderWidth: 1, padding: 12, }, errorText: { fontSize: 13, fontWeight: '500', }, ctaButton: { marginTop: 20, height: 56, borderRadius: 14, justifyContent: 'center', alignItems: 'center', }, ctaText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700', }, ctaLoading: { flexDirection: 'row', alignItems: 'center', gap: 10, }, });