Skip to content

Commit dc0e766

Browse files
committed
fix: defer inline login enrollment
1 parent b808499 commit dc0e766

5 files changed

Lines changed: 76 additions & 24 deletions

File tree

packages/shared/src/contexts/AuthContext.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import React, {
88
} from 'react';
99
import type { QueryObserverResult } from '@tanstack/react-query';
1010
import { useRouter } from 'next/router';
11-
import { useFeatureValue } from '@growthbook/growthbook-react';
11+
import { GrowthBookContext } from '@growthbook/growthbook-react';
1212
import type { AnonymousUser, LoggedUser } from '../lib/user';
1313
import { deleteAccount, logout as dispatchLogout } from '../lib/user';
1414
import type { AccessToken, Boot, Visit } from '../lib/boot';
@@ -24,6 +24,7 @@ import {
2424
onboardingUrl,
2525
webFunnelPrefix,
2626
} from '../lib/constants';
27+
import { featureInlineLogin } from '../lib/featureInlineLogin';
2728

2829
export interface LoginState {
2930
trigger: AuthTriggersType;
@@ -79,6 +80,7 @@ export interface AuthContextData {
7980
isGdprCovered?: boolean;
8081
isValidRegion?: boolean;
8182
isFunnel?: boolean;
83+
inlineLoginEnabled?: boolean;
8284
}
8385

8486
const isExtension = checkIsExtension();
@@ -159,20 +161,35 @@ export const AuthContextProvider = ({
159161
isAndroidApp,
160162
}: AuthContextProviderProps): ReactElement => {
161163
const [loginState, setLoginState] = useState<LoginState | null>(null);
164+
const [inlineLoginEnabled, setInlineLoginEnabled] = useState<boolean>();
165+
const inlineLoginEnabledRef = useRef<boolean>();
162166
const endUser = user && 'providers' in user ? user : null;
163167
const referral = user?.referralId || user?.referrer;
164168
const referralOrigin = user?.referralOrigin;
165169
const router = useRouter();
166170
const isFunnelRef = useRef(!!router?.pathname?.startsWith(webFunnelPrefix));
171+
const growthbookContext = useContext(GrowthBookContext);
172+
const growthbook = growthbookContext?.growthbook;
167173
const isValidRegion = useMemo(
168174
() => !invalidPlusRegions.includes(geo?.region),
169175
[geo?.region],
170176
);
171-
// Inline-login experiment flag. Source of truth for the local default lives
172-
// in `lib/featureManagement.ts` as `featureInlineLogin`. We can't import it
173-
// here because `featureManagement` → `graphql/posts` → `AuthContext` would
174-
// be a cycle, so the default is duplicated below; keep them in sync.
175-
const isInlineLoginEnabled = useFeatureValue<boolean>('inline_login', true);
177+
const evaluateInlineLogin = useCallback((): boolean => {
178+
if (!isNullOrUndefined(inlineLoginEnabledRef.current)) {
179+
return inlineLoginEnabledRef.current;
180+
}
181+
182+
const isEnabled =
183+
growthbook?.getFeatureValue(
184+
featureInlineLogin.id,
185+
featureInlineLogin.defaultValue,
186+
) === true;
187+
188+
inlineLoginEnabledRef.current = isEnabled;
189+
setInlineLoginEnabled(isEnabled);
190+
191+
return isEnabled;
192+
}, [growthbook]);
176193

177194
return (
178195
<AuthContext.Provider
@@ -186,6 +203,7 @@ export const AuthContextProvider = ({
186203
firstVisit: user?.firstVisit,
187204
trackingId: user?.id,
188205
shouldShowLogin: loginState !== null,
206+
inlineLoginEnabled,
189207
showLogin: useCallback(
190208
({ trigger, options = {} }) => {
191209
const hasCompanion = !!isCompanionActivated();
@@ -196,6 +214,7 @@ export const AuthContextProvider = ({
196214
}
197215

198216
const params = new URLSearchParams(globalThis?.location.search);
217+
const shouldUseInlineLogin = !isExtension && evaluateInlineLogin();
199218

200219
setLoginState({ ...options, trigger });
201220
if (isExtension) {
@@ -206,19 +225,20 @@ export const AuthContextProvider = ({
206225
params.set(AFTER_AUTH_PARAM, window.location.pathname);
207226
}
208227

228+
const onboardingPath = isExtension
229+
? `${onboardingUrl}?${params.toString()}`
230+
: `/onboarding?${params.toString()}`;
231+
209232
// Inline login experiment: render the modal in-place instead of
210233
// redirecting to /onboarding. Extension keeps the redirect because
211234
// it has no host page to mount the modal on.
212-
if (isInlineLoginEnabled && !isExtension) {
235+
if (shouldUseInlineLogin) {
213236
return;
214237
}
215238

216-
const onboardingPath = `${onboardingUrl}?${params.toString()}`;
217-
router.push(
218-
isExtension ? onboardingPath : `/onboarding?${params.toString()}`,
219-
);
239+
router.push(onboardingPath);
220240
},
221-
[router, isInlineLoginEnabled],
241+
[evaluateInlineLogin, router],
222242
),
223243
closeLogin: useCallback(() => setLoginState(null), []),
224244
loginState,

packages/shared/src/contexts/BootProvider.spec.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ const AuthMock = ({ updatedUser, loginTrigger }: AuthMockProps) => {
417417
getRedirectUri,
418418
trackingId,
419419
anonymous,
420+
inlineLoginEnabled,
420421
} = useContext(AuthContext);
421422

422423
return (
@@ -461,6 +462,9 @@ const AuthMock = ({ updatedUser, loginTrigger }: AuthMockProps) => {
461462
</button>
462463
<span data-test-value={trackingId}>Tracking ID</span>
463464
<span data-test-value={JSON.stringify(anonymous)}>Anonymous User</span>
465+
<span data-test-value={`${inlineLoginEnabled}`}>
466+
Inline Login Enabled
467+
</span>
464468
</>
465469
);
466470
};
@@ -501,6 +505,31 @@ it('should trigger show login callback', async () => {
501505
await expectToHaveTestValue(login, JSON.stringify({ trigger: expected }));
502506
});
503507

508+
it('should evaluate inline login only after auth intent', async () => {
509+
renderComponent(<AuthMock loginTrigger={AuthTriggers.Comment} />, {
510+
...defaultBootData,
511+
user: defaultAnonymousUser,
512+
exp: {
513+
f: '{}',
514+
e: [],
515+
a: [],
516+
features: {
517+
inline_login: {
518+
defaultValue: true,
519+
},
520+
},
521+
},
522+
});
523+
524+
const login = await screen.findByText('Log in');
525+
const inlineLogin = await screen.findByText('Inline Login Enabled');
526+
await expectToHaveTestValue(inlineLogin, 'undefined');
527+
528+
fireEvent.click(login);
529+
530+
await expectToHaveTestValue(inlineLogin, 'true');
531+
});
532+
504533
it('should trigger close login callback', async () => {
505534
const expected = AuthTriggers.Comment;
506535
renderComponent(<AuthMock loginTrigger={expected} />, {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const featureInlineLogin = {
2+
id: 'inline_login',
3+
defaultValue: false as boolean,
4+
};

packages/shared/src/lib/featureManagement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { PlusItemStatus } from '../components/plus/PlusListItem';
55
import { isDevelopment } from './constants';
66
import { BriefingType } from '../graphql/posts';
77

8+
export { featureInlineLogin } from './featureInlineLogin';
9+
810
export class Feature<T extends JSONValue> {
911
readonly id: string;
1012

@@ -59,8 +61,6 @@ export const featurePlusCtaCopy = new Feature('plus_cta_copy', {
5961

6062
export const featurePlusApiLanding = new Feature('plus_api_landing_v2', false);
6163

62-
export const featureInlineLogin = new Feature('inline_login', false);
63-
6464
export const featureLuckyButton = new Feature('lucky_button', false);
6565

6666
export const featureSmartComposer = new Feature('smart_composer', false);

packages/webapp/pages/_app.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ import {
5252
WebKitMessageHandlers,
5353
} from '@dailydotdev/shared/src/lib/ios';
5454
import { useCheckLocation } from '@dailydotdev/shared/src/hooks/useCheckLocation';
55-
import { useFeature } from '@dailydotdev/shared/src/components/GrowthBookProvider';
56-
import { featureInlineLogin } from '@dailydotdev/shared/src/lib/featureManagement';
5755
import Seo, { defaultSeo, defaultSeoTitle } from '../next-seo';
5856
import useWebappVersion from '../hooks/useWebappVersion';
5957
import { getAppOrigin, getSiteOrigin } from '../lib/seo';
@@ -104,8 +102,8 @@ const onboardingExcludedPaths = [
104102
'/jobs',
105103
'/settings',
106104
];
107-
// When the inline_login experiment is on, we only force the rest of onboarding
108-
// when the user lands on the main feed — everywhere else they can keep
105+
// Once auth intent assigns the user to inline_login, only force the rest of
106+
// onboarding when they land on the main feed. Everywhere else they can keep
109107
// browsing after the inline first step.
110108
const mainFeedPathnames = new Set([
111109
'/',
@@ -177,8 +175,8 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
177175
shouldShowLogin,
178176
closeLogin,
179177
loginState,
178+
inlineLoginEnabled,
180179
} = useAuthContext();
181-
const isInlineLoginEnabled = useFeature(featureInlineLogin);
182180
const { showBanner, onAcceptCookies, onOpenBanner, onHideBanner } =
183181
useCookieBanner();
184182
useWebVitals();
@@ -240,9 +238,10 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
240238
return;
241239
}
242240

243-
// Inline login experiment: defer the rest of onboarding until the user
244-
// navigates to the main feed; otherwise let them keep browsing.
245-
if (isInlineLoginEnabled && !mainFeedPathnames.has(router.pathname)) {
241+
// Inline login experiment: after auth intent enrolls the user, defer the
242+
// rest of onboarding until they navigate to the main feed; otherwise let
243+
// them keep browsing.
244+
if (inlineLoginEnabled && !mainFeedPathnames.has(router.pathname)) {
246245
return;
247246
}
248247

@@ -255,7 +254,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
255254
router,
256255
router.pathname,
257256
isOnboardingComplete,
258-
isInlineLoginEnabled,
257+
inlineLoginEnabled,
259258
]);
260259

261260
useEffect(() => {
@@ -407,7 +406,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
407406
<DndContextProvider>
408407
{getLayout(<Component {...pageProps} />, pageProps, layoutProps)}
409408
</DndContextProvider>
410-
{isInlineLoginEnabled && shouldShowLogin && (
409+
{inlineLoginEnabled && shouldShowLogin && (
411410
<AuthModal
412411
isOpen={shouldShowLogin}
413412
onRequestClose={closeLogin}

0 commit comments

Comments
 (0)