@@ -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,12 @@ 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-
235183 // Show a choice ad set (impressions are fired by the component for visible ads only)
236184 const showChoiceAds = ( ads : AdResponse [ ] ) : void => {
237- setAd ( ads [ 0 ] ?? null ) // Keep backwards compat for ad field
238- setAdData ( { variant : 'choice' , ads } )
185+ setAds ( ads )
239186 }
240187
241- type FetchAdResult =
242- | { variant : 'banner' ; ad : AdResponse }
243- | { variant : 'choice' ; ads : AdResponse [ ] }
244- | null
188+ type FetchAdResult = { ads : AdResponse [ ] } | null
245189
246190 // Fetch an ad via web API
247191 const fetchAd = async ( ) : Promise < FetchAdResult > => {
@@ -324,21 +268,15 @@ export const useGravityAd = (options?: {
324268 }
325269
326270 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- }
336271
337- if ( data . ad ) {
338- return { variant : 'banner' , ad : data . ad as AdResponse }
272+ if ( Array . isArray ( data . ads ) && data . ads . length > 0 ) {
273+ return { ads : data . ads as AdResponse [ ] }
339274 }
340275 } catch ( err ) {
341- logger . error ( { err, provider : providerToTry } , '[ads] Failed to fetch ad' )
276+ logger . error (
277+ { err, provider : providerToTry } ,
278+ '[ads] Failed to fetch ad' ,
279+ )
342280 }
343281 }
344282
@@ -363,30 +301,15 @@ export const useGravityAd = (options?: {
363301 const result = canFetchNew ? await fetchAd ( ) : null
364302
365303 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- }
304+ addToChoiceCache ( ctrl , result . ads )
305+ ctrl . adsShownSinceActivity += 1
306+ showChoiceAds ( result . ads )
376307 } else {
377308 // 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- }
309+ const cachedSet = nextFromChoiceCache ( ctrl )
310+ if ( cachedSet ) {
311+ ctrl . adsShownSinceActivity += 1
312+ showChoiceAds ( cachedSet )
390313 }
391314 }
392315 } finally {
@@ -414,34 +337,25 @@ export const useGravityAd = (options?: {
414337 const result = await fetchAd ( )
415338 if ( result ) {
416339 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- }
340+ addToChoiceCache ( ctrl , result . ads )
341+ showChoiceAds ( result . ads )
425342 ctrl . adsShownSinceActivity = 1
426343 }
427344 setIsLoading ( false )
428345 } ) ( )
429346
430347 // Start interval for rotation (consistent 60s intervals)
431348 const id = setInterval ( ( ) => tickRef . current ( ) , AD_ROTATION_INTERVAL_MS )
432- ctrlRef . current . intervalId = id
433349
434350 return ( ) => {
435351 clearInterval ( id )
436- ctrlRef . current . intervalId = null
437352 }
438353 } , [ shouldStart , shouldHideAds , provider , fallbackProvider , surface ] )
439354
440- // Don't return ad when ads should be hidden
355+ // Don't return ads when ads should be hidden
441356 const visible = shouldStart && ! shouldHideAds
442357 return {
443- ad : visible ? ad : null ,
444- adData : visible ? adData : null ,
358+ ads : visible ? ads : null ,
445359 isLoading,
446360 recordImpression : recordImpressionOnce ,
447361 }
0 commit comments