Skip to content

Commit cbab975

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

5 files changed

Lines changed: 130 additions & 24 deletions

File tree

packages/shared/src/contexts/AuthContext.tsx

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import type { ReactElement, ReactNode } from 'react';
22
import React, {
33
useCallback,
44
useContext,
5+
useEffect,
56
useMemo,
67
useRef,
78
useState,
89
} from 'react';
910
import type { QueryObserverResult } from '@tanstack/react-query';
1011
import { useRouter } from 'next/router';
11-
import { useFeatureValue } 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';
@@ -49,6 +49,12 @@ type ShowLoginParams = {
4949
options?: LoginOptions;
5050
};
5151

52+
export interface InlineLoginExperiment {
53+
enabled?: boolean;
54+
isLoading: boolean;
55+
requestEvaluation: () => void;
56+
}
57+
5258
export interface AuthContextData {
5359
user?: LoggedUser;
5460
isLoggedIn: boolean;
@@ -79,6 +85,7 @@ export interface AuthContextData {
7985
isGdprCovered?: boolean;
8086
isValidRegion?: boolean;
8187
isFunnel?: boolean;
88+
inlineLoginEnabled?: boolean;
8289
}
8390

8491
const isExtension = checkIsExtension();
@@ -126,6 +133,7 @@ export type AuthContextProviderProps = {
126133
isFetched?: boolean;
127134
children?: ReactNode;
128135
firstLoad?: boolean;
136+
inlineLoginExperiment?: InlineLoginExperiment;
129137
} & Pick<
130138
AuthContextData,
131139
| 'getRedirectUri'
@@ -157,22 +165,39 @@ export const AuthContextProvider = ({
157165
firstLoad,
158166
geo,
159167
isAndroidApp,
168+
inlineLoginExperiment,
160169
}: AuthContextProviderProps): ReactElement => {
161170
const [loginState, setLoginState] = useState<LoginState | null>(null);
171+
const [pendingLoginPath, setPendingLoginPath] = useState<string | null>(null);
162172
const endUser = user && 'providers' in user ? user : null;
163173
const referral = user?.referralId || user?.referrer;
164174
const referralOrigin = user?.referralOrigin;
165175
const router = useRouter();
166176
const isFunnelRef = useRef(!!router?.pathname?.startsWith(webFunnelPrefix));
177+
const inlineLoginEnabled = inlineLoginExperiment?.enabled;
167178
const isValidRegion = useMemo(
168179
() => !invalidPlusRegions.includes(geo?.region),
169180
[geo?.region],
170181
);
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);
182+
183+
useEffect(() => {
184+
if (!pendingLoginPath || inlineLoginExperiment?.isLoading) {
185+
return;
186+
}
187+
188+
if (inlineLoginEnabled) {
189+
setPendingLoginPath(null);
190+
return;
191+
}
192+
193+
router.push(pendingLoginPath);
194+
setPendingLoginPath(null);
195+
}, [
196+
inlineLoginEnabled,
197+
inlineLoginExperiment?.isLoading,
198+
pendingLoginPath,
199+
router,
200+
]);
176201

177202
return (
178203
<AuthContext.Provider
@@ -186,6 +211,7 @@ export const AuthContextProvider = ({
186211
firstVisit: user?.firstVisit,
187212
trackingId: user?.id,
188213
shouldShowLogin: loginState !== null,
214+
inlineLoginEnabled,
189215
showLogin: useCallback(
190216
({ trigger, options = {} }) => {
191217
const hasCompanion = !!isCompanionActivated();
@@ -206,19 +232,22 @@ export const AuthContextProvider = ({
206232
params.set(AFTER_AUTH_PARAM, window.location.pathname);
207233
}
208234

235+
const onboardingPath = isExtension
236+
? `${onboardingUrl}?${params.toString()}`
237+
: `/onboarding?${params.toString()}`;
238+
209239
// Inline login experiment: render the modal in-place instead of
210240
// redirecting to /onboarding. Extension keeps the redirect because
211241
// it has no host page to mount the modal on.
212-
if (isInlineLoginEnabled && !isExtension) {
242+
if (inlineLoginExperiment && !isExtension) {
243+
inlineLoginExperiment.requestEvaluation();
244+
setPendingLoginPath(onboardingPath);
213245
return;
214246
}
215247

216-
const onboardingPath = `${onboardingUrl}?${params.toString()}`;
217-
router.push(
218-
isExtension ? onboardingPath : `/onboarding?${params.toString()}`,
219-
);
248+
router.push(onboardingPath);
220249
},
221-
[router, isInlineLoginEnabled],
250+
[inlineLoginExperiment, router],
222251
),
223252
closeLogin: useCallback(() => setLoginState(null), []),
224253
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/shared/src/contexts/BootProvider.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import dynamic from 'next/dynamic';
55
import type { Boot, BootApp, BootCacheData } from '../lib/boot';
66
import { getBootData } from '../lib/boot';
77
import { AuthContextProvider } from './AuthContext';
8+
import type { AuthContextProviderProps } from './AuthContext';
89
import type { AnonymousUser, LoggedUser } from '../lib/user';
910
import { AlertContextProvider } from './AlertContext';
1011
import { generateQueryKey, RequestKey, STALE_TIME } from '../lib/query';
@@ -29,6 +30,7 @@ import { LogContextProvider } from './LogContext';
2930
import { REQUEST_APP_ACCOUNT_TOKEN_MUTATION } from '../graphql/users';
3031
import { isConnectionError } from '../lib/errors';
3132
import { EngagementAdsProvider } from './EngagementAdsContext';
33+
import { useInlineLoginExperiment } from '../hooks/auth/useInlineLoginExperiment';
3234

3335
const ServerError = dynamic(
3436
() =>
@@ -44,6 +46,19 @@ const ConnectionError = dynamic(
4446
),
4547
);
4648

49+
const AuthContextProviderWithInlineLogin = (
50+
props: AuthContextProviderProps,
51+
): ReactElement => {
52+
const inlineLoginExperiment = useInlineLoginExperiment();
53+
54+
return (
55+
<AuthContextProvider
56+
{...props}
57+
inlineLoginExperiment={inlineLoginExperiment}
58+
/>
59+
);
60+
};
61+
4762
function filteredProps<T extends Record<string, unknown>>(
4863
obj: T,
4964
filteredKeys: (keyof T)[],
@@ -365,7 +380,7 @@ export const BootDataProvider = ({
365380
experimentation={cachedBootData?.exp}
366381
updateExperimentation={updateExperimentation}
367382
>
368-
<AuthContextProvider
383+
<AuthContextProviderWithInlineLogin
369384
user={user}
370385
updateUser={updateUser}
371386
tokenRefreshed={updatedAtActive > 0}
@@ -411,7 +426,7 @@ export const BootDataProvider = ({
411426
</AlertContextProvider>
412427
</EngagementAdsProvider>
413428
</SettingsContextProvider>
414-
</AuthContextProvider>
429+
</AuthContextProviderWithInlineLogin>
415430
</GrowthBookProvider>
416431
);
417432
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useCallback, useMemo, useState } from 'react';
2+
import { useFeaturesReadyContext } from '../../components/GrowthBookProvider';
3+
import type { InlineLoginExperiment } from '../../contexts/AuthContext';
4+
import { featureInlineLogin } from '../../lib/featureManagement';
5+
import { useConditionalFeature } from '../useConditionalFeature';
6+
7+
export const useInlineLoginExperiment = (): InlineLoginExperiment => {
8+
const { ready: areFeaturesReady } = useFeaturesReadyContext();
9+
const [shouldEvaluate, setShouldEvaluate] = useState(false);
10+
const requestEvaluation = useCallback(() => {
11+
setShouldEvaluate(true);
12+
}, []);
13+
const canEvaluate = shouldEvaluate && areFeaturesReady;
14+
const { value, isLoading } = useConditionalFeature({
15+
feature: featureInlineLogin,
16+
shouldEvaluate: canEvaluate,
17+
});
18+
19+
return useMemo(
20+
() => ({
21+
enabled: canEvaluate && !isLoading ? value : undefined,
22+
isLoading: shouldEvaluate && (!areFeaturesReady || isLoading),
23+
requestEvaluation,
24+
}),
25+
[
26+
areFeaturesReady,
27+
canEvaluate,
28+
isLoading,
29+
requestEvaluation,
30+
shouldEvaluate,
31+
value,
32+
],
33+
);
34+
};

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)