diff --git a/packages/shared/src/contexts/AuthContext.tsx b/packages/shared/src/contexts/AuthContext.tsx index 77d7795922..adeba0120a 100644 --- a/packages/shared/src/contexts/AuthContext.tsx +++ b/packages/shared/src/contexts/AuthContext.tsx @@ -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'; @@ -82,6 +83,8 @@ export interface AuthContextData { } const isExtension = checkIsExtension(); +const inlineLoginFeatureId = 'inline_login'; +const inlineLoginDefaultValue: boolean = false; const AuthContext = React.createContext(null); export const useAuthContext = (): AuthContextData => useContext(AuthContext); export default AuthContext; @@ -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('inline_login', true); return ( setLoginState(null), []), loginState, diff --git a/packages/shared/src/contexts/BootProvider.spec.tsx b/packages/shared/src/contexts/BootProvider.spec.tsx index 821266be25..74f905be17 100644 --- a/packages/shared/src/contexts/BootProvider.spec.tsx +++ b/packages/shared/src/contexts/BootProvider.spec.tsx @@ -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'; @@ -47,9 +49,19 @@ jest.mock('../lib/user', () => { const getRedirectUriMock = jest.fn(); +const mockUseRouter = (router: Partial = {}) => { + 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 }; @@ -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(, { + ...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(, { diff --git a/packages/webapp/pages/_app.tsx b/packages/webapp/pages/_app.tsx index cac9d52033..381a4250cd 100644 --- a/packages/webapp/pages/_app.tsx +++ b/packages/webapp/pages/_app.tsx @@ -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'; @@ -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'; @@ -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'; @@ -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', @@ -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(); @@ -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; } @@ -255,7 +259,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { router, router.pathname, isOnboardingComplete, - isInlineLoginEnabled, + inlineLoginEnabled, ]); useEffect(() => { @@ -407,7 +411,7 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { {getLayout(, pageProps, layoutProps)} - {isInlineLoginEnabled && shouldShowLogin && ( + {inlineLoginEnabled && shouldShowLogin && (