Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions packages/shared/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import React, {
} from 'react';
import type { QueryObserverResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useFeatureValue } from '@growthbook/growthbook-react';
import type { GrowthBookContextValue } from '@growthbook/growthbook-react';
import { GrowthBookContext } from '@growthbook/growthbook-react';
import type { AnonymousUser, LoggedUser } from '../lib/user';
import { deleteAccount, logout as dispatchLogout } from '../lib/user';
import type { AccessToken, Boot, Visit } from '../lib/boot';
Expand Down Expand Up @@ -82,6 +83,8 @@ export interface AuthContextData {
}

const isExtension = checkIsExtension();
const inlineLoginFeatureId = 'inline_login';
const inlineLoginDefaultValue: boolean = false;
const AuthContext = React.createContext<AuthContextData>(null);
export const useAuthContext = (): AuthContextData => useContext(AuthContext);
export default AuthContext;
Expand Down Expand Up @@ -164,15 +167,14 @@ export const AuthContextProvider = ({
const referralOrigin = user?.referralOrigin;
const router = useRouter();
const isFunnelRef = useRef(!!router?.pathname?.startsWith(webFunnelPrefix));
const growthbookContext = useContext(
GrowthBookContext,
) as unknown as GrowthBookContextValue;
const growthbook = growthbookContext?.growthbook;
const isValidRegion = useMemo(
() => !invalidPlusRegions.includes(geo?.region),
[geo?.region],
);
// Inline-login experiment flag. Source of truth for the local default lives
// in `lib/featureManagement.ts` as `featureInlineLogin`. We can't import it
// here because `featureManagement` → `graphql/posts` → `AuthContext` would
// be a cycle, so the default is duplicated below; keep them in sync.
const isInlineLoginEnabled = useFeatureValue<boolean>('inline_login', true);

return (
<AuthContext.Provider
Expand All @@ -196,6 +198,12 @@ export const AuthContextProvider = ({
}

const params = new URLSearchParams(globalThis?.location.search);
const shouldUseInlineLogin =
!isExtension &&
growthbook?.getFeatureValue(
inlineLoginFeatureId,
inlineLoginDefaultValue,
) === true;

setLoginState({ ...options, trigger });
if (isExtension) {
Expand All @@ -206,19 +214,20 @@ export const AuthContextProvider = ({
params.set(AFTER_AUTH_PARAM, window.location.pathname);
}

const onboardingPath = isExtension
? `${onboardingUrl}?${params.toString()}`
: `/onboarding?${params.toString()}`;

// Inline login experiment: render the modal in-place instead of
// redirecting to /onboarding. Extension keeps the redirect because
// it has no host page to mount the modal on.
if (isInlineLoginEnabled && !isExtension) {
if (shouldUseInlineLogin) {
return;
}

const onboardingPath = `${onboardingUrl}?${params.toString()}`;
router.push(
isExtension ? onboardingPath : `/onboarding?${params.toString()}`,
);
router.push(onboardingPath);
},
[router, isInlineLoginEnabled],
[growthbook, router],
),
closeLogin: useCallback(() => setLoginState(null), []),
loginState,
Expand Down
47 changes: 47 additions & 0 deletions packages/shared/src/contexts/BootProvider.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ReactNode } from 'react';
import React, { useContext } from 'react';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import nock from 'nock';
import type { RenderResult } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
Expand Down Expand Up @@ -47,9 +49,19 @@ jest.mock('../lib/user', () => {

const getRedirectUriMock = jest.fn();

const mockUseRouter = (router: Partial<NextRouter> = {}) => {
jest.mocked(useRouter).mockReturnValue({
query: {},
push: jest.fn(),
pathname: '/',
...router,
} as unknown as NextRouter);
};

beforeEach(() => {
nock.cleanAll();
localStorage.clear();
mockUseRouter();
});

const defaultAlerts: Alerts = { filter: true, rankLastSeen: undefined };
Expand Down Expand Up @@ -501,6 +513,41 @@ it('should trigger show login callback', async () => {
await expectToHaveTestValue(login, JSON.stringify({ trigger: expected }));
});

it('should keep inline login on page when enabled after auth intent', async () => {
const push = jest.fn();
mockUseRouter({
push,
pathname: '/posts/shared',
});

renderComponent(<AuthMock loginTrigger={AuthTriggers.Comment} />, {
...defaultBootData,
user: defaultAnonymousUser,
exp: {
f: '{}',
e: [],
a: [],
features: {
inline_login: {
defaultValue: true,
},
},
},
});

const login = await screen.findByText('Log in');
await expectToHaveTestValue(login, 'null');
expect(push).not.toHaveBeenCalled();

fireEvent.click(login);

await expectToHaveTestValue(
login,
JSON.stringify({ trigger: AuthTriggers.Comment }),
);
expect(push).not.toHaveBeenCalled();
});

it('should trigger close login callback', async () => {
const expected = AuthTriggers.Comment;
renderComponent(<AuthMock loginTrigger={expected} />, {
Expand Down
28 changes: 16 additions & 12 deletions packages/webapp/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ProgressiveEnhancementContextProvider } from '@dailydotdev/shared/src/c
import { SubscriptionContextProvider } from '@dailydotdev/shared/src/contexts/SubscriptionContext';
import { ShortcutsProvider } from '@dailydotdev/shared/src/features/shortcuts/contexts/ShortcutsProvider';
import { canonicalFromRouter } from '@dailydotdev/shared/src/lib/canonical';
import { featureInlineLogin } from '@dailydotdev/shared/src/lib/featureManagement';
import '@dailydotdev/shared/src/styles/globals.css';
import useLogPageView from '@dailydotdev/shared/src/hooks/log/useLogPageView';
import { BootDataProvider } from '@dailydotdev/shared/src/contexts/BootProvider';
Expand All @@ -36,7 +37,10 @@ import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/type
import { defaultQueryClientConfig } from '@dailydotdev/shared/src/lib/query';
import { useWebVitals } from '@dailydotdev/shared/src/hooks/useWebVitals';
import { LazyModalElement } from '@dailydotdev/shared/src/components/modals/LazyModalElement';
import { useManualScrollRestoration } from '@dailydotdev/shared/src/hooks';
import {
useConditionalFeature,
useManualScrollRestoration,
} from '@dailydotdev/shared/src/hooks';
import { useScrollbarWidth } from '@dailydotdev/shared/src/hooks/useScrollbarWidth';
import { PushNotificationContextProvider } from '@dailydotdev/shared/src/contexts/PushNotificationContext';
import { SerwistProvider } from '@serwist/turbopack/react';
Expand All @@ -52,8 +56,6 @@ import {
WebKitMessageHandlers,
} from '@dailydotdev/shared/src/lib/ios';
import { useCheckLocation } from '@dailydotdev/shared/src/hooks/useCheckLocation';
import { useFeature } from '@dailydotdev/shared/src/components/GrowthBookProvider';
import { featureInlineLogin } from '@dailydotdev/shared/src/lib/featureManagement';
import Seo, { defaultSeo, defaultSeoTitle } from '../next-seo';
import useWebappVersion from '../hooks/useWebappVersion';
import { getAppOrigin, getSiteOrigin } from '../lib/seo';
Expand Down Expand Up @@ -104,9 +106,8 @@ const onboardingExcludedPaths = [
'/jobs',
'/settings',
];
// When the inline_login experiment is on, we only force the rest of onboarding
// when the user lands on the main feed — everywhere else they can keep
// browsing after the inline first step.
// While inline_login is active for an auth intent, only force the rest of
// onboarding when the user lands on the main feed.
const mainFeedPathnames = new Set([
'/',
'/popular',
Expand Down Expand Up @@ -178,7 +179,10 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
closeLogin,
loginState,
} = useAuthContext();
const isInlineLoginEnabled = useFeature(featureInlineLogin);
const { value: inlineLoginEnabled } = useConditionalFeature({
feature: featureInlineLogin,
shouldEvaluate: shouldShowLogin,
});
const { showBanner, onAcceptCookies, onOpenBanner, onHideBanner } =
useCookieBanner();
useWebVitals();
Expand Down Expand Up @@ -240,9 +244,9 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
return;
}

// Inline login experiment: defer the rest of onboarding until the user
// navigates to the main feed; otherwise let them keep browsing.
if (isInlineLoginEnabled && !mainFeedPathnames.has(router.pathname)) {
// Inline login experiment: while the auth intent is active, defer the rest
// of onboarding until they navigate to the main feed.
if (inlineLoginEnabled && !mainFeedPathnames.has(router.pathname)) {
return;
}

Expand All @@ -255,7 +259,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
router,
router.pathname,
isOnboardingComplete,
isInlineLoginEnabled,
inlineLoginEnabled,
]);

useEffect(() => {
Expand Down Expand Up @@ -407,7 +411,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement {
<DndContextProvider>
{getLayout(<Component {...pageProps} />, pageProps, layoutProps)}
</DndContextProvider>
{isInlineLoginEnabled && shouldShowLogin && (
{inlineLoginEnabled && shouldShowLogin && (
<AuthModal
isOpen={shouldShowLogin}
onRequestClose={closeLogin}
Expand Down
Loading