feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -12,26 +12,13 @@ interface AdBannerProps {
onOpenUpgradeModal: () => void
}
const DEFAULT_CREATIVE: AdCreativePayload = {
id: 'cursor_default',
networkId: 'direct_sponsor',
networkName: 'Direct Partner',
title: 'Cursor AI — Next-Gen AI Code Editor',
description: 'Build software with intelligent voice agents & lightning-speed code search.',
ctaText: 'Learn More',
clickUrl: 'https://cursor.com',
sponsorTag: 'Sponsor',
advertiserName: 'Cursor AI',
bidEcpm: 15.5,
format: 'banner_dock',
}
export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.ReactElement | null {
const [creative, setCreative] = useState<AdCreativePayload>(DEFAULT_CREATIVE)
const [creative, setCreative] = useState<AdCreativePayload | null>(null)
// Fetch highest bidding ad creative via mediation auction
const fetchAdAuction = useCallback(async () => {
if (tier !== 'free') return
setCreative(null)
try {
if (window.electronAPI?.ads?.requestAuction) {
const res = await window.electronAPI.ads.requestAuction({
@ -52,7 +39,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
}
}
} catch {
// fallback to default creative
setCreative(null)
}
}, [tier])
@ -64,7 +51,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
}, [fetchAdAuction])
// Pro, Pro+, Team, Enterprise users are 100% Ad-Free
if (tier !== 'free') {
if (tier !== 'free' || creative === null) {
return null
}
@ -124,7 +111,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
justifyContent: 'center',
flexShrink: 0,
fontSize: '11px',
fontWeight: 800,
fontWeight: 600,
fontFamily: d3roFontMono,
color:
creative.networkId === 'google_ad_manager'
@ -159,7 +146,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
sx={{
fontFamily: d3roFontMono,
fontSize: '9px',
fontWeight: 900,
fontWeight: 600,
color: '#000',
bgcolor: '#facc15',
px: 0.6,
@ -177,7 +164,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
sx={{
fontFamily: d3roFontSans,
fontSize: '12px',
fontWeight: 700,
fontWeight: 500,
color: d3roPalette.text.primary,
}}
>
@ -188,7 +175,7 @@ export function AdBanner({ tier, onOpenUpgradeModal }: AdBannerProps): React.Rea
sx={{
fontFamily: d3roFontMono,
fontSize: '9px',
fontWeight: 700,
fontWeight: 500,
color:
creative.networkId === 'ethical_ads'
? '#4ade80'

View file

@ -1,5 +1,6 @@
// apps/desktop/src/renderer/components/ads/RewardedQuotaModal.tsx
// Dynamic Rewarded Video Modal with Multi-Network Video Auction (Unity/AppLovin/Playwire/ElevenLabs)
// 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'
@ -13,32 +14,21 @@ interface RewardedQuotaModalProps {
onRewardClaimed: (addedTokens: number) => void
}
const DEFAULT_VIDEO_CREATIVE: AdCreativePayload = {
id: 'elevenlabs_video_default',
networkId: 'direct_sponsor',
networkName: 'ElevenLabs Direct Partner',
title: 'ElevenLabs Voice Synthesizer',
description: 'Human-like AI audio generation & voice cloning for developers and creators.',
ctaText: 'Explore Voice AI',
clickUrl: 'https://elevenlabs.io',
sponsorTag: 'Rewarded Sponsor',
advertiserName: 'ElevenLabs',
bidEcpm: 18.0,
format: 'rewarded_video',
rewardTokens: 50,
durationSeconds: 15,
}
export function RewardedQuotaModal({
open,
onClose,
onRewardClaimed,
}: RewardedQuotaModalProps): React.ReactElement {
const [creative, setCreative] = useState<AdCreativePayload>(DEFAULT_VIDEO_CREATIVE)
const [timeLeft, setTimeLeft] = useState(15)
const [creative, setCreative] = useState<AdCreativePayload | null>(null)
const [timeLeft, setTimeLeft] = useState(0)
const [completed, setCompleted] = useState(false)
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const fetchRewardedAuction = useCallback(async () => {
setLoading(true)
setErrorMessage(null)
setCreative(null)
try {
if (window.electronAPI?.ads?.requestAuction) {
const res = await window.electronAPI.ads.requestAuction({
@ -57,22 +47,31 @@ export function RewardedQuotaModal({
networkName: res.data.winner.networkName,
earnedEcpm: res.data.winningBidEcpm,
})
return
}
}
setErrorMessage('A verified rewarded ad is not available right now.')
} catch {
// fallback
setErrorMessage('The rewarded ad provider could not be reached.')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (!open) {
setTimeLeft(15)
setCreative(null)
setTimeLeft(0)
setCompleted(false)
setErrorMessage(null)
return
}
fetchRewardedAuction()
void fetchRewardedAuction()
}, [open, fetchRewardedAuction])
useEffect(() => {
if (!open || creative === null || timeLeft <= 0 || completed) return undefined
const interval = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
@ -85,21 +84,52 @@ export function RewardedQuotaModal({
}, 1000)
return () => clearInterval(interval)
}, [open, fetchRewardedAuction])
}, [completed, creative, open, timeLeft])
const handleClaim = async () => {
let tokens = 50
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?.tokensAdded) {
tokens = res.data.tokensAdded
if (res.success && res.data?.success && res.data.tokensAdded > 0) {
onRewardClaimed(res.data.tokensAdded)
onClose()
return
}
}
onRewardClaimed(tokens)
onClose()
setErrorMessage('Reward verification failed. No tokens were added.')
}
if (creative === null) {
return (
<Modal open={open} onClose={onClose}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 440 },
bgcolor: d3roPalette.bg.modal,
borderRadius: d3roRadius.modal,
border: `1px solid ${d3roPalette.glass.hairline}`,
boxShadow: d3roShadow.modal,
p: 3,
outline: 'none',
}}
>
<Typography sx={{ fontFamily: d3roFontSans, fontWeight: 500, color: d3roPalette.text.primary }}>
Rewarded ad unavailable
</Typography>
<Typography sx={{ mt: 1.25, fontFamily: d3roFontSans, fontSize: 13, color: d3roPalette.text.secondary }}>
{loading ? 'Checking verified ad providers…' : errorMessage ?? 'No verified ad is available.'}
</Typography>
<Button onClick={onClose} sx={{ mt: 2, textTransform: 'none' }}>Close</Button>
</Box>
</Modal>
)
}
const duration = creative.durationSeconds || 15
@ -138,8 +168,8 @@ export function RewardedQuotaModal({
>
<PlayCircle size={18} color={d3roPalette.accent.light} />
</Box>
<Typography sx={{ fontFamily: d3roFontSans, fontWeight: 700, fontSize: '15px', color: d3roPalette.text.primary }}>
Sponsor Video Refill +50 Free AI Tokens
<Typography sx={{ fontFamily: d3roFontSans, fontWeight: 500, fontSize: '15px', color: d3roPalette.text.primary }}>
Verified rewarded ad
</Typography>
</Box>
<Box onClick={onClose} sx={{ cursor: 'pointer', color: d3roPalette.text.dimLabel, '&:hover': { color: d3roPalette.text.primary } }}>
@ -180,7 +210,7 @@ export function RewardedQuotaModal({
sx={{
fontFamily: d3roFontMono,
fontSize: '9px',
fontWeight: 900,
fontWeight: 600,
color: '#fff',
bgcolor: '#2563eb',
px: 1,
@ -189,7 +219,7 @@ export function RewardedQuotaModal({
letterSpacing: '0.5px',
}}
>
UNITY ADS TEST MODE
{creative.networkName}
</Typography>
<Typography
sx={{
@ -198,7 +228,7 @@ export function RewardedQuotaModal({
color: 'rgba(255, 255, 255, 0.6)',
}}
>
Placement: Rewarded_Tokens_Refill
Reward verification required
</Typography>
</Box>
@ -217,7 +247,7 @@ export function RewardedQuotaModal({
borderRadius: '4px',
fontFamily: d3roFontMono,
fontSize: '11px',
fontWeight: 700,
fontWeight: 500,
zIndex: 2,
}}
>
@ -243,7 +273,7 @@ export function RewardedQuotaModal({
<Sparkles size={28} color="#60a5fa" />
</Box>
<Typography sx={{ fontFamily: d3roFontSans, fontWeight: 800, fontSize: '15px', color: '#ffffff' }}>
<Typography sx={{ fontFamily: d3roFontSans, fontWeight: 600, fontSize: '15px', color: '#ffffff' }}>
{creative.title}
</Typography>
<Typography sx={{ fontFamily: d3roFontSans, fontSize: '11px', color: 'rgba(255, 255, 255, 0.75)', mt: 0.5, maxWidth: 360 }}>
@ -269,7 +299,7 @@ export function RewardedQuotaModal({
{/* Footer Actions */}
<Box sx={{ mt: 3, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography sx={{ fontFamily: d3roFontMono, fontSize: '11px', color: d3roPalette.text.dimLabel }}>
Reward: <span style={{ color: d3roPalette.tag.greenText, fontWeight: 700 }}>+50 Cloud AI Tokens</span>
Reward is added only after server verification.
</Typography>
{completed ? (
@ -279,7 +309,7 @@ export function RewardedQuotaModal({
startIcon={<CheckCircle2 size={16} />}
sx={{
fontFamily: d3roFontSans,
fontWeight: 700,
fontWeight: 500,
fontSize: '12px',
textTransform: 'none',
bgcolor: d3roPalette.tag.greenText,
@ -290,7 +320,7 @@ export function RewardedQuotaModal({
'&:hover': { bgcolor: '#4ade80' },
}}
>
Claim +50 Free Tokens
Verify reward
</Button>
) : (
<Button