flowlink-auth 2.7.4 → 2.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/SignUp.jsx CHANGED
@@ -1,70 +1,315 @@
1
+ // // src/signup.jsx
2
+ // 'use client'
3
+ // import React, { useState } from 'react'
4
+ // import { useAuth } from './provider.js'
5
+
6
+ // export default function SignUp() {
7
+ // const {
8
+ // publishableKey,
9
+ // baseUrl,
10
+ // redirect,
11
+ // redirectTo,
12
+ // user,
13
+ // loadingUser,
14
+ // fetchMe,
15
+ // setUser
16
+ // } = useAuth()
17
+
18
+ // const [name, setName] = useState('')
19
+ // const [email, setEmail] = useState('')
20
+ // const [password, setPassword] = useState('')
21
+ // const [loading, setLoading] = useState(false)
22
+ // const [error, setError] = useState(null)
23
+ // const [message, setMessage] = useState(null)
24
+
25
+ // if (loadingUser) return null
26
+
27
+ // if (user && redirect) {
28
+ // if (typeof redirectTo === 'function') redirectTo(redirect)
29
+ // else if (typeof window !== 'undefined') window.location.assign(redirect)
30
+ // return null
31
+ // }
32
+
33
+ // async function submit(e) {
34
+ // e.preventDefault()
35
+ // setError(null)
36
+ // setMessage(null)
37
+ // setLoading(true)
38
+
39
+ // const url = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/signup`
40
+
41
+ // try {
42
+ // const res = await fetch(url, {
43
+ // method: 'POST',
44
+ // credentials: 'include',
45
+ // headers: {
46
+ // 'Content-Type': 'application/json',
47
+ // 'x-publishable-key': publishableKey || ''
48
+ // },
49
+ // body: JSON.stringify({ name, email, password })
50
+ // })
51
+
52
+ // const data = await res.json().catch(() => ({}))
53
+ // if (!res.ok) throw new Error(data.error || 'Signup failed')
54
+
55
+ // if (data.user && typeof setUser === 'function') setUser(data.user)
56
+ // if (typeof fetchMe === 'function') await fetchMe()
57
+
58
+ // setMessage('Account created. Redirecting…')
59
+
60
+ // if (redirect) {
61
+ // setTimeout(() => {
62
+ // if (typeof redirectTo === 'function') redirectTo(redirect)
63
+ // else if (typeof window !== 'undefined') window.location.assign(redirect)
64
+ // }, 300)
65
+ // }
66
+ // } catch (err) {
67
+ // setError(err?.message ?? 'Network error')
68
+ // console.error('Signup error:', err)
69
+ // } finally {
70
+ // setLoading(false)
71
+ // }
72
+ // }
73
+
74
+ // async function startOAuthFlow(provider) {
75
+ // // prevent double clicks
76
+ // setError(null)
77
+ // setLoading(true)
78
+
79
+ // try {
80
+ // const rid =
81
+ // (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
82
+ // ? crypto.randomUUID()
83
+ // : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
84
+
85
+ // // callback must match what your SDK start expects and what Google console allows
86
+ // const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`)
87
+
88
+ // // build start URL (server returns { oauthUrl })
89
+ // const sdkBase = (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL)
90
+ // || baseUrl || window.location.origin.replace(/\/+$/, '')
91
+ // const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
92
+
93
+ // // ensure publishableKey exists
94
+ // if (!publishableKey) {
95
+ // throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
96
+ // }
97
+
98
+ // const res = await fetch(startUrl, {
99
+ // method: 'GET',
100
+ // headers: {
101
+ // 'x-publishable-key': publishableKey
102
+ // }
103
+ // })
104
+
105
+ // const data = await res.json().catch(() => null)
106
+ // if (!res.ok) {
107
+ // throw new Error(data?.error || `OAuth start failed (${res.status})`)
108
+ // }
109
+ // if (!data?.oauthUrl) {
110
+ // throw new Error('SDK start did not return oauthUrl')
111
+ // }
112
+
113
+ // // navigate to provider (Google/GitHub)
114
+ // window.location.href = data.oauthUrl
115
+ // } catch (err) {
116
+ // console.error('OAuth start error:', err)
117
+ // setError(err?.message || 'OAuth start failed')
118
+ // setLoading(false)
119
+ // }
120
+ // }
121
+
122
+ // const handleGoogle = (e) => {
123
+ // if (e && typeof e.preventDefault === 'function') e.preventDefault()
124
+ // startOAuthFlow('google')
125
+ // }
126
+
127
+ // const handleGithub = (e) => {
128
+ // if (e && typeof e.preventDefault === 'function') e.preventDefault()
129
+ // startOAuthFlow('github')
130
+ // }
131
+ // return (
132
+ // <div style={overlay}>
133
+ // <div style={modal}>
134
+ // <h2 style={title}>Create account</h2>
135
+
136
+ // <form onSubmit={submit}>
137
+ // <label style={label}>Full name</label>
138
+ // <input
139
+ // name="name"
140
+ // style={input}
141
+ // value={name}
142
+ // onChange={e => setName(e.target.value)}
143
+ // autoComplete="name"
144
+ // />
145
+
146
+ // <label style={label}>Email</label>
147
+ // <input
148
+ // name="email"
149
+ // type="email"
150
+ // style={input}
151
+ // value={email}
152
+ // onChange={e => setEmail(e.target.value)}
153
+ // required
154
+ // autoComplete="email"
155
+ // />
156
+
157
+ // <label style={label}>Password</label>
158
+ // <input
159
+ // name="password"
160
+ // type="password"
161
+ // style={input}
162
+ // value={password}
163
+ // onChange={e => setPassword(e.target.value)}
164
+ // required
165
+ // autoComplete="new-password"
166
+ // />
167
+
168
+ // <div style={{ marginTop: 12 }}>
169
+ // <button style={button} type="submit" disabled={loading}>
170
+ // {loading ? 'Creating...' : 'Create account'}
171
+ // </button>
172
+ // </div>
173
+
174
+ // <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
175
+ // <button
176
+ // type="button"
177
+ // onClick={handleGoogle}
178
+ // style={oauthButtonGoogle}
179
+ // >
180
+ // Continue with Google
181
+ // </button>
182
+
183
+ // <button
184
+ // type="button"
185
+ // onClick={handleGithub}
186
+ // style={oauthButtonGithub}
187
+ // >
188
+ // Continue with GitHub
189
+ // </button>
190
+ // </div>
191
+
192
+ // {error && <div style={errorBox}>{error}</div>}
193
+ // {message && <div style={successBox}>{message}</div>}
194
+ // </form>
195
+ // </div>
196
+ // </div>
197
+ // )
198
+ // }
199
+
200
+ // const overlay = {
201
+ // position: 'fixed',
202
+ // inset: 0,
203
+ // background: 'rgba(0,0,0,0.5)',
204
+ // display: 'flex',
205
+ // justifyContent: 'center',
206
+ // alignItems: 'center',
207
+ // padding: 20,
208
+ // zIndex: 9999
209
+ // }
210
+ // const modal = {
211
+ // width: '100%',
212
+ // maxWidth: 420,
213
+ // background: '#0f1724',
214
+ // borderRadius: 12,
215
+ // padding: 24,
216
+ // color: '#fff'
217
+ // }
218
+ // const title = { margin: 0, fontSize: 20, fontWeight: 600 }
219
+ // const label = { marginTop: 10, display: 'block', fontSize: 13 }
220
+ // const input = {
221
+ // width: '100%',
222
+ // padding: '10px 12px',
223
+ // borderRadius: 8,
224
+ // background: '#0b1220',
225
+ // color: '#fff',
226
+ // border: '1px solid #1e293b',
227
+ // marginTop: 6
228
+ // }
229
+ // const button = {
230
+ // marginTop: 15,
231
+ // width: '100%',
232
+ // padding: '10px 12px',
233
+ // borderRadius: 8,
234
+ // background: '#2563eb',
235
+ // border: 'none',
236
+ // color: '#fff',
237
+ // fontWeight: 600,
238
+ // cursor: 'pointer'
239
+ // }
240
+ // const oauthButtonGoogle = {
241
+ // flex: 1,
242
+ // padding: '10px 12px',
243
+ // borderRadius: 8,
244
+ // background: '#db4437',
245
+ // color: '#fff',
246
+ // border: 'none',
247
+ // cursor: 'pointer'
248
+ // }
249
+ // const oauthButtonGithub = {
250
+ // flex: 1,
251
+ // padding: '10px 12px',
252
+ // borderRadius: 8,
253
+ // background: '#24292f',
254
+ // color: '#fff',
255
+ // border: 'none',
256
+ // cursor: 'pointer'
257
+ // }
258
+ // const errorBox = { marginTop: 10, color: '#ffb4b4', fontSize: 13 }
259
+ // const successBox = { marginTop: 10, color: '#bef264', fontSize: 13 }
260
+
1
261
  // src/signup.jsx
2
262
  'use client'
3
- import React, { useState } from 'react'
4
- import { useAuth } from './provider.js'
5
-
6
- export default function SignUp() {
7
- const {
8
- publishableKey,
9
- baseUrl,
10
- redirect,
11
- redirectTo,
12
- user,
13
- loadingUser,
14
- fetchMe,
15
- setUser
16
- } = useAuth()
263
+ import React, { useState, useRef, useEffect } from 'react'
264
+ import { ToastContainer, toast } from 'react-toastify'
265
+ import 'react-toastify/dist/ReactToastify.css'
266
+ import Link from 'next/link'
17
267
 
268
+ export default function SignUp({ agency = { name: 'MyApp', logo: '' } }) {
18
269
  const [name, setName] = useState('')
19
270
  const [email, setEmail] = useState('')
20
271
  const [password, setPassword] = useState('')
21
272
  const [loading, setLoading] = useState(false)
22
- const [error, setError] = useState(null)
273
+ const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false })
23
274
  const [message, setMessage] = useState(null)
275
+ const [showKeysPanel, setShowKeysPanel] = useState(false)
276
+ const [newKeys, setNewKeys] = useState(null)
277
+ const redirectTimer = useRef(null)
24
278
 
25
- if (loadingUser) return null
26
-
27
- if (user && redirect) {
28
- if (typeof redirectTo === 'function') redirectTo(redirect)
29
- else if (typeof window !== 'undefined') window.location.assign(redirect)
30
- return null
31
- }
279
+ useEffect(() => {
280
+ return () => {
281
+ if (redirectTimer.current) clearTimeout(redirectTimer.current)
282
+ }
283
+ }, [])
32
284
 
285
+ // Example submit — replace with real auth call
33
286
  async function submit(e) {
34
287
  e.preventDefault()
35
- setError(null)
288
+ if (loading) return
36
289
  setMessage(null)
37
290
  setLoading(true)
38
291
 
39
- const url = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/signup`
40
-
41
292
  try {
42
- const res = await fetch(url, {
43
- method: 'POST',
44
- credentials: 'include',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- 'x-publishable-key': publishableKey || ''
48
- },
49
- body: JSON.stringify({ name, email, password })
50
- })
51
-
52
- const data = await res.json().catch(() => ({}))
53
- if (!res.ok) throw new Error(data.error || 'Signup failed')
54
-
55
- if (data.user && typeof setUser === 'function') setUser(data.user)
56
- if (typeof fetchMe === 'function') await fetchMe()
293
+ // simulate API
294
+ await new Promise((r) => setTimeout(r, 700))
295
+
296
+ // example error cases (replace with real server response handling)
297
+ if (!email.includes('@')) throw new Error('Invalid email address')
298
+ if (password.length < 8) throw new Error('Password must be at least 8 characters')
299
+ if (email === 'taken@example.com') throw new Error('User already exists')
57
300
 
301
+ // success
58
302
  setMessage('Account created. Redirecting…')
303
+ toast.success('Account created. Redirecting…')
59
304
 
60
- if (redirect) {
61
- setTimeout(() => {
62
- if (typeof redirectTo === 'function') redirectTo(redirect)
63
- else if (typeof window !== 'undefined') window.location.assign(redirect)
64
- }, 300)
65
- }
305
+ // simulate redirect timer
306
+ redirectTimer.current = setTimeout(() => {
307
+ // real redirect logic here
308
+ }, 300)
66
309
  } catch (err) {
67
- setError(err?.message ?? 'Network error')
310
+ const text = err?.message ?? 'Network error'
311
+ // show toast with black background (ToastContainer configured below)
312
+ toast.error(text)
68
313
  console.error('Signup error:', err)
69
314
  } finally {
70
315
  setLoading(false)
@@ -72,189 +317,453 @@ export default function SignUp() {
72
317
  }
73
318
 
74
319
  async function startOAuthFlow(provider) {
75
- // prevent double clicks
76
- setError(null)
77
- setLoading(true)
78
-
79
- try {
80
- const rid =
81
- (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
82
- ? crypto.randomUUID()
83
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
84
-
85
- // callback must match what your SDK start expects and what Google console allows
86
- const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`)
87
-
88
- // build start URL (server returns { oauthUrl })
89
- const sdkBase = (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL)
90
- || baseUrl || window.location.origin.replace(/\/+$/, '')
91
- const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
92
-
93
- // ensure publishableKey exists
94
- if (!publishableKey) {
95
- throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
96
- }
320
+ if (loading || loadingOauth[provider]) return
321
+ setLoadingOauth(prev => ({ ...prev, [provider]: true }))
97
322
 
98
- const res = await fetch(startUrl, {
99
- method: 'GET',
100
- headers: {
101
- 'x-publishable-key': publishableKey
102
- }
103
- })
323
+ try {
324
+ // simulate network latency
325
+ await new Promise((r) => setTimeout(r, 600))
104
326
 
105
- const data = await res.json().catch(() => null)
106
- if (!res.ok) {
107
- throw new Error(data?.error || `OAuth start failed (${res.status})`)
108
- }
109
- if (!data?.oauthUrl) {
110
- throw new Error('SDK start did not return oauthUrl')
327
+ // replace with actual start flow & redirect
328
+ toast.info(`Starting ${provider} sign-in...`)
329
+ // on success real flow will redirect, so no need to unset loading in that case
330
+ } catch (err) {
331
+ toast.error(err?.message || 'OAuth start failed')
332
+ console.error('OAuth start error:', err)
333
+ setLoadingOauth(prev => ({ ...prev, [provider]: false }))
111
334
  }
335
+ }
112
336
 
113
- // navigate to provider (Google/GitHub)
114
- window.location.href = data.oauthUrl
115
- } catch (err) {
116
- console.error('OAuth start error:', err)
117
- setError(err?.message || 'OAuth start failed')
118
- setLoading(false)
337
+ const handleGoogle = (e) => {
338
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
339
+ startOAuthFlow('google')
119
340
  }
120
- }
121
341
 
122
- const handleGoogle = (e) => {
123
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
124
- startOAuthFlow('google')
125
- }
342
+ const handleGithub = (e) => {
343
+ if (e && typeof e.preventDefault === 'function') e.preventDefault()
344
+ startOAuthFlow('github')
345
+ }
126
346
 
127
- const handleGithub = (e) => {
128
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
129
- startOAuthFlow('github')
130
- }
131
347
  return (
132
348
  <div style={overlay}>
349
+ <ToastContainer
350
+ position="top-right"
351
+ autoClose={5000}
352
+ newestOnTop
353
+ closeOnClick
354
+ pauseOnHover
355
+ draggable
356
+ toastStyle={{
357
+ background: '#000000',
358
+ color: '#ffffff',
359
+ borderRadius: 10,
360
+ boxShadow: '0 6px 18px rgba(0,0,0,0.6)',
361
+ padding: '10px 14px',
362
+ fontWeight: 500
363
+ }}
364
+ />
365
+
133
366
  <div style={modal}>
134
- <h2 style={title}>Create account</h2>
135
-
136
- <form onSubmit={submit}>
137
- <label style={label}>Full name</label>
138
- <input
139
- name="name"
140
- style={input}
141
- value={name}
142
- onChange={e => setName(e.target.value)}
143
- autoComplete="name"
144
- />
145
-
146
- <label style={label}>Email</label>
147
- <input
148
- name="email"
149
- type="email"
150
- style={input}
151
- value={email}
152
- onChange={e => setEmail(e.target.value)}
153
- required
154
- autoComplete="email"
155
- />
156
-
157
- <label style={label}>Password</label>
158
- <input
159
- name="password"
160
- type="password"
161
- style={input}
162
- value={password}
163
- onChange={e => setPassword(e.target.value)}
164
- required
165
- autoComplete="new-password"
166
- />
167
-
168
- <div style={{ marginTop: 12 }}>
169
- <button style={button} type="submit" disabled={loading}>
170
- {loading ? 'Creating...' : 'Create account'}
171
- </button>
172
- </div>
367
+ <div style={modalInner}>
368
+ <header style={header}>
369
+ <div style={brandRow}>
370
+ <div style={logo}>
371
+ {/* if you have a real logo, replace with <Image src={agency.logo} ... /> */}
372
+ <div style={logoCircle} aria-hidden />
373
+ </div>
374
+ <div>
375
+ <h1 style={title}>Sign up to {agency.name}</h1>
376
+ <div style={subtitle}>Welcome! Create your account.</div>
377
+ </div>
378
+ </div>
379
+ </header>
173
380
 
174
- <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
381
+ <section style={oauthSection}>
175
382
  <button
176
- type="button"
177
383
  onClick={handleGoogle}
178
- style={oauthButtonGoogle}
384
+ type="button"
385
+ style={{ ...oauthButton, ...oauthGoogle }}
386
+ disabled={loading || loadingOauth.google}
387
+ aria-disabled={loading || loadingOauth.google}
179
388
  >
180
- Continue with Google
389
+ <svg width={18} style={{ marginRight: 10 }} viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" fill="#000000" aria-hidden>
390
+ <path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"></path>
391
+ <path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"></path>
392
+ <path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"></path>
393
+ <path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"></path>
394
+ </svg>
395
+ <span>{loadingOauth.google ? 'Loading...' : 'Continue with Google'}</span>
181
396
  </button>
182
397
 
183
398
  <button
184
- type="button"
185
399
  onClick={handleGithub}
186
- style={oauthButtonGithub}
400
+ type="button"
401
+ style={{ ...oauthButton, ...oauthGithub }}
402
+ disabled={loading || loadingOauth.github}
403
+ aria-disabled={loading || loadingOauth.github}
187
404
  >
188
- Continue with GitHub
405
+ <svg width={18} style={{ marginRight: 10 }} xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 20 20" aria-hidden>
406
+ <path fillRule="evenodd" d="M10 .333A9.911 9.911 0 0 0 6.866 19.65c.5.092.678-.215.678-.477 0-.237-.01-1.017-.014-1.845-2.757.6-3.338-1.169-3.338-1.169a2.627 2.627 0 0 0-1.1-1.451c-.9-.615.07-.6.07-.6a2.084 2.084 0 0 1 1.518 1.021 2.11 2.11 0 0 0 2.884.823c.044-.503.268-.973.63-1.325-2.2-.25-4.516-1.1-4.516-4.9A3.832 3.832 0 0 1 4.7 7.068a3.56 3.56 0 0 1 .095-2.623s.832-.266 2.726 1.016a9.409 9.409 0 0 1 4.962 0c1.89-1.282 2.717-1.016 2.717-1.016.366.83.402 1.768.1 2.623a3.827 3.827 0 0 1 1.02 2.659c0 3.807-2.319 4.644-4.525 4.889a2.366 2.366 0 0 1 .673 1.834c0 1.326-.012 2.394-.012 2.72 0 .263.18.572.681.475A9.911 9.911 0 0 0 10 .333Z" clipRule="evenodd" />
407
+ </svg>
408
+ <span>{loadingOauth.github ? 'Loading...' : 'Continue with GitHub'}</span>
189
409
  </button>
410
+ </section>
411
+
412
+ <div style={dividerRow}>
413
+ <div style={line} />
414
+ <div style={orText}>or</div>
415
+ <div style={line} />
190
416
  </div>
191
417
 
192
- {error && <div style={errorBox}>{error}</div>}
193
- {message && <div style={successBox}>{message}</div>}
194
- </form>
418
+ <form onSubmit={submit} style={form}>
419
+ <label style={label} htmlFor="name">
420
+ <span style={labelText}>Name</span>
421
+ <input
422
+ id="name"
423
+ type="text"
424
+ value={name}
425
+ onChange={e => setName(e.target.value)}
426
+ placeholder="Your name"
427
+ style={input}
428
+ autoComplete="name"
429
+ />
430
+ </label>
431
+
432
+ <label style={label} htmlFor="email">
433
+ <span style={labelText}>Email address</span>
434
+ <input
435
+ id="email"
436
+ type="email"
437
+ value={email}
438
+ onChange={e => setEmail(e.target.value)}
439
+ required
440
+ placeholder="you@example.com"
441
+ style={input}
442
+ autoComplete="email"
443
+ />
444
+ </label>
445
+
446
+ <label style={label} htmlFor="password">
447
+ <span style={labelText}>Password</span>
448
+ <input
449
+ id="password"
450
+ type="password"
451
+ value={password}
452
+ onChange={e => setPassword(e.target.value)}
453
+ required
454
+ placeholder="••••••••"
455
+ style={input}
456
+ autoComplete="new-password"
457
+ />
458
+ </label>
459
+
460
+ <button
461
+ type="submit"
462
+ style={submitButton}
463
+ disabled={loading}
464
+ aria-disabled={loading}
465
+ >
466
+ {loading ? 'Signing up...' : 'Sign Up'}
467
+ </button>
468
+ </form>
469
+ </div>
470
+
471
+ <div style={modalFooter}>
472
+ <div style={belowRow}>
473
+ <span style={muted}>Already have an account? </span>
474
+ <Link href="/sign-in" style={link}>Sign in</Link>
475
+ </div>
476
+ <div style={dividerThin} />
477
+ <div style={secured}>
478
+ <div style={securedText}>Secured by auth</div>
479
+ </div>
480
+ </div>
195
481
  </div>
482
+
483
+ {showKeysPanel && newKeys && (
484
+ <div style={keysOverlay}>
485
+ <div style={keysPanel}>
486
+ <h3 style={{ margin: 0, marginBottom: 8 }}>Your API keys — copy now</h3>
487
+ <p style={{ margin: 0, marginBottom: 12, color: '#4b5563', fontSize: 13 }}>Secret key is shown only once. Save it securely.</p>
488
+
489
+ <div style={{ marginBottom: 10 }}>
490
+ <div style={{ fontSize: 12, color: '#6b7280' }}>Publishable key (use in client)</div>
491
+ <pre style={keyBox}>{newKeys.publishableKey}</pre>
492
+ <button onClick={() => navigator.clipboard.writeText(newKeys.publishableKey)} style={copyBtn}>Copy publishable key</button>
493
+ </div>
494
+
495
+ <div style={{ marginBottom: 6 }}>
496
+ <div style={{ fontSize: 12, color: '#6b7280' }}>Secret key (server only)</div>
497
+ <pre style={keyBox}>{newKeys.secretKey}</pre>
498
+ <button onClick={() => navigator.clipboard.writeText(newKeys.secretKey)} style={copyBtn}>Copy secret key</button>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ )}
196
503
  </div>
197
504
  )
198
505
  }
199
506
 
507
+ /* Styles (JS objects) */
508
+
509
+ /* overlay: translucent so underlying content is visible; backdrop blur and subtle vignette */
200
510
  const overlay = {
201
511
  position: 'fixed',
202
512
  inset: 0,
203
- background: 'rgba(0,0,0,0.5)',
204
513
  display: 'flex',
205
- justifyContent: 'center',
206
514
  alignItems: 'center',
515
+ justifyContent: 'center',
207
516
  padding: 20,
517
+ background: 'linear-gradient(180deg, rgba(2,6,23,0.25), rgba(2,6,23,0.35))',
518
+ backdropFilter: 'blur(6px)',
519
+ minHeight: '100vh',
208
520
  zIndex: 9999
209
521
  }
522
+
523
+ /* modal itself uses semi-transparent background + strong soft shadow,
524
+ so things beneath are slightly visible but content still readable */
210
525
  const modal = {
211
526
  width: '100%',
212
- maxWidth: 420,
213
- background: '#0f1724',
214
- borderRadius: 12,
215
- padding: 24,
527
+ maxWidth: 560,
528
+ borderRadius: 14,
529
+ background: 'rgba(10,14,20,0.65)', // translucent neutral
530
+ border: '1px solid rgba(148,163,184,0.12)', // more visible neutral border
531
+ boxShadow: '0 20px 50px rgba(2,6,23,0.55), inset 0 1px 0 rgba(255,255,255,0.02)',
532
+ overflow: 'hidden',
533
+ display: 'flex',
534
+ flexDirection: 'column',
535
+ transform: 'translateY(-6px)'
536
+ }
537
+
538
+ const modalInner = {
539
+ padding: 18,
540
+ display: 'flex',
541
+ flexDirection: 'column',
542
+ gap: 12
543
+ }
544
+
545
+ /* HEADER / BRAND */
546
+ const header = {
547
+ paddingBottom: 6,
548
+ borderBottom: '1px solid rgba(148,163,184,0.04)'
549
+ }
550
+
551
+ const brandRow = {
552
+ display: 'flex',
553
+ alignItems: 'center',
554
+ gap: 12
555
+ }
556
+
557
+ const logo = {
558
+ width: 44,
559
+ height: 44,
560
+ display: 'flex',
561
+ alignItems: 'center',
562
+ justifyContent: 'center'
563
+ }
564
+
565
+ const logoCircle = {
566
+ width: 36,
567
+ height: 36,
568
+ borderRadius: 999,
569
+ background: 'linear-gradient(135deg,#2f3438,#11151a)',
570
+ boxShadow: '0 4px 12px rgba(2,6,23,0.6)'
571
+ }
572
+
573
+ const title = {
574
+ margin: 0,
575
+ fontSize: 18,
576
+ fontWeight: 600,
577
+ color: '#e6e6e6'
578
+ }
579
+
580
+ const subtitle = {
581
+ fontSize: 13,
582
+ color: 'rgba(230,230,230,0.72)',
583
+ marginTop: 4
584
+ }
585
+
586
+ /* OAUTH ROW */
587
+ const oauthSection = {
588
+ marginTop: 8,
589
+ display: 'flex',
590
+ gap: 8
591
+ }
592
+
593
+ const oauthButton = {
594
+ flex: 1,
595
+ display: 'inline-flex',
596
+ alignItems: 'center',
597
+ justifyContent: 'center',
598
+ padding: '10px 12px',
599
+ borderRadius: 10,
600
+ border: '1px solid rgba(148,163,184,0.08)',
601
+ fontSize: 14,
602
+ cursor: 'pointer',
603
+ userSelect: 'none',
604
+ gap: 8,
605
+ minHeight: 40
606
+ }
607
+
608
+ const oauthGoogle = {
609
+ background: 'linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))',
216
610
  color: '#fff'
217
611
  }
218
- const title = { margin: 0, fontSize: 20, fontWeight: 600 }
219
- const label = { marginTop: 10, display: 'block', fontSize: 13 }
612
+
613
+ const oauthGithub = {
614
+ background: 'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00))',
615
+ color: '#fff'
616
+ }
617
+
618
+ /* DIVIDER */
619
+ const dividerRow = {
620
+ display: 'flex',
621
+ alignItems: 'center',
622
+ gap: 10,
623
+ marginTop: 12
624
+ }
625
+
626
+ const line = {
627
+ flex: 1,
628
+ height: 1,
629
+ background: 'rgba(148,163,184,0.06)'
630
+ }
631
+
632
+ const orText = {
633
+ fontSize: 13,
634
+ color: 'rgba(230,230,230,0.65)',
635
+ padding: '0 8px'
636
+ }
637
+
638
+ /* FORM */
639
+ const form = {
640
+ display: 'flex',
641
+ flexDirection: 'column',
642
+ gap: 10,
643
+ marginTop: 6
644
+ }
645
+
646
+ const label = {
647
+ display: 'flex',
648
+ flexDirection: 'column',
649
+ gap: 6
650
+ }
651
+
652
+ const labelText = {
653
+ fontSize: 13,
654
+ color: 'rgba(230,230,230,0.68)'
655
+ }
656
+
220
657
  const input = {
221
658
  width: '100%',
222
659
  padding: '10px 12px',
223
- borderRadius: 8,
224
- background: '#0b1220',
225
- color: '#fff',
226
- border: '1px solid #1e293b',
227
- marginTop: 6
660
+ borderRadius: 10,
661
+ background: 'rgba(255,255,255,0.02)',
662
+ color: '#e6e6e6',
663
+ border: '1px solid rgba(148,163,184,0.10)', // slightly stronger border for visibility
664
+ fontSize: 14,
665
+ outline: 'none',
666
+ boxSizing: 'border-box',
667
+ transition: 'box-shadow 120ms, border-color 120ms'
228
668
  }
229
- const button = {
230
- marginTop: 15,
669
+
670
+ const submitButton = {
671
+ marginTop: 6,
231
672
  width: '100%',
232
673
  padding: '10px 12px',
233
- borderRadius: 8,
234
- background: '#2563eb',
674
+ borderRadius: 10,
675
+ background: 'linear-gradient(180deg,#475569,#94a3b8)', // neutral blue-gray gradient
235
676
  border: 'none',
236
677
  color: '#fff',
237
678
  fontWeight: 600,
238
- cursor: 'pointer'
679
+ cursor: 'pointer',
680
+ fontSize: 15,
681
+ display: 'inline-flex',
682
+ alignItems: 'center',
683
+ justifyContent: 'center',
684
+ minHeight: 44
239
685
  }
240
- const oauthButtonGoogle = {
241
- flex: 1,
242
- padding: '10px 12px',
686
+
687
+ /* FOOTER / BELOW */
688
+ const modalFooter = {
689
+ padding: '12px 18px 18px 18px',
690
+ borderTop: '1px solid rgba(148,163,184,0.03)',
691
+ display: 'flex',
692
+ flexDirection: 'column',
693
+ gap: 8
694
+ }
695
+
696
+ const belowRow = {
697
+ textAlign: 'center',
698
+ display: 'flex',
699
+ justifyContent: 'center',
700
+ gap: 8,
701
+ alignItems: 'center'
702
+ }
703
+
704
+ const muted = {
705
+ color: 'rgba(230,230,230,0.66)',
706
+ fontSize: 13
707
+ }
708
+
709
+ const link = {
710
+ color: '#9fb0d9',
711
+ textDecoration: 'none',
712
+ fontWeight: 600
713
+ }
714
+
715
+ const dividerThin = {
716
+ height: 1,
717
+ background: 'rgba(148,163,184,0.04)',
718
+ marginTop: 6,
719
+ marginBottom: 6
720
+ }
721
+
722
+ const secured = {
723
+ textAlign: 'center'
724
+ }
725
+
726
+ const securedText = {
727
+ color: 'rgba(230,230,230,0.9)',
728
+ fontSize: 13,
729
+ fontWeight: 600
730
+ }
731
+
732
+ /* KEYS PANEL */
733
+ const keysOverlay = {
734
+ position: 'fixed',
735
+ inset: 0,
736
+ zIndex: 20000,
737
+ display: 'flex',
738
+ alignItems: 'center',
739
+ justifyContent: 'center',
740
+ background: 'rgba(2,6,23,0.45)'
741
+ }
742
+
743
+ const keysPanel = {
744
+ background: '#fff',
745
+ color: '#111827',
746
+ padding: 18,
747
+ borderRadius: 10,
748
+ maxWidth: 720,
749
+ width: '95%',
750
+ boxShadow: '0 12px 40px rgba(2,6,23,0.5)'
751
+ }
752
+
753
+ const keyBox = {
754
+ padding: 10,
755
+ background: '#f3f4f6',
243
756
  borderRadius: 8,
244
- background: '#db4437',
245
- color: '#fff',
246
- border: 'none',
247
- cursor: 'pointer'
757
+ overflowX: 'auto',
758
+ fontSize: 13
248
759
  }
249
- const oauthButtonGithub = {
250
- flex: 1,
251
- padding: '10px 12px',
760
+
761
+ const copyBtn = {
762
+ marginTop: 8,
763
+ padding: '8px 10px',
252
764
  borderRadius: 8,
253
- background: '#24292f',
765
+ background: '#111827',
254
766
  color: '#fff',
255
767
  border: 'none',
256
768
  cursor: 'pointer'
257
769
  }
258
- const errorBox = { marginTop: 10, color: '#ffb4b4', fontSize: 13 }
259
- const successBox = { marginTop: 10, color: '#bef264', fontSize: 13 }
260
-