From 6812f39f425023b07df19bb59919546b360dfc51 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 1 Aug 2025 18:40:32 -0400 Subject: [PATCH] feat: implement TriggerTokenProvider for access token management - Added a new TriggerTokenProvider component to manage access tokens for onboarding processes. - Introduced healAndSetAccessToken function to create and set public access tokens via cookies. - Updated layout and onboarding components to utilize the new provider for improved token handling. - Enhanced error handling and loading states for better user experience during token retrieval. --- .../src/actions/trigger/heal-access-token.ts | 44 +++++++++++++ .../[orgId]/components/OnboardingTracker.tsx | 17 ++--- apps/app/src/app/(app)/[orgId]/layout.tsx | 44 +++++++------ apps/app/src/app/(app)/setup/go/[id]/page.tsx | 34 ++++++++-- .../src/components/trigger-token-provider.tsx | 65 +++++++++++++++++++ 5 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 apps/app/src/actions/trigger/heal-access-token.ts create mode 100644 apps/app/src/components/trigger-token-provider.tsx diff --git a/apps/app/src/actions/trigger/heal-access-token.ts b/apps/app/src/actions/trigger/heal-access-token.ts new file mode 100644 index 000000000..e552d4597 --- /dev/null +++ b/apps/app/src/actions/trigger/heal-access-token.ts @@ -0,0 +1,44 @@ +'use server'; + +import { auth } from '@trigger.dev/sdk/v3'; +import { cookies } from 'next/headers'; + +// Server action that can set cookies (called from client components or forms) +export async function healAndSetAccessToken(triggerJobId: string): Promise { + try { + const cookieStore = await cookies(); + + const token = await auth.createPublicToken({ + scopes: { + read: { + runs: [triggerJobId], + }, + }, + }); + + cookieStore.set('publicAccessToken', token); + + return token; + } catch (error) { + console.error('Failed to heal and set access token:', error); + return null; + } +} + +// Helper function for server components (doesn't set cookies) +export async function createAccessToken(triggerJobId: string): Promise { + try { + const token = await auth.createPublicToken({ + scopes: { + read: { + runs: [triggerJobId], + }, + }, + }); + + return token; + } catch (error) { + console.error('Failed to create access token:', error); + return null; + } +} diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 590596ab6..122310065 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -32,22 +32,17 @@ const getFriendlyStatusName = (status: string): string => { .replace(/\b\w/g, (char) => char.toUpperCase()); }; -export const OnboardingTracker = ({ - onboarding, - publicAccessToken, -}: { - onboarding: Onboarding; - publicAccessToken: string; -}) => { +export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => { const [currentMessageIndex, setCurrentMessageIndex] = useState(0); const triggerJobId = onboarding.triggerJobId; + // useRealtimeRun will automatically get the token from TriggerProvider context const { run, error } = useRealtimeRun(triggerJobId || '', { - accessToken: publicAccessToken, + enabled: !!triggerJobId, }); useEffect(() => { - if (!triggerJobId || !publicAccessToken) return; + if (!triggerJobId) return; let interval: NodeJS.Timeout; if (run && IN_PROGRESS_STATUSES.includes(run.status)) { @@ -58,9 +53,9 @@ export const OnboardingTracker = ({ setCurrentMessageIndex(0); // Reset when not in progress } return () => clearInterval(interval); - }, [run, triggerJobId, publicAccessToken]); + }, [run, triggerJobId]); - if (!triggerJobId || !publicAccessToken) { + if (!triggerJobId) { return
Unable to load onboarding tracker.
; } diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index d6ec8ddcf..8ba4f54a3 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -3,6 +3,7 @@ import { CheckoutCompleteDialog } from '@/components/dialogs/checkout-complete-d import { Header } from '@/components/header'; import { AssistantSheet } from '@/components/sheets/assistant-sheet'; import { Sidebar } from '@/components/sidebar'; +import { TriggerTokenProvider } from '@/components/trigger-token-provider'; import { SidebarProvider } from '@/context/sidebar-context'; import { auth } from '@/utils/auth'; import { db } from '@db'; @@ -27,7 +28,7 @@ export default async function Layout({ const cookieStore = await cookies(); const isCollapsed = cookieStore.get('sidebar-collapsed')?.value === 'true'; - const publicAccessToken = cookieStore.get('publicAccessToken')?.value; + let publicAccessToken = cookieStore.get('publicAccessToken')?.value || undefined; // Check if user has access to this organization const session = await auth.api.getSession({ @@ -75,24 +76,27 @@ export default async function Layout({ const pixelsOffset = isOnboardingRunning ? navbarHeight + onboardingHeight : navbarHeight; return ( - - } isCollapsed={isCollapsed}> - {onboarding?.triggerJobId && ( - - )} -
-
- {children} -
- - - - - - - + + + } isCollapsed={isCollapsed}> + {onboarding?.triggerJobId && } +
+
+ {children} +
+ + + + + + + + ); } diff --git a/apps/app/src/app/(app)/setup/go/[id]/page.tsx b/apps/app/src/app/(app)/setup/go/[id]/page.tsx index 2ca30a837..5e271eda8 100644 --- a/apps/app/src/app/(app)/setup/go/[id]/page.tsx +++ b/apps/app/src/app/(app)/setup/go/[id]/page.tsx @@ -1,5 +1,6 @@ import { LogoSpinner } from '@/components/logo-spinner'; -import { TriggerProvider } from '@/components/trigger-provider'; +import { TriggerTokenProvider } from '@/components/trigger-token-provider'; +import { db } from '@db'; import { cookies } from 'next/headers'; import { OnboardingStatus } from './components/onboarding-status'; @@ -10,10 +11,35 @@ interface PageProps { export default async function RunPage({ params }: PageProps) { const { id } = await params; const cookieStore = await cookies(); - const publicAccessToken = cookieStore.get('publicAccessToken'); + const publicAccessToken = cookieStore.get('publicAccessToken')?.value || undefined; + + const onboarding = await db.onboarding.findUnique({ + where: { + organizationId: id, + }, + }); + + const triggerJobId = onboarding?.triggerJobId; + + if (!triggerJobId) { + return ( +
+
+
+
+

Onboarding Not Found

+

+ No onboarding process found for this organization. +

+
+
+
+
+ ); + } return ( - +
@@ -32,6 +58,6 @@ export default async function RunPage({ params }: PageProps) {
-
+ ); } diff --git a/apps/app/src/components/trigger-token-provider.tsx b/apps/app/src/components/trigger-token-provider.tsx new file mode 100644 index 000000000..7b23e42cb --- /dev/null +++ b/apps/app/src/components/trigger-token-provider.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { healAndSetAccessToken } from '@/actions/trigger/heal-access-token'; +import { TriggerProvider } from '@/components/trigger-provider'; +import { useEffect, useState } from 'react'; + +interface TriggerTokenProviderProps { + triggerJobId?: string; + initialToken?: string; + children: React.ReactNode; +} + +export function TriggerTokenProvider({ + triggerJobId, + initialToken, + children, +}: TriggerTokenProviderProps) { + const [token, setToken] = useState(initialToken || null); + const [isLoading, setIsLoading] = useState(!initialToken && !!triggerJobId); + + useEffect(() => { + async function ensureToken() { + if (triggerJobId && !initialToken) { + try { + // This runs as a proper server action and can set cookies + const healedToken = await healAndSetAccessToken(triggerJobId); + setToken(healedToken); + } catch (error) { + console.error('Failed to heal token:', error); + } finally { + setIsLoading(false); + } + } + } + + ensureToken(); + }, [triggerJobId, initialToken]); + + // If we need a token but don't have one yet, show loading + if (triggerJobId && isLoading) { + return ( +
+
+ Connecting... +
+ ); + } + + // If we need a token but failed to get one, show error + if (triggerJobId && !token && !isLoading) { + return ( +
+ Failed to establish connection +
+ ); + } + + // If no trigger job needed, just render children + if (!triggerJobId) { + return <>{children}; + } + + // Wrap everything in TriggerProvider with the token + return {children}; +}