// apps/desktop/src/renderer/components/ads/RewardedQuotaModal.tsx // Rewarded ad UI. It renders only a provider-returned creative and grants // nothing until the main-process verification boundary confirms it. import React, { useState, useEffect, useCallback } from 'react' import { Modal, Box, Typography, Button, LinearProgress } from '@mui/material' import { PlayCircle, Sparkles, Clock, X, CheckCircle2 } from 'lucide-react' import { d3roPalette, d3roFontSans, d3roFontMono, d3roRadius, d3roShadow } from '@d3ro/ui/theme' import type { AdCreativePayload } from '@d3ro/core/types' interface RewardedQuotaModalProps { open: boolean onClose: () => void onRewardClaimed: (addedTokens: number) => void } export function RewardedQuotaModal({ open, onClose, onRewardClaimed, }: RewardedQuotaModalProps): React.ReactElement { const [creative, setCreative] = useState(null) const [timeLeft, setTimeLeft] = useState(0) const [completed, setCompleted] = useState(false) const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState(null) const fetchRewardedAuction = useCallback(async () => { setLoading(true) setErrorMessage(null) setCreative(null) try { if (window.electronAPI?.ads?.requestAuction) { const res = await window.electronAPI.ads.requestAuction({ placement: 'rewarded_video_quota', format: 'rewarded_video', floorEcpm: 4.0, }) if (res.success && res.data?.winner) { setCreative(res.data.winner) setTimeLeft(res.data.winner.durationSeconds || 15) // Record video impression window.electronAPI.ads.recordImpression({ adId: res.data.winner.id, format: 'rewarded_video', network: res.data.winner.networkId, networkName: res.data.winner.networkName, earnedEcpm: res.data.winningBidEcpm, }) return } } setErrorMessage('A verified rewarded ad is not available right now.') } catch { setErrorMessage('The rewarded ad provider could not be reached.') } finally { setLoading(false) } }, []) useEffect(() => { if (!open) { setCreative(null) setTimeLeft(0) setCompleted(false) setErrorMessage(null) return } void fetchRewardedAuction() }, [open, fetchRewardedAuction]) useEffect(() => { if (!open || creative === null || timeLeft <= 0 || completed) return undefined const interval = setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { clearInterval(interval) setCompleted(true) return 0 } return prev - 1 }) }, 1000) return () => clearInterval(interval) }, [completed, creative, open, timeLeft]) const handleClaim = async () => { if (creative === null || !completed) return if (window.electronAPI?.ads?.claimReward) { const res = await window.electronAPI.ads.claimReward({ adId: creative.id, networkId: creative.networkId as string, }) if (res.success && res.data?.success && res.data.tokensAdded > 0) { onRewardClaimed(res.data.tokensAdded) onClose() return } } setErrorMessage('Reward verification failed. No tokens were added.') } if (creative === null) { return ( Rewarded ad unavailable {loading ? 'Checking verified ad providers…' : errorMessage ?? 'No verified ad is available.'} ) } const duration = creative.durationSeconds || 15 const progressPercent = ((duration - timeLeft) / duration) * 100 return ( {/* Header */} Verified rewarded ad {/* Video Ad Screen with Authentic Unity / AppLovin Test Mode UI */} {/* Unity / AppLovin Official Test Watermark Ribbon */} {creative.networkName} Reward verification required {completed ? 'Reward Ready' : `00:${timeLeft.toString().padStart(2, '0')}`} {/* Test Commercial Creative Content */} {creative.title} {creative.description} {/* Video Progress Bar at bottom */} {/* Footer Actions */} Reward is added only after server verification. {completed ? ( ) : ( )} ) }