import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Modal, Animated, StyleSheet, KeyboardAvoidingView, Platform, } from 'react-native'; import { useDubsTheme } from '../theme'; import { useDubs } from '../../provider'; import { useClaim } from '../../hooks/useClaim'; import type { ClaimMutationResult } from '../../hooks/useClaim'; export interface ClaimPrizeSheetProps { visible: boolean; onDismiss: () => void; gameId: string; /** Prize amount in SOL */ prizeAmount: number; /** When true, shows refund language instead of prize language */ isRefund?: boolean; /** Callbacks */ onSuccess?: (result: ClaimMutationResult) => void; onError?: (error: Error) => void; } const STATUS_LABELS: Record = { building: 'Building transaction...', signing: 'Approve in wallet...', confirming: 'Confirming...', success: 'Claimed!', }; export function ClaimPrizeSheet({ visible, onDismiss, gameId, prizeAmount, isRefund = false, onSuccess, onError, }: ClaimPrizeSheetProps) { const t = useDubsTheme(); const { wallet } = useDubs(); const mutation = useClaim(); const overlayOpacity = useRef(new Animated.Value(0)).current; const celebrationScale = useRef(new Animated.Value(0)).current; const celebrationOpacity = useRef(new Animated.Value(0)).current; const [showCelebration, setShowCelebration] = useState(false); // 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) { mutation.reset(); setShowCelebration(false); celebrationScale.setValue(0); celebrationOpacity.setValue(0); } }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps // Celebration animation + auto-dismiss on success useEffect(() => { if (mutation.status === 'success' && mutation.data) { setShowCelebration(true); Animated.parallel([ Animated.spring(celebrationScale, { toValue: 1, tension: 50, friction: 6, useNativeDriver: true, }), Animated.timing(celebrationOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }), ]).start(); onSuccess?.(mutation.data); const timer = setTimeout(() => { onDismiss(); }, 2500); 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 isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error'; const canClaim = !isMutating && mutation.status !== 'success' && !!wallet.publicKey; const handleClaim = useCallback(async () => { if (!wallet.publicKey) return; try { await mutation.execute({ playerWallet: wallet.publicKey.toBase58(), gameId, amountClaimed: prizeAmount, }); } catch { // Error is already captured in mutation state } }, [wallet.publicKey, mutation.execute, gameId, prizeAmount]); // eslint-disable-line react-hooks/exhaustive-deps const statusLabel = STATUS_LABELS[mutation.status] || ''; return ( {/* Drag handle */} {/* Header */} {showCelebration ? (isRefund ? 'Refund Claimed!' : 'Prize Claimed!') : (isRefund ? 'Claim Refund' : 'Claim Prize')} {'\u2715'} {/* Celebration */} {showCelebration && ( {'🏆'} +{prizeAmount} SOL {isRefund ? 'Refund sent to your wallet' : 'Winnings sent to your wallet'} )} {/* Summary Card */} {!showCelebration && ( {isRefund ? 'Refund' : 'Prize'} {prizeAmount} SOL Game {gameId.slice(0, 8)}...{gameId.slice(-4)} )} {/* Error Display */} {mutation.error && ( {mutation.error.message} )} {/* CTA Button */} {!showCelebration && ( {isMutating ? ( {statusLabel} ) : ( {isRefund ? 'Claim Refund' : 'Claim Prize'} — {prizeAmount} SOL )} )} {/* Explorer link on success */} {mutation.data?.explorerUrl && ( View on Solscan )} ); } 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, }, // Celebration celebrationContainer: { alignItems: 'center', paddingVertical: 32, gap: 8, }, celebrationEmoji: { fontSize: 64, }, celebrationText: { fontSize: 32, fontWeight: '800', }, celebrationSubtext: { fontSize: 14, marginTop: 4, }, // Summary 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, }, // Error errorBox: { marginTop: 16, borderRadius: 12, borderWidth: 1, padding: 12, }, errorText: { fontSize: 13, fontWeight: '500', }, // CTA 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, }, explorerHint: { textAlign: 'center', marginTop: 12, fontSize: 13, }, });