Skip to content

Commit 08bab4e

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

3 files changed

Lines changed: 73 additions & 22 deletions

File tree

packages/shared/src/contexts/AuthContext.tsx

Lines changed: 35 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';
@@ -79,9 +79,12 @@ export interface AuthContextData {
7979
isGdprCovered?: boolean;
8080
isValidRegion?: boolean;
8181
isFunnel?: boolean;
82+
inlineLoginEnabled?: boolean;
8283
}
8384

8485
const isExtension = checkIsExtension();
86+
const inlineLoginFeatureId = 'inline_login';
87+
const inlineLoginDefaultValue = false;
8588
const AuthContext = React.createContext<AuthContextData>(null);
8689
export const useAuthContext = (): AuthContextData => useContext(AuthContext);
8790
export default AuthContext;
@@ -159,20 +162,37 @@ export const AuthContextProvider = ({
159162
isAndroidApp,
160163
}: AuthContextProviderProps): ReactElement => {
161164
const [loginState, setLoginState] = useState<LoginState | null>(null);
165+
const [inlineLoginEnabled, setInlineLoginEnabled] = useState<boolean>();
166+
const inlineLoginEnabledRef = useRef<boolean>();
162167
const endUser = user && 'providers' in user ? user : null;
163168
const referral = user?.referralId || user?.referrer;
164169
const referralOrigin = user?.referralOrigin;
165170
const router = useRouter();
166171
const isFunnelRef = useRef(!!router?.pathname?.startsWith(webFunnelPrefix));
172+
const growthbookContext = useContext(GrowthBookContext);
173+
const growthbook = growthbookContext?.growthbook;
167174
const isValidRegion = useMemo(
168175
() => !invalidPlusRegions.includes(geo?.region),
169176
[geo?.region],
170177
);
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);
178+
const evaluateInlineLogin = useCallback((): boolean => {
179+
if (!isNullOrUndefined(inlineLoginEnabledRef.current)) {
180+
return inlineLoginEnabledRef.current;
181+
}
182+
183+
// Keep this id/default in sync with `featureInlineLogin`. Importing it here
184+
// would create a cycle through `graphql/posts` back to `AuthContext`.
185+
const isEnabled =
186+
growthbook?.getFeatureValue(
187+
inlineLoginFeatureId,
188+
inlineLoginDefaultValue,
189+
) === true;
190+
191+
inlineLoginEnabledRef.current = isEnabled;
192+
setInlineLoginEnabled(isEnabled);
193+
194+
return isEnabled;
195+
}, [growthbook]);
176196

177197
return (
178198
<AuthContext.Provider
@@ -186,6 +206,7 @@ export const AuthContextProvider = ({
186206
firstVisit: user?.firstVisit,
187207
trackingId: user?.id,
188208
shouldShowLogin: loginState !== null,
209+
inlineLoginEnabled,
189210
showLogin: useCallback(
190211
({ trigger, options = {} }) => {
191212
const hasCompanion = !!isCompanionActivated();
@@ -196,6 +217,7 @@ export const AuthContextProvider = ({
196217
}
197218

198219
const params = new URLSearchParams(globalThis?.location.search);
220+
const shouldUseInlineLogin = !isExtension && evaluateInlineLogin();
199221

200222
setLoginState({ ...options, trigger });
201223
if (isExtension) {
@@ -206,19 +228,20 @@ export const AuthContextProvider = ({
206228
params.set(AFTER_AUTH_PARAM, window.location.pathname);
207229
}
208230

231+
const onboardingPath = isExtension
232+
? `${onboardingUrl}?${params.toString()}`
233+
: `/onboarding?${params.toString()}`;
234+
209235
// Inline login experiment: render the modal in-place instead of
210236
// redirecting to /onboarding. Extension keeps the redirect because
211237
// it has no host page to mount the modal on.
212-
if (isInlineLoginEnabled && !isExtension) {
238+
if (shouldUseInlineLogin) {
213239
return;
214240
}
215241

216-
const onboardingPath = `${onboardingUrl}?${params.toString()}`;
217-
router.push(
218-
isExtension ? onboardingPath : `/onboarding?${params.toString()}`,
219-
);
242+
router.push(onboardingPath);
220243
},
221-
[router, isInlineLoginEnabled],
244+
[evaluateInlineLogin, router],
222245
),
223246
closeLogin: useCallback(() => setLoginState(null), []),
224247
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} />, {

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)