Skip to content

Commit 7a0103b

Browse files
msukkariclaude
andcommitted
refactor(web): move askgh login wall into useCreateNewChatThread hook
Centralizes the login wall logic in the hook so all chat creation entry points are guarded automatically. This adds the login wall to the general /chat landing page and removes the duplicate manual logic from the askgh landing page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 11184ea commit 7a0103b

File tree

5 files changed

+83
-68
lines changed

5 files changed

+83
-68
lines changed

packages/web/src/app/[domain]/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,14 @@ import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfigur
1010
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
1111
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
1212
import { getRepoImageSrc } from '@/lib/utils';
13-
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
14-
import { Descendant, Transforms } from "slate";
15-
import { useSlate } from "slate-react";
16-
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
17-
import { captureEvent } from "@/hooks/useCaptureEvent";
18-
19-
const PENDING_MESSAGE_KEY = "askgh_pending_message";
13+
import { useMemo, useState } from "react";
2014

2115
interface LandingPageProps {
2216
languageModels: LanguageModelInfo[];
2317
repoName: string;
2418
repoDisplayName?: string;
2519
imageUrl?: string | null;
2620
repoId: number;
27-
providers: IdentityProviderMetadata[];
2821
isAuthenticated: boolean;
2922
}
3023

@@ -34,14 +27,10 @@ export const LandingPage = ({
3427
repoDisplayName,
3528
imageUrl,
3629
repoId,
37-
providers,
3830
isAuthenticated,
3931
}: LandingPageProps) => {
40-
const editor = useSlate();
41-
const { createNewChatThread, isLoading } = useCreateNewChatThread();
32+
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
4233
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
43-
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
44-
const hasRestoredPendingMessage = useRef(false);
4534
const isChatBoxDisabled = languageModels.length === 0;
4635

4736
const selectedSearchScopes = useMemo(() => [
@@ -53,45 +42,6 @@ export const LandingPage = ({
5342
} satisfies RepoSearchScope,
5443
], [repoDisplayName, repoName]);
5544

56-
// Intercept submit to check auth status
57-
const handleSubmit = useCallback((children: Descendant[]) => {
58-
if (!isAuthenticated) {
59-
captureEvent('wa_askgh_login_wall_prompted', {});
60-
// Store message in sessionStorage to survive OAuth redirect
61-
sessionStorage.setItem(PENDING_MESSAGE_KEY, JSON.stringify(children));
62-
setIsLoginModalOpen(true);
63-
return;
64-
}
65-
createNewChatThread(children, selectedSearchScopes);
66-
}, [isAuthenticated, createNewChatThread, selectedSearchScopes]);
67-
68-
// Restore pending message to editor and auto-submit after login
69-
useEffect(() => {
70-
if (isAuthenticated && !hasRestoredPendingMessage.current) {
71-
const stored = sessionStorage.getItem(PENDING_MESSAGE_KEY);
72-
if (stored) {
73-
hasRestoredPendingMessage.current = true;
74-
sessionStorage.removeItem(PENDING_MESSAGE_KEY);
75-
try {
76-
const message = JSON.parse(stored) as Descendant[];
77-
78-
// Restore the message content to the editor by replacing all nodes
79-
// Remove all existing nodes
80-
while (editor.children.length > 0) {
81-
Transforms.removeNodes(editor, { at: [0] });
82-
}
83-
// Insert the restored content at the beginning
84-
Transforms.insertNodes(editor, message, { at: [0] });
85-
86-
// Allow the UI to render the restored text before auto-submitting
87-
createNewChatThread(message, selectedSearchScopes);
88-
} catch (error) {
89-
console.error('Failed to restore pending message:', error);
90-
}
91-
}
92-
}
93-
}, [isAuthenticated, editor, createNewChatThread, selectedSearchScopes]);
94-
9545
const imageSrc = imageUrl ? getRepoImageSrc(imageUrl, repoId) : undefined;
9646
const displayName = repoDisplayName ?? repoName;
9747

@@ -119,7 +69,9 @@ export const LandingPage = ({
11969
<div className="w-full max-w-[800px]">
12070
<div className="border rounded-md w-full shadow-sm">
12171
<ChatBox
122-
onSubmit={handleSubmit}
72+
onSubmit={(children) => {
73+
createNewChatThread(children, selectedSearchScopes);
74+
}}
12375
className="min-h-[50px]"
12476
isRedirecting={isLoading}
12577
languageModels={languageModels}
@@ -155,11 +107,11 @@ export const LandingPage = ({
155107
</div>
156108

157109
<LoginModal
158-
isOpen={isLoginModalOpen}
159-
onOpenChange={setIsLoginModalOpen}
160-
providers={providers}
110+
isOpen={loginWall.isOpen}
111+
onOpenChange={loginWall.onOpenChange}
112+
providers={loginWall.providers}
161113
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
162114
/>
163115
</div>
164116
)
165-
}
117+
}

packages/web/src/app/[domain]/askgh/[owner]/[repo]/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
88
import { RepoIndexedGuard } from "./components/repoIndexedGuard";
99
import { LandingPage } from "./components/landingPage";
1010
import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
11-
import { getIdentityProviderMetadata } from "@/lib/identityProviders";
1211
import { auth } from "@/auth";
1312

1413
interface PageProps {
@@ -48,7 +47,6 @@ export default async function GitHubRepoPage(props: PageProps) {
4847

4948
const repoInfo = await unwrapServiceError(getRepoInfo(repoId));
5049
const languageModels = await unwrapServiceError(getConfiguredLanguageModelsInfo());
51-
const providers = getIdentityProviderMetadata();
5250

5351
return (
5452
<RepoIndexedGuard initialRepoInfo={repoInfo}>
@@ -59,7 +57,6 @@ export default async function GitHubRepoPage(props: PageProps) {
5957
repoDisplayName={repoInfo.displayName ?? undefined}
6058
imageUrl={repoInfo.imageUrl ?? undefined}
6159
repoId={repoInfo.id}
62-
providers={providers}
6360
isAuthenticated={!!session?.user}
6461
/>
6562
</CustomSlateEditor>

packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ import { useState } from "react";
1010
import { useLocalStorage } from "usehooks-ts";
1111
import { SearchModeSelector } from "../../components/searchModeSelector";
1212
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
13+
import { LoginModal } from "@/app/components/loginModal";
1314

1415
interface LandingPageChatBox {
1516
languageModels: LanguageModelInfo[];
1617
repos: RepositoryQuery[];
1718
searchContexts: SearchContextQuery[];
19+
isAuthenticated: boolean;
1820
}
1921

2022
export const LandingPageChatBox = ({
2123
languageModels,
2224
repos,
2325
searchContexts,
26+
isAuthenticated,
2427
}: LandingPageChatBox) => {
25-
const { createNewChatThread, isLoading } = useCreateNewChatThread();
28+
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
2629
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
2730
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
2831
const isChatBoxDisabled = languageModels.length === 0;
@@ -65,6 +68,13 @@ export const LandingPageChatBox = ({
6568
{isChatBoxDisabled && (
6669
<NotConfiguredErrorBanner className="mt-4" />
6770
)}
71+
72+
<LoginModal
73+
isOpen={loginWall.isOpen}
74+
onOpenChange={loginWall.onOpenChange}
75+
providers={loginWall.providers}
76+
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
77+
/>
6878
</div >
6979
)
70-
}
80+
}

packages/web/src/app/[domain]/chat/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export default async function Page(props: PageProps) {
103103
languageModels={languageModels}
104104
repos={allRepos}
105105
searchContexts={searchContexts}
106+
isAuthenticated={!!session}
106107
/>
107108
</CustomSlateEditor>
108109

packages/web/src/features/chat/useCreateNewChatThread.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
'use client';
22

3-
import { useCallback, useState } from "react";
3+
import { useCallback, useEffect, useRef, useState } from "react";
44
import { Descendant } from "slate";
55
import { createUIMessage, getAllMentionElements } from "./utils";
66
import { slateContentToString } from "./utils";
77
import { useToast } from "@/components/hooks/use-toast";
88
import { useRouter } from "next/navigation";
9-
import { createChat } from "./actions";
9+
import { createChat, getAskGhLoginWallData } from "./actions";
1010
import { isServiceError } from "@/lib/utils";
1111
import { createPathWithQueryParams } from "@/lib/utils";
1212
import { SearchScope, SET_CHAT_STATE_SESSION_STORAGE_KEY, SetChatStatePayload } from "./types";
1313
import { useSessionStorage } from "usehooks-ts";
1414
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
15+
import type { IdentityProviderMetadata } from "@/lib/identityProviders";
16+
import useCaptureEvent from "@/hooks/useCaptureEvent";
1517

16-
export const useCreateNewChatThread = () => {
18+
const PENDING_NEW_CHAT_KEY = "askgh_pending_new_chat";
19+
20+
interface UseCreateNewChatThreadOptions {
21+
isAuthenticated?: boolean;
22+
}
23+
24+
export const useCreateNewChatThread = ({ isAuthenticated = false }: UseCreateNewChatThreadOptions = {}) => {
1725
const [isLoading, setIsLoading] = useState(false);
1826
const { toast } = useToast();
1927
const router = useRouter();
2028
const [, setChatState] = useSessionStorage<SetChatStatePayload | null>(SET_CHAT_STATE_SESSION_STORAGE_KEY, null);
29+
const [loginWallState, setLoginWallState] = useState<{ isOpen: boolean; providers: IdentityProviderMetadata[] }>({ isOpen: false, providers: [] });
30+
const hasRestoredPendingMessage = useRef(false);
31+
const captureEvent = useCaptureEvent();
2132

22-
const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => {
33+
const doCreateChat = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => {
2334
const text = slateContentToString(children);
2435
const mentions = getAllMentionElements(children);
25-
36+
2637
const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedSearchScopes);
2738

2839
setIsLoading(true);
@@ -46,8 +57,52 @@ export const useCreateNewChatThread = () => {
4657
router.refresh();
4758
}, [router, toast, setChatState]);
4859

60+
const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => {
61+
if (!isAuthenticated) {
62+
const result = await getAskGhLoginWallData();
63+
if (!isServiceError(result) && result.isEnabled) {
64+
captureEvent('wa_askgh_login_wall_prompted', {});
65+
sessionStorage.setItem(PENDING_NEW_CHAT_KEY, JSON.stringify({ children, selectedSearchScopes }));
66+
setLoginWallState({ isOpen: true, providers: result.providers });
67+
return;
68+
}
69+
}
70+
71+
doCreateChat(children, selectedSearchScopes);
72+
}, [isAuthenticated, captureEvent, doCreateChat]);
73+
74+
// Restore pending message after OAuth redirect
75+
useEffect(() => {
76+
if (!isAuthenticated || hasRestoredPendingMessage.current) {
77+
return;
78+
}
79+
80+
const stored = sessionStorage.getItem(PENDING_NEW_CHAT_KEY);
81+
if (!stored) {
82+
return;
83+
}
84+
85+
hasRestoredPendingMessage.current = true;
86+
sessionStorage.removeItem(PENDING_NEW_CHAT_KEY);
87+
88+
try {
89+
const { children, selectedSearchScopes } = JSON.parse(stored) as {
90+
children: Descendant[];
91+
selectedSearchScopes: SearchScope[];
92+
};
93+
doCreateChat(children, selectedSearchScopes);
94+
} catch (error) {
95+
console.error('Failed to restore pending message:', error);
96+
}
97+
}, [isAuthenticated, doCreateChat]);
98+
4999
return {
50100
createNewChatThread,
51101
isLoading,
102+
loginWall: {
103+
isOpen: loginWallState.isOpen,
104+
providers: loginWallState.providers,
105+
onOpenChange: (open: boolean) => setLoginWallState(prev => ({ ...prev, isOpen: open })),
106+
},
52107
};
53-
}
108+
}

0 commit comments

Comments
 (0)