@@ -662,6 +662,7 @@ const loggedInBoot = async ({
662662 ] ,
663663 balance ,
664664 clickbaitTries ,
665+ anonymousTheme ,
665666 ] = await Promise . all ( [
666667 visitSection ( req , res ) ,
667668 getRoles ( userId ) ,
@@ -687,6 +688,7 @@ const loggedInBoot = async ({
687688 } ) ,
688689 getBalanceBoot ( { userId } ) ,
689690 getClickbaitTries ( { userId } ) ,
691+ getAnonymousTheme ( userId ) ,
690692 ] ) ;
691693
692694 const profileCompletion = calculateProfileCompletion ( user , experienceFlags ) ;
@@ -695,6 +697,12 @@ const loggedInBoot = async ({
695697 return handleNonExistentUser ( con , req , res , middleware ) ;
696698 }
697699
700+ // Apply anonymous theme (e.g. recruiter light mode) if user has no saved settings
701+ const finalSettings =
702+ ! settings . updatedAt && anonymousTheme
703+ ? { ...settings , theme : anonymousTheme }
704+ : settings ;
705+
698706 const hasLocationSet = ! ! user . flags ?. location ?. lastStored ;
699707 const isTeamMember = exp ?. a ?. team === 1 ;
700708 const isPlus = isPlusMember ( user . subscriptionFlags ?. cycle ) ;
@@ -781,7 +789,7 @@ const loggedInBoot = async ({
781789 subDays ( new Date ( ) , FEED_SURVEY_INTERVAL ) >
782790 alerts . lastFeedSettingsFeedback ,
783791 } ,
784- settings : excludeProperties ( settings , [
792+ settings : excludeProperties ( finalSettings , [
785793 'userId' ,
786794 'updatedAt' ,
787795 'bookmarkSlug' ,
@@ -809,6 +817,46 @@ const getAnonymousFirstVisit = async (trackingId?: string) => {
809817 return finalValue ;
810818} ;
811819
820+ const ANONYMOUS_THEME_TTL = ONE_DAY_IN_SECONDS * 30 ; // 30 days, same as firstVisit
821+
822+ const getThemeRedisKey = ( id : string ) : string =>
823+ generateStorageKey ( StorageTopic . Boot , 'theme' , id ) ;
824+
825+ /**
826+ * Get stored theme preference from Redis for anonymous or authenticated users
827+ */
828+ export const getAnonymousTheme = async (
829+ id ?: string ,
830+ ) : Promise < string | null > => {
831+ if ( ! id ) return null ;
832+ return getRedisObject ( getThemeRedisKey ( id ) ) ;
833+ } ;
834+
835+ /**
836+ * Store theme preference in Redis for anonymous or authenticated users
837+ */
838+ export const setAnonymousTheme = async (
839+ id : string ,
840+ theme : string ,
841+ ) : Promise < void > => {
842+ await setRedisObjectWithExpiry (
843+ getThemeRedisKey ( id ) ,
844+ theme ,
845+ ANONYMOUS_THEME_TTL ,
846+ ) ;
847+ } ;
848+
849+ /**
850+ * Determine default theme based on referrer
851+ * Recruiter-facing pages default to light mode
852+ */
853+ const getDefaultThemeForReferrer = ( referrer ?: string ) : string => {
854+ if ( referrer === 'recruiter' ) {
855+ return 'bright' ; // light mode
856+ }
857+ return 'darcula' ; // dark mode
858+ } ;
859+
812860// We released the firstVisit at July 10, 2023.
813861// There should have been enough buffer time since we are releasing on July 13, 2023.
814862export const onboardingV2Requirement = new Date ( 2023 , 6 , 13 ) ;
@@ -820,16 +868,24 @@ const anonymousBoot = async (
820868 middleware ?: BootMiddleware ,
821869 shouldVerify = false ,
822870 email ?: string ,
871+ referrer ?: string ,
823872) : Promise < AnonymousBoot > => {
824873 const geo = geoSection ( req ) ;
825874
826- const [ visit , extra , firstVisit , exp ] = await Promise . all ( [
875+ const [ visit , extra , firstVisit , exp , existingTheme ] = await Promise . all ( [
827876 visitSection ( req , res ) ,
828877 middleware ? middleware ( con , req , res ) : { } ,
829878 getAnonymousFirstVisit ( req . trackingId ) ,
830879 getExperimentation ( { userId : req . trackingId , con, ...geo } ) ,
880+ getAnonymousTheme ( req . trackingId ) ,
831881 ] ) ;
832882
883+ // Determine theme: use existing preference or referrer-based default
884+ const theme = existingTheme ?? getDefaultThemeForReferrer ( referrer ) ;
885+ if ( ! existingTheme && req . trackingId ) {
886+ await setAnonymousTheme ( req . trackingId , theme ) ;
887+ }
888+
833889 return {
834890 user : {
835891 firstVisit,
@@ -844,7 +900,10 @@ const anonymousBoot = async (
844900 changelog : false ,
845901 shouldShowFeedFeedback : false ,
846902 } ,
847- settings : SETTINGS_DEFAULT ,
903+ settings : {
904+ ...SETTINGS_DEFAULT ,
905+ ...( theme && { theme } ) ,
906+ } ,
848907 notifications : { unreadNotificationsCount : 0 } ,
849908 squads : [ ] ,
850909 exp,
@@ -859,6 +918,9 @@ export const getBootData = async (
859918 res : FastifyReply ,
860919 middleware ?: BootMiddleware ,
861920) : Promise < AnonymousBoot | LoggedInBoot > => {
921+ // Extract referrer from query params (e.g., ?referrer=recruiter)
922+ const referrer = ( req . query as { referrer ?: string } ) ?. referrer ;
923+
862924 if (
863925 req . userId &&
864926 req . accessToken ?. expiresIn &&
@@ -880,9 +942,25 @@ export const getBootData = async (
880942 setRawCookie ( res , whoami . cookie ) ;
881943 }
882944 if ( whoami . verified === false ) {
883- return anonymousBoot ( con , req , res , middleware , true , whoami ?. email ) ;
945+ return anonymousBoot (
946+ con ,
947+ req ,
948+ res ,
949+ middleware ,
950+ true ,
951+ whoami ?. email ,
952+ referrer ,
953+ ) ;
884954 }
885955 if ( req . userId !== whoami . userId ) {
956+ // Migrate theme from anonymous trackingId to new userId before overwriting
957+ const oldTrackingId = req . trackingId ;
958+ if ( oldTrackingId && oldTrackingId !== whoami . userId ) {
959+ const anonymousTheme = await getAnonymousTheme ( oldTrackingId ) ;
960+ if ( anonymousTheme ) {
961+ await setAnonymousTheme ( whoami . userId , anonymousTheme ) ;
962+ }
963+ }
886964 req . userId = whoami . userId ;
887965 req . trackingId = req . userId ;
888966 setTrackingId ( req , res , req . trackingId ) ;
@@ -897,9 +975,9 @@ export const getBootData = async (
897975 } ) ;
898976 } else if ( req . cookies [ cookies . kratos . key ] ) {
899977 await clearAuthentication ( req , res , 'invalid cookie' ) ;
900- return anonymousBoot ( con , req , res , middleware ) ;
978+ return anonymousBoot ( con , req , res , middleware , false , undefined , referrer ) ;
901979 }
902- return anonymousBoot ( con , req , res , middleware ) ;
980+ return anonymousBoot ( con , req , res , middleware , false , undefined , referrer ) ;
903981} ;
904982
905983const COMPANION_QUERY = parse ( `query Post($url: String) {
@@ -1149,6 +1227,7 @@ const funnelBoots = {
11491227const funnelHandler : RouteHandler = async ( req , res ) => {
11501228 const con = await createOrGetConnection ( ) ;
11511229 const { id = 'funnel' } = req . params as { id : keyof typeof funnelBoots } ;
1230+ const referrer = ( req . query as { referrer ?: string } ) ?. referrer ;
11521231
11531232 if ( id in funnelBoots ) {
11541233 const funnel = funnelBoots [ id ] ;
@@ -1157,6 +1236,9 @@ const funnelHandler: RouteHandler = async (req, res) => {
11571236 req ,
11581237 res ,
11591238 generateFunnelBootMiddle ( funnel ) ,
1239+ false ,
1240+ undefined ,
1241+ referrer ,
11601242 ) ) as FunnelBoot ;
11611243 return res . send ( data ) ;
11621244 }
0 commit comments