Skip to content

Commit c803893

Browse files
committed
[TOOL-4347] Dashboard: Use Nebula auth token for nebula floating chat (#6928)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on enhancing the `NebulaChatButton` component by modifying authentication handling, improving the login process, and introducing a new method for managing the Nebula authentication token. ### Detailed summary - Removed `authToken` from several components. - Updated `doLogin` function to accept `loginPayload`. - Introduced `getNebulaAuthToken` to manage token retrieval. - Replaced `Suspense` with a new `ChatContent` component for better token handling. - Added `useNebulaAuthToken` hook to fetch authentication token using React Query. - Enhanced error handling and login flow in `doNebulaLogin`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 4839173 commit c803893

File tree

8 files changed

+142
-79
lines changed

8 files changed

+142
-79
lines changed

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ The following is the user's message:
103103
networks={chain.testnet ? "testnet" : "mainnet"}
104104
isFloating={true}
105105
pageType="chain"
106-
authToken={authToken ?? undefined}
107106
label="Ask AI about this chain"
108107
client={client}
109108
nebulaParams={{

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { isAddress, isContractDeployed } from "thirdweb/utils";
1010
import { shortenIfAddress } from "utils/usedapp-external";
1111
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
1212
import {
13-
getAuthToken,
1413
getAuthTokenWalletAddress,
1514
getUserThirdwebClient,
1615
} from "../../../../api/lib/getAuthToken";
@@ -47,10 +46,7 @@ export default async function Layout(props: {
4746
notFound();
4847
}
4948

50-
const [authToken, accountAddress] = await Promise.all([
51-
getAuthToken(),
52-
getAuthTokenWalletAddress(),
53-
]);
49+
const accountAddress = await getAuthTokenWalletAddress();
5450

5551
const client = await getUserThirdwebClient();
5652
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
@@ -118,7 +114,6 @@ The following is the user's message:`;
118114
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
119115
isFloating={true}
120116
pageType="contract"
121-
authToken={authToken ?? undefined}
122117
label="Ask AI about this contract"
123118
client={client}
124119
nebulaParams={{

apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ export default async function SupportPage() {
161161
networks="all"
162162
isFloating={false}
163163
pageType="support"
164-
authToken={authToken ?? undefined}
165164
label="Ask Nebula AI for support"
166165
client={client}
167166
nebulaParams={{

apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.stories.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.

apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
44
import { Button } from "@/components/ui/button";
55
import { ToolTipLabel } from "@/components/ui/tooltip";
66
import { cn } from "@/lib/utils";
7+
import { useQuery } from "@tanstack/react-query";
78
import { useTrack } from "hooks/analytics/useTrack";
89
import { ExternalLinkIcon, RefreshCcwIcon, XIcon } from "lucide-react";
910
import Link from "next/link";
@@ -19,12 +20,12 @@ import type { ThirdwebClient } from "thirdweb";
1920
import type { NebulaContext } from "../../api/chat";
2021
import type { ExamplePrompt } from "../../data/examplePrompts";
2122
import { NebulaIcon } from "../../icons/NebulaIcon";
23+
import { getNebulaAuthToken } from "./actions";
2224

2325
const LazyFloatingChatContent = lazy(() => import("./FloatingChatContent"));
2426

2527
export function NebulaChatButton(props: {
2628
pageType: "chain" | "contract" | "support";
27-
authToken: string | undefined;
2829
examplePrompts: ExamplePrompt[];
2930
networks: NebulaContext["networks"];
3031
label: string;
@@ -97,7 +98,6 @@ export function NebulaChatButton(props: {
9798
onClose={closeModal}
9899
isOpen={isOpen}
99100
hasBeenOpened={hasBeenOpened}
100-
authToken={props.authToken}
101101
client={props.client}
102102
nebulaParams={props.nebulaParams}
103103
examplePrompts={props.examplePrompts}
@@ -111,7 +111,6 @@ function NebulaChatUIContainer(props: {
111111
onClose: () => void;
112112
isOpen: boolean;
113113
hasBeenOpened: boolean;
114-
authToken: string | undefined;
115114
examplePrompts: ExamplePrompt[];
116115
pageType: "chain" | "contract" | "support";
117116
client: ThirdwebClient;
@@ -183,29 +182,54 @@ function NebulaChatUIContainer(props: {
183182
{/* once opened keep the component mounted to preserve the states */}
184183
<div className="relative flex grow flex-col overflow-hidden">
185184
{shouldRenderChat && (
186-
<Suspense
187-
fallback={
188-
<div className="absolute inset-0 flex items-center justify-center">
189-
<Spinner className="size-10" />
190-
</div>
191-
}
192-
>
193-
<LazyFloatingChatContent
194-
networks={props.networks}
195-
authToken={props.authToken}
196-
client={props.client}
197-
nebulaParams={props.nebulaParams}
198-
key={nebulaSessionKey}
199-
examplePrompts={props.examplePrompts}
200-
pageType={props.pageType}
201-
/>
202-
</Suspense>
185+
<ChatContent
186+
sessionKey={nebulaSessionKey}
187+
networks={props.networks}
188+
client={props.client}
189+
nebulaParams={props.nebulaParams}
190+
examplePrompts={props.examplePrompts}
191+
pageType={props.pageType}
192+
/>
203193
)}
204194
</div>
205195
</div>
206196
);
207197
}
208198

199+
function ChatContent(
200+
props: Omit<
201+
React.ComponentProps<typeof LazyFloatingChatContent>,
202+
"authToken"
203+
> & {
204+
sessionKey: number;
205+
},
206+
) {
207+
const { sessionKey, ...restProps } = props;
208+
const nebulaAuthTokenQuery = useNebulaAuthToken();
209+
210+
if (nebulaAuthTokenQuery.isPending) {
211+
return <LoadingScreen />;
212+
}
213+
214+
return (
215+
<Suspense fallback={<LoadingScreen />}>
216+
<LazyFloatingChatContent
217+
key={sessionKey}
218+
{...restProps}
219+
authToken={nebulaAuthTokenQuery.data ?? undefined}
220+
/>
221+
</Suspense>
222+
);
223+
}
224+
225+
function LoadingScreen() {
226+
return (
227+
<div className="absolute inset-0 flex items-center justify-center">
228+
<Spinner className="size-10" />
229+
</div>
230+
);
231+
}
232+
209233
function useOutsideClick(onOutsideClick: () => void) {
210234
const ref = useRef<HTMLDivElement>(null);
211235

@@ -234,3 +258,17 @@ function useOutsideClick(onOutsideClick: () => void) {
234258

235259
return ref;
236260
}
261+
262+
function useNebulaAuthToken() {
263+
return useQuery({
264+
queryKey: ["nebula-auth-token"],
265+
queryFn: async () => {
266+
const jwt = await getNebulaAuthToken();
267+
return jwt || null;
268+
},
269+
retry: false,
270+
refetchOnWindowFocus: false,
271+
refetchOnMount: false,
272+
refetchOnReconnect: false,
273+
});
274+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use server";
2+
3+
import {
4+
getAuthToken,
5+
getAuthTokenWalletAddress,
6+
} from "../../../../(app)/api/lib/getAuthToken";
7+
import { getNebulaLoginStatus } from "../../../_utils/isLoggedIntoNebula";
8+
import {
9+
doNebulaLogin,
10+
getNebulaLoginPayload,
11+
} from "../../../login/auth-actions";
12+
13+
export async function getNebulaAuthToken() {
14+
const [dashboardAuthToken, dashboardAuthTokenAddress] = await Promise.all([
15+
getAuthToken(),
16+
getAuthTokenWalletAddress(),
17+
]);
18+
19+
// if not logged in to dashboard
20+
if (!dashboardAuthToken || !dashboardAuthTokenAddress) {
21+
return undefined;
22+
}
23+
24+
const nebulaLoginStatus = await getNebulaLoginStatus();
25+
26+
// if already logged in to nebula
27+
if (nebulaLoginStatus.isLoggedIn) {
28+
return nebulaLoginStatus.authToken;
29+
}
30+
31+
// automatically login to nebula with the dashboard auth token ---
32+
const loginPayload = await getNebulaLoginPayload({
33+
address: dashboardAuthTokenAddress,
34+
chainId: 1,
35+
});
36+
37+
if (!loginPayload) {
38+
return undefined;
39+
}
40+
41+
const result = await doNebulaLogin({
42+
type: "floating-chat",
43+
loginPayload: {
44+
payload: loginPayload,
45+
token: dashboardAuthToken,
46+
},
47+
});
48+
49+
if (result.success) {
50+
return result.token;
51+
}
52+
53+
return undefined;
54+
}

apps/dashboard/src/app/nebula-app/login/NebulaConnectEmbedLogin.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,18 @@ function CustomConnectEmbed(props: {
132132
<ConnectEmbed
133133
auth={{
134134
getLoginPayload: getNebulaLoginPayload,
135-
doLogin: async (params) => {
135+
doLogin: async (loginPayload) => {
136136
if (isVercel() && !turnstileToken) {
137137
setAlwaysShowTurnstile(true);
138138
throw new Error("Please complete the captcha.");
139139
}
140140

141141
try {
142-
const result = await doNebulaLogin(params, turnstileToken);
142+
const result = await doNebulaLogin({
143+
type: "nebula-app",
144+
loginPayload: loginPayload,
145+
turnstileToken,
146+
});
143147
if (result.error) {
144148
console.error(
145149
"Failed to login",

apps/dashboard/src/app/nebula-app/login/auth-actions.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,29 @@ export async function getNebulaLoginPayload(
4242
}
4343

4444
export async function doNebulaLogin(
45-
payload: VerifyLoginPayloadParams,
46-
turnstileToken: string | undefined,
45+
params:
46+
| {
47+
type: "nebula-app";
48+
loginPayload: VerifyLoginPayloadParams;
49+
turnstileToken: string | undefined;
50+
}
51+
| {
52+
type: "floating-chat";
53+
loginPayload: {
54+
payload: LoginPayload;
55+
token: string;
56+
};
57+
},
4758
) {
4859
// only validate the turnstile token if we are in a vercel environment
49-
if (isVercel()) {
50-
if (!turnstileToken) {
60+
if (params.type === "nebula-app" && isVercel()) {
61+
if (!params.turnstileToken) {
5162
return {
5263
error: "Please complete the captcha.",
5364
};
5465
}
5566

56-
const result = await verifyTurnstileToken(turnstileToken);
67+
const result = await verifyTurnstileToken(params.turnstileToken);
5768
if (!result.success) {
5869
return {
5970
error: "Invalid captcha. Please try again.",
@@ -71,15 +82,16 @@ export async function doNebulaLogin(
7182
"Content-Type": "application/json",
7283
"X-Secret-Key": NEBULA_APP_SECRET_KEY,
7384
},
74-
body: JSON.stringify(payload),
85+
body: JSON.stringify(params.loginPayload),
7586
});
7687

7788
// if the request failed, log the error and throw an error
7889
if (!res.ok) {
7990
try {
8091
// clear the cookies to prevent any weird issues
8192
cookieStore.delete(
82-
NEBULA_COOKIE_PREFIX_TOKEN + getAddress(payload.payload.address),
93+
NEBULA_COOKIE_PREFIX_TOKEN +
94+
getAddress(params.loginPayload.payload.address),
8395
);
8496
cookieStore.delete(NEBULA_COOKIE_ACTIVE_ACCOUNT);
8597
} catch {
@@ -127,7 +139,8 @@ export async function doNebulaLogin(
127139

128140
// set the token cookie
129141
cookieStore.set(
130-
NEBULA_COOKIE_PREFIX_TOKEN + getAddress(payload.payload.address),
142+
NEBULA_COOKIE_PREFIX_TOKEN +
143+
getAddress(params.loginPayload.payload.address),
131144
jwt,
132145
{
133146
httpOnly: true,
@@ -140,7 +153,7 @@ export async function doNebulaLogin(
140153
// set the active account cookie
141154
cookieStore.set(
142155
NEBULA_COOKIE_ACTIVE_ACCOUNT,
143-
getAddress(payload.payload.address),
156+
getAddress(params.loginPayload.payload.address),
144157
{
145158
httpOnly: true,
146159
secure: true,
@@ -151,6 +164,7 @@ export async function doNebulaLogin(
151164

152165
return {
153166
success: true,
167+
token: jwt,
154168
};
155169
}
156170

0 commit comments

Comments
 (0)