11'use client'
22
33import { useCallback , useEffect , useState } from 'react'
4- import { AlertCircle , CheckCircle2 , Loader2 , Shield } from 'lucide-react'
4+ import { AlertCircle , CheckCircle2 , Loader2 , Mail } from 'lucide-react'
55import { useParams , useRouter } from 'next/navigation'
66import { Button } from '@/components/emcn'
7- import { useSession } from '@/lib/auth/auth-client'
7+ import { GmailIcon , OutlookIcon } from '@/components/icons'
8+ import { client , useSession } from '@/lib/auth/auth-client'
89
910interface InvitationInfo {
1011 credentialSetName : string
1112 organizationName : string
13+ providerId : string | null
1214 email : string | null
1315}
1416
17+ type AcceptedState = 'connecting' | 'already-connected'
18+
19+ /**
20+ * Maps credential set provider IDs to OAuth provider IDs
21+ * The credential set stores 'gmail' but the OAuth provider is 'google-email'
22+ */
23+ function getOAuthProviderId ( credentialSetProviderId : string ) : string {
24+ if ( credentialSetProviderId === 'gmail' ) {
25+ return 'google-email'
26+ }
27+ // outlook is the same in both
28+ return credentialSetProviderId
29+ }
30+
1531export default function CredentialAccountInvitePage ( ) {
1632 const params = useParams ( )
1733 const router = useRouter ( )
@@ -23,7 +39,7 @@ export default function CredentialAccountInvitePage() {
2339 const [ loading , setLoading ] = useState ( true )
2440 const [ error , setError ] = useState < string | null > ( null )
2541 const [ accepting , setAccepting ] = useState ( false )
26- const [ accepted , setAccepted ] = useState ( false )
42+ const [ acceptedState , setAcceptedState ] = useState < AcceptedState | null > ( null )
2743
2844 useEffect ( ( ) => {
2945 async function fetchInvitation ( ) {
@@ -48,7 +64,9 @@ export default function CredentialAccountInvitePage() {
4864
4965 const handleAccept = useCallback ( async ( ) => {
5066 if ( ! session ?. user ?. id ) {
51- router . push ( `/login?callbackUrl=${ encodeURIComponent ( `/credential-account/${ token } ` ) } ` )
67+ // Include invite_flow=true so the login page preserves callbackUrl when linking to signup
68+ const callbackUrl = encodeURIComponent ( `/credential-account/${ token } ` )
69+ router . push ( `/login?invite_flow=true&callbackUrl=${ callbackUrl } ` )
5270 return
5371 }
5472
@@ -64,13 +82,63 @@ export default function CredentialAccountInvitePage() {
6482 return
6583 }
6684
67- setAccepted ( true )
85+ const data = await res . json ( )
86+ const credentialSetProviderId = data . providerId || invitation ?. providerId
87+
88+ // Check if user already has this provider connected
89+ let isAlreadyConnected = false
90+ if ( credentialSetProviderId ) {
91+ const oauthProviderId = getOAuthProviderId ( credentialSetProviderId )
92+ try {
93+ const connectionsRes = await fetch ( '/api/auth/oauth/connections' )
94+ if ( connectionsRes . ok ) {
95+ const connectionsData = await connectionsRes . json ( )
96+ const connections = connectionsData . connections || [ ]
97+ isAlreadyConnected = connections . some (
98+ ( conn : { provider : string ; accounts ?: { id : string } [ ] } ) =>
99+ conn . provider === oauthProviderId && conn . accounts && conn . accounts . length > 0
100+ )
101+ }
102+ } catch {
103+ // If we can't check connections, proceed with OAuth flow
104+ }
105+ }
106+
107+ if ( isAlreadyConnected ) {
108+ // Already connected - redirect to workspace
109+ setAcceptedState ( 'already-connected' )
110+ setTimeout ( ( ) => {
111+ router . push ( '/workspace' )
112+ } , 2000 )
113+ } else if ( credentialSetProviderId === 'gmail' || credentialSetProviderId === 'outlook' ) {
114+ // Not connected - start OAuth flow
115+ setAcceptedState ( 'connecting' )
116+
117+ // Small delay to show success message before redirect
118+ setTimeout ( async ( ) => {
119+ try {
120+ const oauthProviderId = getOAuthProviderId ( credentialSetProviderId )
121+ await client . oauth2 . link ( {
122+ providerId : oauthProviderId ,
123+ callbackURL : `${ window . location . origin } /workspace` ,
124+ } )
125+ } catch ( oauthError ) {
126+ // OAuth redirect will happen, this catch is for any pre-redirect errors
127+ console . error ( 'OAuth initiation error:' , oauthError )
128+ // If OAuth fails, redirect to workspace where they can connect manually
129+ router . push ( '/workspace' )
130+ }
131+ } , 1500 )
132+ } else {
133+ // No provider specified - just redirect to workspace
134+ router . push ( '/workspace' )
135+ }
68136 } catch {
69137 setError ( 'Failed to accept invitation' )
70138 } finally {
71139 setAccepting ( false )
72140 }
73- } , [ session ?. user ?. id , token , router ] )
141+ } , [ session ?. user ?. id , token , router , invitation ?. providerId ] )
74142
75143 if ( loading || sessionLoading ) {
76144 return (
@@ -94,19 +162,49 @@ export default function CredentialAccountInvitePage() {
94162 )
95163 }
96164
97- if ( accepted ) {
165+ const ProviderIcon =
166+ invitation ?. providerId === 'outlook'
167+ ? OutlookIcon
168+ : invitation ?. providerId === 'gmail'
169+ ? GmailIcon
170+ : Mail
171+ const providerName =
172+ invitation ?. providerId === 'outlook'
173+ ? 'Outlook'
174+ : invitation ?. providerId === 'gmail'
175+ ? 'Gmail'
176+ : 'email'
177+
178+ if ( acceptedState === 'already-connected' ) {
98179 return (
99180 < div className = 'flex min-h-screen items-center justify-center bg-[var(--bg)]' >
100181 < div className = 'flex max-w-[400px] flex-col items-center gap-[24px] p-[32px]' >
101182 < CheckCircle2 className = 'h-[48px] w-[48px] text-green-500' />
102- < p className = 'font-medium text-[20px] text-[var(--text-primary)]' > Welcome!</ p >
183+ < p className = 'font-medium text-[20px] text-[var(--text-primary)]' > You're all set!</ p >
184+ < p className = 'text-center text-[13px] text-[var(--text-secondary)]' >
185+ You've joined { invitation ?. credentialSetName } . Your { providerName } account is already
186+ connected.
187+ </ p >
188+ < p className = 'text-[12px] text-[var(--text-tertiary)]' > Redirecting to workspace...</ p >
189+ < Loader2 className = 'h-[24px] w-[24px] animate-spin text-[var(--text-muted)]' />
190+ </ div >
191+ </ div >
192+ )
193+ }
194+
195+ if ( acceptedState === 'connecting' ) {
196+ return (
197+ < div className = 'flex min-h-screen items-center justify-center bg-[var(--bg)]' >
198+ < div className = 'flex max-w-[400px] flex-col items-center gap-[24px] p-[32px]' >
199+ < ProviderIcon className = 'h-[48px] w-[48px]' />
200+ < p className = 'font-medium text-[20px] text-[var(--text-primary)]' >
201+ Connecting to { providerName } ...
202+ </ p >
103203 < p className = 'text-center text-[13px] text-[var(--text-secondary)]' >
104- You've successfully joined { invitation ?. credentialSetName } . Connect your OAuth
105- credentials in Settings → Integrations .
204+ You've joined { invitation ?. credentialSetName } . You'll be redirected to connect your{ ' ' }
205+ { providerName } account .
106206 </ p >
107- < Button variant = 'tertiary' onClick = { ( ) => router . push ( '/workspace' ) } >
108- Go to Dashboard
109- </ Button >
207+ < Loader2 className = 'h-[24px] w-[24px] animate-spin text-[var(--text-muted)]' />
110208 </ div >
111209 </ div >
112210 )
@@ -116,15 +214,22 @@ export default function CredentialAccountInvitePage() {
116214 < div className = 'flex min-h-screen items-center justify-center bg-[var(--bg)]' >
117215 < div className = 'w-full max-w-[400px] p-[32px]' >
118216 < div className = 'flex flex-col items-center gap-[8px]' >
119- < Shield className = 'h-[48px] w-[48px] text-[var(--brand-400)]' />
120- < p className = 'font-medium text-[20px] text-[var(--text-primary)]' > Join Credential Set</ p >
217+ < ProviderIcon className = 'h-[48px] w-[48px]' />
218+ < p className = 'font-medium text-[20px] text-[var(--text-primary)]' >
219+ Join Email Polling Group
220+ </ p >
121221 < p className = 'text-center text-[13px] text-[var(--text-secondary)]' >
122222 You've been invited to join{ ' ' }
123223 < span className = 'font-medium text-[var(--text-primary)]' >
124224 { invitation ?. credentialSetName }
125225 </ span > { ' ' }
126226 by { invitation ?. organizationName }
127227 </ p >
228+ { invitation ?. providerId && (
229+ < p className = 'mt-[8px] text-center text-[12px] text-[var(--text-tertiary)]' >
230+ You'll be asked to connect your { providerName } account after accepting.
231+ </ p >
232+ ) }
128233 </ div >
129234
130235 < div className = 'mt-[32px] flex flex-col gap-[16px]' >
@@ -141,7 +246,10 @@ export default function CredentialAccountInvitePage() {
141246 Joining...
142247 </ >
143248 ) : (
144- 'Accept Invitation'
249+ < >
250+ < ProviderIcon className = 'mr-[8px] h-[16px] w-[16px]' />
251+ Accept & Connect { providerName }
252+ </ >
145253 ) }
146254 </ Button >
147255 </ >
@@ -158,8 +266,8 @@ export default function CredentialAccountInvitePage() {
158266 </ div >
159267
160268 < p className = 'mt-[24px] text-center text-[11px] text-[var(--text-muted)]' >
161- By joining, you agree to share your OAuth credentials with this credential set for use in
162- automated workflows.
269+ By joining, you agree to share your { providerName } credentials with this polling group for
270+ use in automated email workflows.
163271 </ p >
164272 </ div >
165273 </ div >
0 commit comments