@@ -9,7 +9,7 @@ import { getAuthToken } from '../utils/auth'
99import { IS_FREEBUFF } from '../utils/constants'
1010import { logger } from '../utils/logger'
1111
12- import type { Message } from '@codebuff/sdk' ;
12+ import type { Message } from '@codebuff/sdk'
1313
1414const AD_ROTATION_INTERVAL_MS = 60 * 1000 // 60 seconds per ad
1515const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then pause fetching new ads
@@ -28,52 +28,26 @@ export type AdResponse = {
2828 credits ?: number // Set after impression is recorded (in cents)
2929}
3030
31- export type AdVariant = 'banner' | 'choice'
32-
3331/**
3432 * Which upstream ad network to query. The server maps each provider onto the
3533 * same normalized response shape, so the rest of the hook is provider-agnostic.
3634 */
3735export type AdProvider = 'gravity' | 'carbon'
3836export type AdSurface = 'waiting_room'
3937
40- export type AdData =
41- | { variant : 'banner' ; ad : AdResponse }
42- | { variant : 'choice' ; ads : AdResponse [ ] }
43-
4438export type GravityAdState = {
45- ad : AdResponse | null
46- adData : AdData | null
39+ ads : AdResponse [ ] | null
4740 isLoading : boolean
4841 recordImpression : ( impUrl : string ) => void
4942}
5043
5144// Consolidated controller state for the ad rotation logic
5245type GravityController = {
53- cache : AdResponse [ ]
54- cacheIndex : number
5546 choiceCache : AdResponse [ ] [ ] // Cache of choice ad sets (each entry is 4 ads)
5647 choiceCacheIndex : number
57- variant : AdVariant | null // Assigned variant from backend
5848 impressionsFired : Set < string >
5949 adsShownSinceActivity : number
6050 tickInFlight : boolean
61- intervalId : ReturnType < typeof setInterval > | null
62- }
63-
64- // Pure helper: add an ad to the cache (if not already present)
65- function addToCache ( ctrl : GravityController , ad : AdResponse ) : void {
66- if ( ctrl . cache . some ( ( x ) => x . impUrl === ad . impUrl ) ) return
67- if ( ctrl . cache . length >= MAX_AD_CACHE_SIZE ) ctrl . cache . shift ( )
68- ctrl . cache . push ( ad )
69- }
70-
71- // Pure helper: get the next cached ad (cycles through the cache)
72- function nextFromCache ( ctrl : GravityController ) : AdResponse | null {
73- if ( ctrl . cache . length === 0 ) return null
74- const ad = ctrl . cache [ ctrl . cacheIndex % ctrl . cache . length ] !
75- ctrl . cacheIndex = ( ctrl . cacheIndex + 1 ) % ctrl . cache . length
76- return ad
7751}
7852
7953// Pure helper: add a choice ad set to the choice cache
@@ -121,8 +95,7 @@ export const useGravityAd = (options?: {
12195 const provider : AdProvider = options ?. provider ?? 'gravity'
12296 const fallbackProvider = options ?. fallbackProvider
12397 const surface = options ?. surface
124- const [ ad , setAd ] = useState < AdResponse | null > ( null )
125- const [ adData , setAdData ] = useState < AdData | null > ( null )
98+ const [ ads , setAds ] = useState < AdResponse [ ] | null > ( null )
12699 const [ isLoading , setIsLoading ] = useState ( false )
127100
128101 // Check if terminal height is too small to show ads
@@ -146,19 +119,15 @@ export const useGravityAd = (options?: {
146119
147120 // Single consolidated controller ref
148121 const ctrlRef = useRef < GravityController > ( {
149- cache : [ ] ,
150- cacheIndex : 0 ,
151122 choiceCache : [ ] ,
152123 choiceCacheIndex : 0 ,
153- variant : null ,
154124 impressionsFired : new Set ( ) ,
155125 adsShownSinceActivity : 0 ,
156126 tickInFlight : false ,
157- intervalId : null ,
158127 } )
159128
160129 // Ref for the tick function (avoids useCallback dependency issues)
161- const tickRef = useRef < ( ) => void > ( ( ) => { } )
130+ const tickRef = useRef < ( ) => void > ( ( ) => { } )
162131
163132 // Ref to track whether ads should be hidden for use in async code
164133 const shouldHideAdsRef = useRef ( shouldHideAds )
@@ -197,26 +166,12 @@ export const useGravityAd = (options?: {
197166 { creditsGranted : data . creditsGranted } ,
198167 '[ads] Ad impression credits granted' ,
199168 )
200- setAd ( ( cur ) =>
201- cur ?. impUrl === impUrl
202- ? { ...cur , credits : data . creditsGranted }
203- : cur ,
204- )
205- // Also update credits in adData for choice ads
206- setAdData ( ( cur ) => {
169+ // Also update credits in visible ads
170+ setAds ( ( cur ) => {
207171 if ( ! cur ) return cur
208- if ( cur . variant === 'choice' ) {
209- return {
210- ...cur ,
211- ads : cur . ads . map ( ( a ) =>
212- a . impUrl === impUrl ? { ...a , credits : data . creditsGranted } : a ,
213- ) ,
214- }
215- }
216- if ( cur . variant === 'banner' && cur . ad . impUrl === impUrl ) {
217- return { ...cur , ad : { ...cur . ad , credits : data . creditsGranted } }
218- }
219- return cur
172+ return cur . map ( ( a ) =>
173+ a . impUrl === impUrl ? { ...a , credits : data . creditsGranted } : a ,
174+ )
220175 } )
221176 }
222177 } )
@@ -225,23 +180,7 @@ export const useGravityAd = (options?: {
225180 } )
226181 }
227182
228- // Show a single banner ad and fire impression
229- const showAd = ( next : AdResponse ) : void => {
230- setAd ( next )
231- setAdData ( { variant : 'banner' , ad : next } )
232- recordImpressionOnce ( next . impUrl )
233- }
234-
235- // Show a choice ad set (impressions are fired by the component for visible ads only)
236- const showChoiceAds = ( ads : AdResponse [ ] ) : void => {
237- setAd ( ads [ 0 ] ?? null ) // Keep backwards compat for ad field
238- setAdData ( { variant : 'choice' , ads } )
239- }
240-
241- type FetchAdResult =
242- | { variant : 'banner' ; ad : AdResponse }
243- | { variant : 'choice' ; ads : AdResponse [ ] }
244- | null
183+ type FetchAdResult = { ads : AdResponse [ ] } | null
245184
246185 // Fetch an ad via web API
247186 const fetchAd = async ( ) : Promise < FetchAdResult > => {
@@ -324,21 +263,15 @@ export const useGravityAd = (options?: {
324263 }
325264
326265 const data = await response . json ( )
327- const variant = data . variant ?? 'banner'
328-
329- if (
330- variant === 'choice' &&
331- Array . isArray ( data . ads ) &&
332- data . ads . length > 0
333- ) {
334- return { variant : 'choice' , ads : data . ads as AdResponse [ ] }
335- }
336266
337- if ( data . ad ) {
338- return { variant : 'banner' , ad : data . ad as AdResponse }
267+ if ( Array . isArray ( data . ads ) && data . ads . length > 0 ) {
268+ return { ads : data . ads as AdResponse [ ] }
339269 }
340270 } catch ( err ) {
341- logger . error ( { err, provider : providerToTry } , '[ads] Failed to fetch ad' )
271+ logger . error (
272+ { err, provider : providerToTry } ,
273+ '[ads] Failed to fetch ad' ,
274+ )
342275 }
343276 }
344277
@@ -363,30 +296,15 @@ export const useGravityAd = (options?: {
363296 const result = canFetchNew ? await fetchAd ( ) : null
364297
365298 if ( result ) {
366- ctrl . variant = result . variant
367- if ( result . variant === 'choice' ) {
368- addToChoiceCache ( ctrl , result . ads )
369- ctrl . adsShownSinceActivity += 1
370- showChoiceAds ( result . ads )
371- } else {
372- addToCache ( ctrl , result . ad )
373- ctrl . adsShownSinceActivity += 1
374- showAd ( result . ad )
375- }
299+ addToChoiceCache ( ctrl , result . ads )
300+ ctrl . adsShownSinceActivity += 1
301+ setAds ( result . ads )
376302 } else {
377303 // Fall back to cached ads
378- if ( ctrl . variant === 'choice' ) {
379- const cachedSet = nextFromChoiceCache ( ctrl )
380- if ( cachedSet ) {
381- ctrl . adsShownSinceActivity += 1
382- showChoiceAds ( cachedSet )
383- }
384- } else {
385- const next = nextFromCache ( ctrl )
386- if ( next ) {
387- ctrl . adsShownSinceActivity += 1
388- showAd ( next )
389- }
304+ const cachedSet = nextFromChoiceCache ( ctrl )
305+ if ( cachedSet ) {
306+ ctrl . adsShownSinceActivity += 1
307+ setAds ( cachedSet )
390308 }
391309 }
392310 } finally {
@@ -414,34 +332,25 @@ export const useGravityAd = (options?: {
414332 const result = await fetchAd ( )
415333 if ( result ) {
416334 const ctrl = ctrlRef . current
417- ctrl . variant = result . variant
418- if ( result . variant === 'choice' ) {
419- addToChoiceCache ( ctrl , result . ads )
420- showChoiceAds ( result . ads )
421- } else {
422- addToCache ( ctrl , result . ad )
423- showAd ( result . ad )
424- }
335+ addToChoiceCache ( ctrl , result . ads )
336+ setAds ( result . ads )
425337 ctrl . adsShownSinceActivity = 1
426338 }
427339 setIsLoading ( false )
428340 } ) ( )
429341
430342 // Start interval for rotation (consistent 60s intervals)
431343 const id = setInterval ( ( ) => tickRef . current ( ) , AD_ROTATION_INTERVAL_MS )
432- ctrlRef . current . intervalId = id
433344
434345 return ( ) => {
435346 clearInterval ( id )
436- ctrlRef . current . intervalId = null
437347 }
438348 } , [ shouldStart , shouldHideAds , provider , fallbackProvider , surface ] )
439349
440- // Don't return ad when ads should be hidden
350+ // Don't return ads when ads should be hidden
441351 const visible = shouldStart && ! shouldHideAds
442352 return {
443- ad : visible ? ad : null ,
444- adData : visible ? adData : null ,
353+ ads : visible ? ads : null ,
445354 isLoading,
446355 recordImpression : recordImpressionOnce ,
447356 }
0 commit comments