Skip to content

Commit 9c5d6bb

Browse files
authored
feat(code): public web interstitial for canvas share links (#66153)
1 parent 2bfd357 commit 9c5d6bb

6 files changed

Lines changed: 88 additions & 0 deletions

File tree

frontend/src/scenes/appScenes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const appScenes: Record<Scene | string, () => any> = {
130130
[Scene.Transformations]: () => import('./data-pipelines/TransformationsScene'),
131131
[Scene.EventFiltering]: () => import('./data-pipelines/event-filtering/EventFilterScene'),
132132
[Scene.Unsubscribe]: () => import('./Unsubscribe/Unsubscribe'),
133+
[Scene.CodeCanvasLink]: () => import('./code-canvas/CodeCanvasLink'),
133134
[Scene.VercelConnect]: () => import('./authentication/vercel/VercelConnect'),
134135
[Scene.VercelLinkError]: () => import('./authentication/vercel/VercelLinkError'),
135136
[Scene.AgenticAccountMismatch]: () => import('./authentication/account/AgenticAccountMismatch'),
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useEffect } from 'react'
2+
3+
import { IconLaptop } from '@posthog/icons'
4+
5+
import { BridgePage } from 'lib/components/BridgePage/BridgePage'
6+
import { LemonButton } from 'lib/lemon-ui/LemonButton'
7+
import { SceneExport } from 'scenes/sceneTypes'
8+
9+
export interface CodeCanvasLinkProps {
10+
channelId: string
11+
dashboardId: string
12+
}
13+
14+
export const scene: SceneExport<CodeCanvasLinkProps> = {
15+
component: CodeCanvasLink,
16+
paramsToProps: ({ params: { channelId, dashboardId } }) => ({
17+
channelId: channelId ?? '',
18+
dashboardId: dashboardId ?? '',
19+
}),
20+
}
21+
22+
// The desktop app registers a different custom scheme per build: production
23+
// installs use `posthog-code://`, local dev builds use `posthog-code-dev://`.
24+
// A dev frontend (./bin/start) is exactly when you're testing against a dev
25+
// desktop build, so target that scheme there. Build-time constant, so it never
26+
// flips after mount and double-fires.
27+
const DESKTOP_SCHEME = process.env.NODE_ENV === 'development' ? 'posthog-code-dev' : 'posthog-code'
28+
29+
function canvasDeepLink(channelId: string, dashboardId: string): string {
30+
return `${DESKTOP_SCHEME}://canvas/${encodeURIComponent(channelId)}/${encodeURIComponent(dashboardId)}`
31+
}
32+
33+
/**
34+
* Public, unauthenticated bridge for desktop-app "canvas" share links
35+
* (`/code/canvas/<channelId>/<dashboardId>`). On mount it deep-links into the desktop
36+
* app via the `posthog-code(-dev)://` custom scheme; for visitors without the app it
37+
* shows an explanation, a manual "open" button (in case the browser blocks the
38+
* auto-redirect), and a download link. The canvas itself only exists in the desktop
39+
* app, so nothing is rendered here beyond this interstitial.
40+
*/
41+
export function CodeCanvasLink({ channelId, dashboardId }: CodeCanvasLinkProps): JSX.Element {
42+
// Null when a param is missing (a partial URL or params not yet resolved) —
43+
// firing with an empty id would send a malformed `<scheme>://canvas//`.
44+
const deepLink = channelId && dashboardId ? canvasDeepLink(channelId, dashboardId) : null
45+
46+
useEffect(() => {
47+
if (deepLink) {
48+
window.location.href = deepLink
49+
}
50+
}, [deepLink])
51+
52+
return (
53+
<BridgePage view="code-canvas-link">
54+
<div className="flex flex-col items-center gap-4 text-center max-w-lg mx-auto">
55+
<IconLaptop className="text-5xl shrink-0" />
56+
<h2 className="text-xl font-semibold m-0">Opening in PostHog Code…</h2>
57+
<p className="text-muted mb-0">
58+
Canvases live in the PostHog Code desktop app. If it's installed, it should open automatically. If
59+
it didn't, use the button below — or download the app.
60+
</p>
61+
<div className="flex flex-col items-center gap-2">
62+
{deepLink && (
63+
<LemonButton
64+
type="primary"
65+
onClick={() => {
66+
window.location.href = deepLink
67+
}}
68+
>
69+
Open in PostHog Code
70+
</LemonButton>
71+
)}
72+
<LemonButton type="secondary" to="https://posthog.com/code" targetBlank>
73+
Download PostHog Code
74+
</LemonButton>
75+
</div>
76+
</div>
77+
</BridgePage>
78+
)
79+
}
80+
81+
export default CodeCanvasLink

frontend/src/scenes/sceneTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export enum Scene {
168168
Transformations = 'Transformations',
169169
EventFiltering = 'EventFiltering',
170170
Unsubscribe = 'Unsubscribe',
171+
CodeCanvasLink = 'CodeCanvasLink',
171172
UserInterview = 'UserInterview',
172173
UserInterviewResponse = 'UserInterviewResponse',
173174
UserInterviews = 'UserInterviews',

frontend/src/scenes/scenes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ export const sceneConfigurations: Record<Scene | string, SceneConfig> = {
581581
iconType: 'data_pipeline',
582582
},
583583
[Scene.Unsubscribe]: { allowUnauthenticated: true, layout: 'app-raw' },
584+
[Scene.CodeCanvasLink]: { allowUnauthenticated: true, layout: 'app-raw' },
584585
[Scene.VerifyEmail]: { allowUnauthenticated: true, layout: 'plain' },
585586
[Scene.WebAnalyticsPageReports]: {
586587
projectBased: true,
@@ -905,6 +906,7 @@ export const routes: Record<string, [Scene | string, string]> = {
905906
[urls.vercelLinkError()]: [Scene.VercelLinkError, 'vercelLinkError'],
906907
[urls.agenticAccountMismatch()]: [Scene.AgenticAccountMismatch, 'agenticAccountMismatch'],
907908
[urls.unsubscribe()]: [Scene.Unsubscribe, 'unsubscribe'],
909+
[urls.codeCanvasLink(':channelId', ':dashboardId')]: [Scene.CodeCanvasLink, 'codeCanvasLink'],
908910
[urls.integrationsRedirect(':kind')]: [Scene.IntegrationsRedirect, 'integrationsRedirect'],
909911
[urls.integration(':slug')]: [Scene.IntegrationsLanding, 'integrationsLanding'],
910912
[urls.stripeConfirmInstall()]: [Scene.StripeConfirmInstall, 'stripeConfirmInstall'],

frontend/src/scenes/urls.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export const urls = {
231231
queryPerformance: (): string => '/instance/query_performance',
232232
materializedColumns: (): string => '/data-management/materialized-columns',
233233
unsubscribe: (): string => '/unsubscribe',
234+
codeCanvasLink: (channelId: string, dashboardId: string): string => `/code/canvas/${channelId}/${dashboardId}`,
234235
integration: (slug: string): string => `/integrations/${slug}`,
235236
integrationsRedirect: (kind: string): string => `/integrations/${kind}/callback`,
236237
stripeConfirmInstall: (): string => '/integrations/stripe/confirm-install',

posthog/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@ def delete_events(request):
548548
"organization/confirm-creation",
549549
"login",
550550
"unsubscribe",
551+
# Public bridge for desktop-app canvas share links — deep-links into PostHog Code.
552+
r"code/canvas/[^/]+/[^/]+",
551553
"verify_email",
552554
r"agentic/account-mismatch",
553555
# OAuth redirect target when logging the local frontend into a remote cloud region;

0 commit comments

Comments
 (0)