Skip to content

Commit d0b34d9

Browse files
committed
Merge project enforcement parent with acquisition attribution
2 parents 1f47329 + d4635ed commit d0b34d9

32 files changed

Lines changed: 1023 additions & 172 deletions

File tree

‎apps/sim/app/(auth)/components/social-login-buttons.tsx‎

Lines changed: 33 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Chip, cn } from '@sim/emcn'
55
import { createLogger } from '@sim/logger'
66
import { getErrorMessage } from '@sim/utils/errors'
77
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
8-
import { client } from '@/lib/auth/auth-client'
8+
import { type SocialSignInProvider, startSocialSignIn } from '@/lib/auth/social-sign-in'
99
import { DEFAULT_POST_AUTH_ROUTE } from '@/app/(auth)/auth-redirect'
1010
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
1111

@@ -15,107 +15,63 @@ interface SocialLoginButtonsProps {
1515
githubAvailable: boolean
1616
googleAvailable: boolean
1717
microsoftAvailable: boolean
18+
view: 'login' | 'signup'
1819
callbackURL?: string
1920
children?: ReactNode
2021
}
2122

23+
/** Display order of the provider buttons. */
24+
const PROVIDERS = [
25+
{ provider: 'google', label: 'Google', icon: GoogleIcon },
26+
{ provider: 'microsoft', label: 'Microsoft', icon: MicrosoftIcon },
27+
{ provider: 'github', label: 'GitHub', icon: GithubIcon },
28+
] as const
29+
2230
export function SocialLoginButtons({
2331
githubAvailable,
2432
googleAvailable,
2533
microsoftAvailable,
34+
view,
2635
callbackURL = DEFAULT_POST_AUTH_ROUTE,
2736
children,
2837
}: SocialLoginButtonsProps) {
29-
const [isGithubLoading, setIsGithubLoading] = useState(false)
30-
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
31-
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false)
32-
33-
async function signInWithGithub() {
34-
if (!githubAvailable) return
38+
const [loadingProvider, setLoadingProvider] = useState<SocialSignInProvider | null>(null)
3539

36-
setIsGithubLoading(true)
37-
try {
38-
await client.signIn.social({ provider: 'github', callbackURL })
39-
} catch (err) {
40-
logger.error('GitHub sign-in failed', { error: getErrorMessage(err) })
41-
} finally {
42-
setIsGithubLoading(false)
43-
}
40+
const available: Record<SocialSignInProvider, boolean> = {
41+
github: githubAvailable,
42+
google: googleAvailable,
43+
microsoft: microsoftAvailable,
4444
}
4545

46-
async function signInWithGoogle() {
47-
if (!googleAvailable) return
48-
49-
setIsGoogleLoading(true)
46+
async function signIn(provider: SocialSignInProvider, label: string) {
47+
setLoadingProvider(provider)
5048
try {
51-
await client.signIn.social({ provider: 'google', callbackURL })
49+
await startSocialSignIn({ provider, view, surface: 'auth_page', callbackURL })
5250
} catch (err) {
53-
logger.error('Google sign-in failed', { error: getErrorMessage(err) })
51+
logger.error(`${label} sign-in failed`, { error: getErrorMessage(err) })
5452
} finally {
55-
setIsGoogleLoading(false)
53+
setLoadingProvider(null)
5654
}
5755
}
5856

59-
async function signInWithMicrosoft() {
60-
if (!microsoftAvailable) return
61-
62-
setIsMicrosoftLoading(true)
63-
try {
64-
await client.signIn.social({ provider: 'microsoft', callbackURL })
65-
} catch (err) {
66-
logger.error('Microsoft sign-in failed', { error: getErrorMessage(err) })
67-
} finally {
68-
setIsMicrosoftLoading(false)
69-
}
70-
}
71-
72-
const githubButton = (
73-
<Chip
74-
fullWidth
75-
leftIcon={GithubIcon}
76-
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
77-
disabled={!githubAvailable || isGithubLoading}
78-
onClick={signInWithGithub}
79-
>
80-
{isGithubLoading ? 'Connecting…' : 'GitHub'}
81-
</Chip>
82-
)
83-
84-
const googleButton = (
85-
<Chip
86-
fullWidth
87-
leftIcon={GoogleIcon}
88-
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
89-
disabled={!googleAvailable || isGoogleLoading}
90-
onClick={signInWithGoogle}
91-
>
92-
{isGoogleLoading ? 'Connecting…' : 'Google'}
93-
</Chip>
94-
)
95-
96-
const microsoftButton = (
97-
<Chip
98-
fullWidth
99-
leftIcon={MicrosoftIcon}
100-
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
101-
disabled={!microsoftAvailable || isMicrosoftLoading}
102-
onClick={signInWithMicrosoft}
103-
>
104-
{isMicrosoftLoading ? 'Connecting…' : 'Microsoft'}
105-
</Chip>
106-
)
107-
108-
const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable
109-
110-
if (!hasAnyOAuthProvider && !children) {
57+
if (!githubAvailable && !googleAvailable && !microsoftAvailable && !children) {
11158
return null
11259
}
11360

11461
return (
11562
<div className='grid gap-3'>
116-
{googleAvailable && googleButton}
117-
{microsoftAvailable && microsoftButton}
118-
{githubAvailable && githubButton}
63+
{PROVIDERS.filter(({ provider }) => available[provider]).map(({ provider, label, icon }) => (
64+
<Chip
65+
key={provider}
66+
fullWidth
67+
leftIcon={icon}
68+
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border)]')}
69+
disabled={loadingProvider !== null}
70+
onClick={() => signIn(provider, label)}
71+
>
72+
{loadingProvider === provider ? 'Connecting…' : label}
73+
</Chip>
74+
))}
11975
{children}
12076
</div>
12177
)

‎apps/sim/app/(auth)/layout.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import type { Metadata } from 'next'
2+
import { AttributionCapture } from '@/app/_shell/consent/attribution-capture'
23
import { AuthShell } from '@/app/(auth)/components'
34

45
export const metadata: Metadata = {
56
robots: { index: false, follow: false },
67
}
78

89
export default function AuthLayout({ children }: { children: React.ReactNode }) {
9-
return <AuthShell>{children}</AuthShell>
10+
return (
11+
<AuthShell>
12+
{children}
13+
<AttributionCapture />
14+
</AuthShell>
15+
)
1016
}

‎apps/sim/app/(auth)/login/login-form.tsx‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useRef, useState } from 'react'
3+
import { useRef, useState } from 'react'
44
import {
55
ChipModal,
66
ChipModalBody,
@@ -21,7 +21,6 @@ import { isSsoEnabled } from '@/lib/core/config/env-flags'
2121
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
2222
import { getBaseUrl } from '@/lib/core/utils/urls'
2323
import { quickValidateEmail } from '@/lib/messaging/email/validation'
24-
import { captureClientEvent } from '@/lib/posthog/client'
2524
import { buildAuthCrossLink, DEFAULT_POST_AUTH_ROUTE } from '@/app/(auth)/auth-redirect'
2625
import {
2726
AuthDivider,
@@ -37,6 +36,7 @@ import {
3736
SocialLoginButtons,
3837
SSOLoginButton,
3938
} from '@/app/(auth)/components'
39+
import { useCaptureWhenReady } from '@/hooks/use-capture-when-ready'
4040

4141
const logger = createLogger('LoginForm')
4242

@@ -97,6 +97,7 @@ export default function LoginPage({
9797
}) {
9898
const router = useRouter()
9999
const searchParams = useSearchParams()
100+
useCaptureWhenReady('login_page_viewed', {})
100101
const [isLoading, setIsLoading] = useState(false)
101102
const [password, setPassword] = useState('')
102103
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
@@ -134,10 +135,6 @@ export default function LoginPage({
134135
: null
135136
)
136137

137-
useEffect(() => {
138-
captureClientEvent('login_page_viewed', {})
139-
}, [])
140-
141138
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
142139
const newEmail = e.target.value
143140
setEmail(newEmail)
@@ -448,6 +445,7 @@ export default function LoginPage({
448445

449446
{showBottomSection && (
450447
<SocialLoginButtons
448+
view='login'
451449
googleAvailable={googleAvailable}
452450
githubAvailable={githubAvailable}
453451
microsoftAvailable={microsoftAvailable}

‎apps/sim/app/(auth)/signup/signup-form.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { Suspense, useEffect, useRef, useState } from 'react'
3+
import { Suspense, useRef, useState } from 'react'
44
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
55
import { createLogger } from '@sim/logger'
66
import { useRouter, useSearchParams } from 'next/navigation'
@@ -13,7 +13,7 @@ import { getEnv, isFalsy } from '@/lib/core/config/env'
1313
import { isSsoEnabled } from '@/lib/core/config/env-flags'
1414
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
1515
import { quickValidateEmail } from '@/lib/messaging/email/validation'
16-
import { captureClientEvent, captureEvent } from '@/lib/posthog/client'
16+
import { captureEvent } from '@/lib/posthog/client'
1717
import {
1818
buildAuthCrossLink,
1919
DEFAULT_POST_AUTH_ROUTE,
@@ -35,6 +35,7 @@ import {
3535
SocialLoginButtons,
3636
SSOLoginButton,
3737
} from '@/app/(auth)/components'
38+
import { useCaptureWhenReady } from '@/hooks/use-capture-when-ready'
3839

3940
const logger = createLogger('SignupForm')
4041

@@ -111,11 +112,9 @@ function SignupFormContent({
111112
const { refetch: refetchSession } = useSession()
112113
const posthog = usePostHog()
113114
const { measurement, marketing } = useTrackingConsent()
115+
useCaptureWhenReady('signup_page_viewed', {})
114116
const [isLoading, setIsLoading] = useState(false)
115117

116-
useEffect(() => {
117-
captureClientEvent('signup_page_viewed', {})
118-
}, [])
119118
const [password, setPassword] = useState('')
120119
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
121120
const [showValidationError, setShowValidationError] = useState(false)
@@ -485,6 +484,7 @@ function SignupFormContent({
485484

486485
{showBottomSection && (
487486
<SocialLoginButtons
487+
view='signup'
488488
githubAvailable={githubAvailable}
489489
googleAvailable={googleAvailable}
490490
microsoftAvailable={microsoftAvailable}

‎apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { useRouter } from 'next/navigation'
1818
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
1919
import { requestJson } from '@/lib/api/client/request'
2020
import { type AuthProviderStatusResponse, getAuthProvidersContract } from '@/lib/api/contracts/auth'
21-
import { client } from '@/lib/auth/auth-client'
21+
import { type SocialSignInProvider, startSocialSignIn } from '@/lib/auth/social-sign-in'
2222
import { getEnv, isFalsy } from '@/lib/core/config/env'
2323
import { isSsoEnabled } from '@/lib/core/config/env-flags'
2424
import { APP_ENTRY_PATH } from '@/lib/navigation/paths'
@@ -79,7 +79,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
7979
const [open, setOpen] = useState(false)
8080
const [view, setView] = useState<AuthView>(defaultView)
8181
const [providerStatus, setProviderStatus] = useState<ProviderStatus | null>(null)
82-
const [socialLoading, setSocialLoading] = useState<'github' | 'google' | 'microsoft' | null>(null)
82+
const [socialLoading, setSocialLoading] = useState<SocialSignInProvider | null>(null)
8383
const brand = getBrandConfig()
8484

8585
useEffect(() => {
@@ -141,10 +141,15 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
141141
})
142142
}
143143

144-
async function handleSocialLogin(provider: 'github' | 'google' | 'microsoft') {
144+
async function handleSocialLogin(provider: SocialSignInProvider) {
145145
setSocialLoading(provider)
146146
try {
147-
await client.signIn.social({ provider, callbackURL: APP_ENTRY_PATH })
147+
await startSocialSignIn({
148+
provider,
149+
view: effectiveView,
150+
surface: 'auth_modal',
151+
callbackURL: APP_ENTRY_PATH,
152+
})
148153
} catch (error) {
149154
logger.warn('Social sign-in did not complete', { provider, error })
150155
} finally {

‎apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
4444
title: 'Cookie Policy',
4545
description:
4646
'What cookies Sim sets, why, how long they last, and how to change your choice at any time.',
47-
lastUpdated: 'September 17, 2026',
47+
lastUpdated: 'October 8, 2026',
4848
intro: [
4949
{
5050
kind: 'paragraph',
@@ -195,6 +195,12 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
195195
'Coordinates analytics state for the current browser tab.',
196196
'Session',
197197
],
198+
[
199+
'sim_attribution_first / sim_attribution_last',
200+
'Sim',
201+
'Records the campaign or website that first and most recently brought you to sim.ai, so a sign-up or demo request can be credited to it. Holds campaign parameters, which ad network a click came from (never the click ID), the referring site’s domain, the landing page path, and when the visit happened. Expires with your analytics consent if that ends sooner.',
202+
'90 days',
203+
],
198204
]),
199205
cookieTable('Marketing', [
200206
[

‎apps/sim/app/(landing)/demo/components/demo-scheduler/demo-scheduler.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useTheme } from 'next-themes'
66
import { trackGoogleAdsConversion, trackGoogleEvent } from '@/lib/analytics/google'
77
import { X_DEMO_BOOKED_EVENT_ID } from '@/lib/consent/scripts'
88
import { useTrackingConsent } from '@/lib/consent/tracking-consent'
9+
import { captureClientEvent } from '@/lib/posthog/client'
910
import type { DemoLead } from '@/app/(landing)/demo/components/demo-form'
1011

1112
const CAL_NAMESPACE = 'demo'
@@ -100,6 +101,7 @@ export function DemoScheduler({ lead }: DemoSchedulerProps) {
100101
const trackDemoBooked = () => {
101102
if (cancelled) return
102103
if (measurement) {
104+
captureClientEvent('landing_demo_booked', {})
103105
trackGoogleEvent('get_a_demo', {
104106
page_path: '/demo',
105107
form_name: 'sim_demo',
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
'use client'
22

3-
import { useEffect } from 'react'
4-
import { captureClientEvent } from '@/lib/posthog/client'
3+
import { useCaptureWhenReady } from '@/hooks/use-capture-when-ready'
54

65
export function LandingAnalytics() {
7-
useEffect(() => {
8-
captureClientEvent('landing_page_viewed', {})
9-
}, [])
10-
6+
useCaptureWhenReady('landing_page_viewed', {})
117
return null
128
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use client'
2+
3+
import { usePathname } from 'next/navigation'
4+
import { useCaptureWhenReady } from '@/hooks/use-capture-when-ready'
5+
6+
/**
7+
* Sends PostHog's `$pageview` for every marketing route, including client
8+
* navigations between them, so pricing, demo, and enterprise visits feed
9+
* PostHog funnels and web analytics. `capture_pageview` stays off app-wide;
10+
* this layout is the only place page views are wanted.
11+
*/
12+
export function LandingPageViewTracker() {
13+
useCaptureWhenReady('$pageview', {}, usePathname())
14+
return null
15+
}

‎apps/sim/app/(landing)/layout.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type { ReactNode } from 'react'
22
import type { Metadata } from 'next'
33
import { isHosted } from '@/lib/core/config/env-flags'
44
import { SITE_URL } from '@/lib/core/utils/urls'
5+
import { AttributionCapture } from '@/app/_shell/consent/attribution-capture'
56
import { LandingShell } from '@/app/(landing)/components'
67
import { LandingConsentTracking } from '@/app/(landing)/landing-consent-tracking'
8+
import { LandingPageViewTracker } from '@/app/(landing)/landing-page-view-tracker'
79

810
/**
911
* Shared layout for all public marketing routes, including platform, solutions,
@@ -19,6 +21,8 @@ export default function LandingLayout({ children }: { children: ReactNode }) {
1921
return (
2022
<LandingShell>
2123
{children}
24+
<LandingPageViewTracker />
25+
<AttributionCapture />
2226
{isHosted && <LandingConsentTracking />}
2327
</LandingShell>
2428
)

0 commit comments

Comments
 (0)