@@ -17,6 +17,11 @@ const BASE_MESSAGE_CLASSNAME = "text-lg font-semibold";
1717const BASE_BUTTON_CLASSNAME =
1818 "flex h-8 justify-center items-center px-4 rounded-md font-semibold" ;
1919
20+ type PromoBannerProps = {
21+ requestPath ?: string ;
22+ seed ?: string ;
23+ } ;
24+
2025const STATIC_PROMOS : PromoData [ ] = Object . values ( promoData ) ;
2126
2227const isPromoActive = ( promo : PromoData | null | undefined ) =>
@@ -57,7 +62,18 @@ const getEligiblePromos = (promos: PromoData[], os: string | null) =>
5762 return promo . osTargets . includes ( os ) ;
5863 } ) ;
5964
60- const selectWeightedPromo = ( promos : PromoData [ ] ) => {
65+ const normalizeSeed = ( seed : string | null | undefined ) =>
66+ seed && seed . length > 0 ? seed : "default" ;
67+
68+ const deterministicRandom = ( seed : string ) => {
69+ let hash = 0 ;
70+ for ( let index = 0 ; index < seed . length ; index += 1 ) {
71+ hash = ( hash * 31 + seed . charCodeAt ( index ) ) | 0 ;
72+ }
73+ return ( hash >>> 0 ) / 0xffffffff ;
74+ } ;
75+
76+ const selectWeightedPromo = ( promos : PromoData [ ] , seed : string ) => {
6177 if ( promos . length === 0 ) {
6278 return null ;
6379 }
@@ -69,7 +85,7 @@ const selectWeightedPromo = (promos: PromoData[]) => {
6985 return getHighestPriorityPromo ( promos ) ;
7086 }
7187
72- let threshold = Math . random ( ) * totalWeight ;
88+ let threshold = deterministicRandom ( seed ) * totalWeight ;
7389
7490 for ( let index = 0 ; index < promos . length ; index += 1 ) {
7591 threshold -= weights [ index ] ;
@@ -85,15 +101,18 @@ const selectWeightedPromo = (promos: PromoData[]) => {
85101const buildPromoList = ( path : string | null ) : PromoData [ ] =>
86102 STATIC_PROMOS . filter ( ( promo ) => ! isSuppressedOnPath ( promo , path ) ) ;
87103
88- const PromoBanner : React . FC = ( ) => {
104+ const PromoBanner : React . FC < PromoBannerProps > = ( { requestPath , seed } ) => {
89105 const browserOS = useBrowserOS ( ) ;
90- const pathName = typeof window !== "undefined" ? window . location . pathname : null ;
106+ const pathName =
107+ requestPath ??
108+ ( typeof window !== "undefined" ? window . location . pathname : null ) ;
91109 const promos = buildPromoList ( pathName ) ;
92110 const eligiblePromos = getEligiblePromos ( promos , browserOS ) ;
93111 const fallbackPromos = promos . filter ( ( promo ) => isPromoActive ( promo ) ) ;
94112 const selectionPool =
95113 eligiblePromos . length > 0 ? eligiblePromos : fallbackPromos ;
96- const selectedPromo = selectWeightedPromo ( selectionPool ) ;
114+ const selectionSeed = normalizeSeed ( seed ?? pathName ?? "default" ) ;
115+ const selectedPromo = selectWeightedPromo ( selectionPool , selectionSeed ) ;
97116
98117 if ( ! selectedPromo || ! selectedPromo . cta ) {
99118 return null ;
0 commit comments