Skip to content

Commit 356f574

Browse files
committed
refactor: enhance authentication flow and UI components, update analytics
1 parent ede9748 commit 356f574

File tree

8 files changed

+257
-73
lines changed

8 files changed

+257
-73
lines changed

src/frontend/src/App.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useSaveCanvas } from "./api/hooks";
88
import type * as TExcalidraw from "@excalidraw/excalidraw";
99
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
1010
import type { ExcalidrawImperativeAPI, AppState } from "@excalidraw/excalidraw/types";
11-
import AuthModal from "./auth/AuthModal";
1211
import { useAuthCheck } from "./api/hooks";
1312

1413
export interface AppProps {
@@ -26,10 +25,7 @@ export default function App({
2625
}: AppProps) {
2726
const { useHandleLibrary, MainMenu } = excalidrawLib;
2827

29-
// Get authentication state from React Query
3028
const { data: isAuthenticated, isLoading: isAuthLoading } = useAuthCheck();
31-
32-
// Get user profile for analytics identification
3329
const { data: userProfile } = useUserProfile();
3430

3531
// Only enable canvas queries if authenticated and not loading
@@ -44,12 +40,10 @@ export default function App({
4440
useCustom(excalidrawAPI, customArgs);
4541
useHandleLibrary({ excalidrawAPI });
4642

47-
// On login and canvas data load, update the scene
48-
// Helper to ensure collaborators is a Map
4943
function normalizeCanvasData(data: any) {
5044
if (!data) return data;
5145
const appState = { ...data.appState };
52-
// Remove width and height so they get recomputed when loading from DB
46+
appState.width = undefined;
5347
if ("width" in appState) {
5448
delete appState.width;
5549
}
@@ -68,40 +62,30 @@ export default function App({
6862
}
6963
}, [excalidrawAPI, canvasData]);
7064

71-
// Use React Query mutation for saving canvas
7265
const { mutate: saveCanvas } = useSaveCanvas({
7366
onSuccess: () => {
74-
console.debug("Canvas saved to database successfully");
75-
// Track canvas save event with PostHog
76-
capture('canvas_saved');
67+
console.debug("[pad.ws] Canvas saved to database successfully");
7768
},
7869
onError: (error) => {
79-
console.error("Failed to save canvas to database:", error);
80-
// Track canvas save failure
81-
capture('canvas_save_failed', {
82-
error: error instanceof Error ? error.message : 'Unknown error'
83-
});
70+
console.error("[pad.ws] Failed to save canvas to database:", error);
8471
}
8572
});
8673

8774
useEffect(() => {
8875
if (excalidrawAPI) {
8976
(window as any).excalidrawAPI = excalidrawAPI;
90-
// Track application loaded event
9177
capture('app_loaded');
9278
}
9379
return () => {
9480
(window as any).excalidrawAPI = null;
9581
};
9682
}, [excalidrawAPI]);
9783

98-
// Ref to store the last sent canvas data for change detection
9984
const lastSentCanvasDataRef = useRef<string>("");
10085

10186
const debouncedLogChange = useCallback(
10287
debounce(
10388
(elements: NonDeletedExcalidrawElement[], state: AppState, files: any) => {
104-
// Only save if authenticated
10589
if (!isAuthenticated) return;
10690

10791
const canvasData = {
@@ -110,11 +94,9 @@ export default function App({
11094
files
11195
};
11296

113-
// Compare with last sent data (deep equality via JSON.stringify)
11497
const serialized = JSON.stringify(canvasData);
11598
if (serialized !== lastSentCanvasDataRef.current) {
11699
lastSentCanvasDataRef.current = serialized;
117-
// Use React Query mutation to save canvas
118100
saveCanvas(canvasData);
119101
}
120102
},
@@ -123,12 +105,18 @@ export default function App({
123105
[saveCanvas, isAuthenticated]
124106
);
125107

126-
// Identify user in PostHog when username is available
127108
useEffect(() => {
128-
if (userProfile?.username) {
129-
posthog.identify(userProfile.username);
109+
if (userProfile?.id) {
110+
posthog.identify(userProfile.id);
111+
if (posthog.people && typeof posthog.people.set === "function") {
112+
const {
113+
id, // do not include in properties
114+
...personProps
115+
} = userProfile;
116+
posthog.people.set(personProps);
117+
}
130118
}
131-
}, [userProfile?.username]);
119+
}, [userProfile]);
132120

133121
return (
134122
<>
@@ -141,7 +129,6 @@ export default function App({
141129
{children}
142130
</ExcalidrawWrapper>
143131

144-
{/* AuthModal is now handled by AuthGate */}
145132
</>
146133
);
147134
}

src/frontend/src/AuthGate.tsx

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,60 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import { useAuthCheck } from "./api/hooks";
33
import AuthModal from "./auth/AuthModal";
44

55
/**
6-
* AuthGate ensures the authentication check is the very first XHR request.
7-
* It blocks rendering of children until the auth check completes.
8-
* If unauthenticated, it shows the AuthModal.
6+
* If unauthenticated, it shows the AuthModal as an overlay, but still renders the app behind it.
7+
*
8+
* If authenticated, it silently primes the Coder OIDC session by loading
9+
* the OIDC callback endpoint in a hidden iframe. This is a workaround:
10+
* without this, users would see the Coder login screen when opening an embedded terminal.
11+
*
12+
* The iframe is removed as soon as it loads, or after a fallback timeout.
913
*/
1014
export default function AuthGate({ children }: { children: React.ReactNode }) {
1115
const { data: isAuthenticated, isLoading } = useAuthCheck();
1216
const [coderAuthDone, setCoderAuthDone] = useState(false);
17+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
18+
const timeoutRef = useRef<number | null>(null);
1319

1420
useEffect(() => {
15-
// When authenticated, also authenticate with Coder using an iframe
21+
// Only run the Coder OIDC priming once per session, after auth is confirmed
1622
if (isAuthenticated === true && !coderAuthDone) {
17-
// Create a hidden iframe to handle Coder authentication
18-
const iframe = document.createElement('iframe');
19-
iframe.style.display = 'none';
20-
iframe.src = 'https://coder.pad.ws/api/v2/users/oidc/callback';
23+
const iframe = document.createElement("iframe");
24+
iframe.style.display = "none";
25+
iframe.src = "https://coder.pad.ws/api/v2/users/oidc/callback";
2126

22-
// Add the iframe to the document
27+
// Remove iframe as soon as it loads, or after 2s fallback
28+
const cleanup = () => {
29+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
30+
setCoderAuthDone(true);
31+
};
32+
33+
iframe.onload = cleanup;
2334
document.body.appendChild(iframe);
35+
iframeRef.current = iframe;
2436

25-
// Clean up after a short delay
26-
setTimeout(() => {
27-
document.body.removeChild(iframe);
28-
setCoderAuthDone(true);
29-
}, 2000); // 2 seconds should be enough for the auth to complete
37+
// Fallback: remove iframe after 5s if onload doesn't fire
38+
timeoutRef.current = window.setTimeout(cleanup, 5000);
39+
40+
// Cleanup on unmount or re-run
41+
return () => {
42+
if (iframeRef.current && iframeRef.current.parentNode) {
43+
iframeRef.current.parentNode.removeChild(iframeRef.current);
44+
}
45+
if (timeoutRef.current) {
46+
clearTimeout(timeoutRef.current);
47+
}
48+
};
3049
}
50+
// eslint-disable-next-line react-hooks/exhaustive-deps
3151
}, [isAuthenticated, coderAuthDone]);
3252

33-
// Always render children (App), but overlay AuthModal if unauthenticated
53+
// Always render children; overlay AuthModal if not authenticated
3454
return (
3555
<>
3656
{children}
37-
{isAuthenticated === false && !isLoading && <AuthModal />}
57+
{isAuthenticated === false && <AuthModal />}
3858
</>
3959
);
4060
}

src/frontend/src/CustomEmbeddableRenderer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const renderCustomEmbeddable = (
3838
default:
3939
return null;
4040
}
41+
} else {
42+
return <iframe className="custom-rendered-embeddable" src={element.link} />;
4143
}
42-
43-
return null;
4444
};

src/frontend/src/ExcalidrawWrapper.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import React, { Children, cloneElement } from 'react';
2-
import DiscordButton from './components/DiscordButton';
3-
import FeedbackButton from './components/FeedbackButton';
2+
import DiscordButton from './ui/DiscordButton';
3+
import FeedbackButton from './ui/FeedbackButton';
44
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types';
55
import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/element/types';
66
import type { AppState } from '@excalidraw/excalidraw/types';
7-
import { MainMenuConfig } from './MainMenu';
7+
import { MainMenuConfig } from './ui/MainMenu';
88
import { renderCustomEmbeddable } from './CustomEmbeddableRenderer';
99

1010
const defaultInitialData = {
1111
elements: [],
1212
appState: {
1313
gridModeEnabled: true,
14-
gridSize: 20,
14+
gridSize: 40,
1515
gridStep: 5,
1616
},
1717
files: {},

src/frontend/src/auth/AuthModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState, useEffect } from "react";
22
import ReactDOM from "react-dom";
3+
import { capture } from "../utils/posthog";
34
import "../styles/AuthModal.scss";
45

56
interface AuthModalProps {
@@ -9,14 +10,14 @@ interface AuthModalProps {
910
}
1011

1112
const AuthModal: React.FC<AuthModalProps> = ({
12-
title = "pad.ws",
13-
description = "Welcome\n\nPad is a browser-based developer environment where you can take notes and code side to side.\n\nYour workspace is an Ubuntu virtual machine that we run for you in the cloud",
13+
description = "This is a browser-based developer environment where you can take notes and code side to side.\n\nYour workspace is an Ubuntu virtual machine that we run for you in the cloud",
1414
infoText = "🚧 This is a beta. Consider all data is temporary and might be deleted at any time while we build",
1515
}) => {
1616
const [isMounted, setIsMounted] = useState(false);
1717

1818
useEffect(() => {
1919
setIsMounted(true);
20+
capture("auth_modal_shown");
2021
// Prevent scrolling when modal is open
2122
document.body.style.overflow = "hidden";
2223

src/frontend/src/pad/buttons/ActionButton.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
315315

316316
const executeAction = () => {
317317
// Track button click event with PostHog
318-
capture('button_clicked', {
318+
capture('custom_button_clicked', {
319319
target: selectedTarget,
320320
action: selectedAction,
321321
codeVariant: selectedTarget === 'code' ? selectedCodeVariant : null
@@ -328,7 +328,6 @@ const ActionButton: React.FC<ActionButtonProps> = ({
328328
return;
329329
}
330330

331-
const elements = excalidrawAPI.getSceneElements();
332331
const baseUrl = getUrl();
333332

334333
if (!baseUrl) {
@@ -347,7 +346,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
347346
scrollToView: true
348347
});
349348

350-
console.debug(`Embedded ${selectedTarget} at URL: ${baseUrl}`);
349+
console.debug(`[pad.ws] Embedded ${selectedTarget} at URL: ${baseUrl}`);
351350

352351
// Track successful embed action
353352
capture('embed_created', {
@@ -361,7 +360,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
361360
return;
362361
}
363362

364-
console.debug(`Opening ${selectedTarget} in new tab from ${baseUrl}`);
363+
console.debug(`[pad.ws] Opening ${selectedTarget} in new tab from ${baseUrl}`);
365364
window.open(baseUrl, '_blank');
366365

367366
// Track tab open action
@@ -388,7 +387,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
388387
} else if (selectedTarget === 'code') {
389388
const prefix = selectedCodeVariant === 'cursor' ? 'cursor' : 'vscode';
390389
magnetLink = `${prefix}://coder.coder-remote/open?owner=${owner}&workspace=${workspace}&url=${url}&token=&openRecent=true&agent=${agent}`;
391-
console.debug(`Opening ${selectedCodeVariant} desktop app with magnet link: ${magnetLink}`);
390+
console.debug(`[pad.ws] Opening ${selectedCodeVariant} desktop app with magnet link: ${magnetLink}`);
392391
window.open(magnetLink, '_blank');
393392

394393
// Track desktop app open action
@@ -415,15 +414,6 @@ const ActionButton: React.FC<ActionButtonProps> = ({
415414

416415

417416
const handleTabClick = (tabType: string, value: string) => {
418-
// Track tab selection change
419-
capture('tab_selection_changed', {
420-
tabType,
421-
newValue: value,
422-
previousTarget: selectedTarget,
423-
previousCodeVariant: selectedTarget === 'code' ? selectedCodeVariant : null,
424-
previousAction: selectedAction
425-
});
426-
427417
if (tabType === 'target') {
428418
setSelectedTarget(value as TargetType);
429419
} else if (tabType === 'editor') {
@@ -440,7 +430,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
440430
setShowOptions(newShowOptions);
441431

442432
// Track settings toggle event
443-
capture('settings_toggled', {
433+
capture('custom_button_edit_settings', {
444434
target: selectedTarget,
445435
action: selectedAction,
446436
codeVariant: selectedTarget === 'code' ? selectedCodeVariant : null,

0 commit comments

Comments
 (0)