diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx
index 631d19e..c3fe5ac 100644
--- a/app/auth/callback/page.tsx
+++ b/app/auth/callback/page.tsx
@@ -3,7 +3,7 @@
import { useEffect, useState, Suspense, useRef, useCallback } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useAuth } from '@/lib/auth/context'
-import { AUTH_URL } from '@/lib/api/client'
+import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth'
import { authMessageFromErrorType, type AuthErrorType } from '@/lib/utils/authErrors'
import { LoadingOverlay } from '@ciphera-net/ui'
@@ -23,13 +23,22 @@ function AuthCallbackContent() {
if (!code || !codeVerifier) return
const result = await exchangeAuthCode(code, codeVerifier, redirectUri)
if (result.success && result.user) {
- login(result.user)
+ // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
+ try {
+ const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
+ const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
+ login(merged)
+ } catch {
+ login(result.user)
+ }
localStorage.removeItem('oauth_state')
localStorage.removeItem('oauth_code_verifier')
if (localStorage.getItem('pulse_pending_checkout')) {
router.push('/welcome')
} else {
- router.push('/')
+ const raw = searchParams.get('returnTo') || '/'
+ const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
+ router.push(safe)
}
} else {
setError(authMessageFromErrorType(result.error as AuthErrorType))
@@ -51,12 +60,20 @@ function AuthCallbackContent() {
const handleDirectTokens = async () => {
const result = await setSessionAction(token, refreshToken)
if (result.success && result.user) {
- login(result.user)
+ // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
+ try {
+ const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
+ const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
+ login(merged)
+ } catch {
+ login(result.user)
+ }
if (typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')) {
router.push('/welcome')
} else {
- const returnTo = searchParams.get('returnTo') || '/'
- router.push(returnTo)
+ const raw = searchParams.get('returnTo') || '/'
+ const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
+ router.push(safe)
}
} else {
setError(authMessageFromErrorType('invalid'))
@@ -91,6 +108,7 @@ function AuthCallbackContent() {
const handleRetry = () => {
setError(null)
+ processedRef.current = false
setIsRetrying(true)
}
diff --git a/app/integrations/nextjs/page.tsx b/app/integrations/nextjs/page.tsx
new file mode 100644
index 0000000..12ec2c4
--- /dev/null
+++ b/app/integrations/nextjs/page.tsx
@@ -0,0 +1,129 @@
+'use client'
+
+import Link from 'next/link'
+import { ArrowLeftIcon } from '@ciphera-net/ui'
+
+export default function NextJsIntegrationPage() {
+ return (
+
+ {/* * --- ATMOSPHERE (Background) --- */}
+
+
+
+
+
+ Back to Integrations
+
+
+
+
+
+ Next.js Integration
+
+
+
+
+
+ The best way to add Pulse to your Next.js application is using the built-in next/script component.
+
+
+
+
+
Using App Router (Recommended)
+
+ Add the script to your root layout file (usually app/layout.tsx or app/layout.js).
+
+
+
+
+ app/layout.tsx
+
+
+
+{`import Script from 'next/script'
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+
+ {children}
+
+ )
+}`}
+
+
+
+
+
Using Pages Router
+
+ If you are using the older Pages Router, add the script to your custom _app.tsx or _document.tsx.
+
+
+
+
+ pages/_app.tsx
+
+
+
+{`import Script from 'next/script'
+import type { AppProps } from 'next/app'
+
+export default function App({ Component, pageProps }: AppProps) {
+ return (
+ <>
+
+
+ >
+ )
+}`}
+
+
+
+
+
Configuration Options
+
+ -
+ data-domain: The domain name you added to your Pulse dashboard (e.g.,
example.com).
+
+ -
+ src: The URL of our tracking script:
https://pulse.ciphera.net/script.js
+
+ -
+ strategy: We recommend
afterInteractive to ensure it loads quickly without blocking hydration.
+
+
+
+
+
+ )
+}
diff --git a/app/integrations/react/page.tsx b/app/integrations/react/page.tsx
new file mode 100644
index 0000000..18a848a
--- /dev/null
+++ b/app/integrations/react/page.tsx
@@ -0,0 +1,119 @@
+'use client'
+
+import Link from 'next/link'
+import { ArrowLeftIcon } from '@ciphera-net/ui'
+
+export default function ReactIntegrationPage() {
+ return (
+
+ {/* * --- ATMOSPHERE (Background) --- */}
+
+
+
+
+
+ Back to Integrations
+
+
+
+
+
+ React Integration
+
+
+
+
+
+ For standard React SPAs (Create React App, Vite, etc.), you can simply add the script tag to your index.html.
+
+
+
+
+
Method 1: index.html (Recommended)
+
+ The simplest way is to add the script tag directly to the <head> of your index.html file.
+
+
+
+
+ public/index.html
+
+
+
+{`
+
+
+
+
+
+
+
+
+ My React App
+
+
+
+
+`}
+
+
+
+
+
Method 2: Programmatic Injection
+
+ If you need to load the script dynamically (e.g., only in production), you can use a useEffect hook in your main App component.
+
+
+
+
+ src/App.tsx
+
+
+
+{`import { useEffect } from 'react'
+
+function App() {
+ useEffect(() => {
+ // Only load in production
+ if (process.env.NODE_ENV === 'production') {
+ const script = document.createElement('script')
+ script.defer = true
+ script.setAttribute('data-domain', 'your-site.com')
+ script.src = 'https://pulse.ciphera.net/script.js'
+ document.head.appendChild(script)
+ }
+ }, [])
+
+ return (
+
+
Hello World
+
+ )
+}`}
+
+
+
+
+
+
+ )
+}
diff --git a/app/integrations/vue/page.tsx b/app/integrations/vue/page.tsx
new file mode 100644
index 0000000..40adb04
--- /dev/null
+++ b/app/integrations/vue/page.tsx
@@ -0,0 +1,113 @@
+'use client'
+
+import Link from 'next/link'
+import { ArrowLeftIcon } from '@ciphera-net/ui'
+
+export default function VueIntegrationPage() {
+ return (
+
+ {/* * --- ATMOSPHERE (Background) --- */}
+
+
+
+
+
+ Back to Integrations
+
+
+
+
+
+ Vue.js Integration
+
+
+
+
+
+ Integrating Pulse with Vue.js is straightforward. You can add the script to your index.html file.
+
+
+
+
+
Method 1: index.html (Recommended)
+
+ Add the script tag to the <head> section of your index.html file. This works for both Vue 2 and Vue 3 projects created with Vue CLI or Vite.
+
+
+
+
+ index.html
+
+
+
+{`
+
+
+
+
+
+
+
+
+ My Vue App
+
+
+
+
+
+`}
+
+
+
+
+
Method 2: Nuxt.js
+
+ For Nuxt.js applications, you should add the script to your nuxt.config.js or nuxt.config.ts file.
+
+
+
+
+ nuxt.config.ts
+
+
+
+{`export default defineNuxtConfig({
+ app: {
+ head: {
+ script: [
+ {
+ src: 'https://pulse.ciphera.net/script.js',
+ defer: true,
+ 'data-domain': 'your-site.com'
+ }
+ ]
+ }
+ }
+})`}
+
+
+
+
+
+
+ )
+}
diff --git a/app/integrations/wordpress/page.tsx b/app/integrations/wordpress/page.tsx
new file mode 100644
index 0000000..a1db8ef
--- /dev/null
+++ b/app/integrations/wordpress/page.tsx
@@ -0,0 +1,81 @@
+'use client'
+
+import Link from 'next/link'
+import { ArrowLeftIcon } from '@ciphera-net/ui'
+
+export default function WordPressIntegrationPage() {
+ return (
+
+ {/* * --- ATMOSPHERE (Background) --- */}
+
+
+
+
+
+ Back to Integrations
+
+
+
+
+
+ WordPress Integration
+
+
+
+
+
+ You can add Pulse to your WordPress site without installing any heavy plugins, or by using a simple code snippet plugin.
+
+
+
+
+
Method 1: Using a Plugin (Easiest)
+
+ - Install a plugin like "Insert Headers and Footers" (WPCode).
+ - Go to the plugin settings and find the "Scripts in Header" section.
+ - Paste the following code snippet:
+
+
+
+
+
Method 2: Edit Theme Files (Advanced)
+
+ If you are comfortable editing your theme files, you can add the script directly to your header.php file.
+
+
+ - Go to Appearance > Theme File Editor.
+ - Select
header.php from the right sidebar.
+ - Paste the script tag just before the closing
</head> tag.
+
+
+
+
+ )
+}
diff --git a/app/layout-content.tsx b/app/layout-content.tsx
index 7fabff9..ef1e128 100644
--- a/app/layout-content.tsx
+++ b/app/layout-content.tsx
@@ -73,14 +73,6 @@ export default function LayoutContent({ children }: { children: React.ReactNode
Features
)}
- {auth.user && (
-
- Tools
-
- )}
>
}
/>
diff --git a/app/tools/page.tsx b/app/tools/page.tsx
deleted file mode 100644
index e33bf6d..0000000
--- a/app/tools/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client'
-
-import UtmBuilder from '@/components/tools/UtmBuilder'
-
-export default function ToolsPage() {
- return (
-
-
Tools
-
-
UTM Campaign Builder
-
-
-
- )
-}
diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx
index 9199c06..7193206 100644
--- a/app/welcome/page.tsx
+++ b/app/welcome/page.tsx
@@ -20,6 +20,7 @@ import { createCheckoutSession } from '@/lib/api/billing'
import { createSite, type Site } from '@/lib/api/sites'
import { setSessionAction } from '@/app/actions/auth'
import { useAuth } from '@/lib/auth/context'
+import apiRequest from '@/lib/api/client'
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import {
trackWelcomeStepView,
@@ -149,7 +150,13 @@ function WelcomeContent() {
const { access_token } = await switchContext(org.organization_id)
const result = await setSessionAction(access_token)
if (result.success && result.user) {
- login(result.user)
+ try {
+ const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
+ const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
+ login(merged)
+ } catch {
+ login(result.user)
+ }
router.refresh()
trackWelcomeWorkspaceSelected()
setStep(3)
@@ -180,7 +187,13 @@ function WelcomeContent() {
const { access_token } = await switchContext(org.id)
const result = await setSessionAction(access_token)
if (result.success && result.user) {
- login(result.user)
+ try {
+ const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
+ const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
+ login(merged)
+ } catch {
+ login(result.user)
+ }
router.refresh()
}
trackWelcomeWorkspaceCreated(!!(typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')))
@@ -302,11 +315,11 @@ function WelcomeContent() {
}, [step, siteName, siteDomain])
if (orgLoading && step === 2) {
- return
+ return
}
if (switchingOrgId) {
- return
+ return
}
if (redirectingCheckout || (planLoading && step === 3)) {
@@ -357,50 +370,73 @@ function WelcomeContent() {
className={cardClass}
>
{orgsLoading ? (
-
-
Loading your workspaces...
+
+
+
Loading your organizations...
) : organizations && organizations.length > 0 ? (
<>
-
-
-
+
+
+
-
+
Choose your organization
-
-
- Continue with an existing organization or create a new one.
+
+
+ Continue with an existing one or create a new organization.
-
- {organizations.map((org) => (
-
- ))}
+
+ {organizations.map((org, index) => {
+ const isCurrent = user?.org_id === org.organization_id
+ const initial = (org.organization_name || 'O').charAt(0).toUpperCase()
+ return (
+
handleSelectOrganization(org)}
+ disabled={!!switchingOrgId}
+ initial={{ opacity: 0, y: 8 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: index * 0.04, duration: 0.2 }}
+ className={`w-full flex items-center gap-3 rounded-xl border px-4 py-3.5 text-left transition-all duration-200 disabled:opacity-60 ${
+ isCurrent
+ ? 'border-brand-orange/60 bg-brand-orange/5 dark:bg-brand-orange/10 shadow-sm'
+ : 'border-neutral-200 dark:border-neutral-700 bg-neutral-50/80 dark:bg-neutral-800/50 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:border-neutral-300 dark:hover:border-neutral-600 hover:shadow-sm'
+ }`}
+ >
+
+ {initial}
+
+
+ {org.organization_name || 'Organization'}
+
+ {isCurrent && (
+ Current
+ )}
+
+
+ )
+ })}
+
+
+
-
>
) : (
@@ -484,7 +520,7 @@ function WelcomeContent() {
onChange={(e) => setOrgSlug(e.target.value)}
className="w-full"
/>
-
+
Used in your organization URL.
@@ -651,9 +687,15 @@ function WelcomeContent() {
variant="primary"
className="flex-1"
disabled={siteLoading || !siteName.trim() || !siteDomain.trim()}
- isLoading={siteLoading}
>
- Add site
+ {siteLoading ? (
+ <>
+
+ Adding...
+ >
+ ) : (
+ 'Add site'
+ )}