flowlink-auth 2.8.1 → 2.8.3
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/dist/SignIn.js +181 -31
- package/dist/SignUp.js +259 -262
- package/package.json +1 -1
- package/src/SignIn.jsx +206 -43
- package/src/SignUp.jsx +326 -326
package/src/SignIn.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/signin.jsx
|
|
2
1
|
'use client'
|
|
3
|
-
import React, { useState } from 'react'
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
4
4
|
import { useAuth } from './provider.js'
|
|
5
5
|
|
|
6
6
|
export default function SignIn({ onSuccess } = {}) {
|
|
@@ -14,13 +14,51 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
14
14
|
completeLogin,
|
|
15
15
|
fetchMe,
|
|
16
16
|
setUser
|
|
17
|
-
} = useAuth()
|
|
17
|
+
} = (typeof useAuth === 'function' ? useAuth() : {}) || {}
|
|
18
18
|
|
|
19
19
|
const [email, setEmail] = useState('')
|
|
20
20
|
const [password, setPassword] = useState('')
|
|
21
21
|
const [loading, setLoading] = useState(false)
|
|
22
|
-
const [
|
|
23
|
-
|
|
22
|
+
const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false })
|
|
23
|
+
|
|
24
|
+
const redirectTimer = useRef(null)
|
|
25
|
+
const toastId = useRef(0)
|
|
26
|
+
const [toasts, setToasts] = useState([])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// soft-disable pinch-zoom on mobile while mounted
|
|
30
|
+
const meta = document.createElement('meta')
|
|
31
|
+
meta.name = 'viewport'
|
|
32
|
+
meta.content = 'width=device-width, initial-scale=1, maximum-scale=1'
|
|
33
|
+
document.head.appendChild(meta)
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
if (redirectTimer.current) clearTimeout(redirectTimer.current)
|
|
37
|
+
const existing = document.querySelector('meta[name="viewport"]')
|
|
38
|
+
if (existing && existing.content === meta.content) document.head.removeChild(existing)
|
|
39
|
+
// clear timers for toasts
|
|
40
|
+
toasts.forEach(t => { if (t._timer) clearTimeout(t._timer) })
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
// Toast helpers (black background)
|
|
46
|
+
function showToast(type, message, ms = 5000) {
|
|
47
|
+
const id = ++toastId.current
|
|
48
|
+
const t = { id, type, message, _timer: null }
|
|
49
|
+
setToasts(prev => [t, ...prev].slice(0, 6))
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
setToasts(prev => prev.filter(x => x.id !== id))
|
|
52
|
+
}, ms)
|
|
53
|
+
t._timer = timer
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function removeToast(id) {
|
|
57
|
+
setToasts(prev => {
|
|
58
|
+
prev.forEach(t => { if (t.id === id && t._timer) clearTimeout(t._timer) })
|
|
59
|
+
return prev.filter(x => x.id !== id)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
24
62
|
|
|
25
63
|
if (loadingUser) return null
|
|
26
64
|
|
|
@@ -32,19 +70,18 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
32
70
|
|
|
33
71
|
async function submit(e) {
|
|
34
72
|
e.preventDefault()
|
|
35
|
-
|
|
36
|
-
|
|
73
|
+
if (loading) return
|
|
74
|
+
setLoading(true)
|
|
37
75
|
|
|
38
76
|
if (!email || !password) {
|
|
39
|
-
|
|
77
|
+
showToast('error', 'Email and password are required')
|
|
78
|
+
setLoading(false)
|
|
40
79
|
return
|
|
41
80
|
}
|
|
42
81
|
|
|
43
|
-
|
|
82
|
+
const endpoint = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/login`
|
|
44
83
|
|
|
45
84
|
try {
|
|
46
|
-
const endpoint = `${(baseUrl || '').replace(/\/+$/, '')}/api/sdk/login`
|
|
47
|
-
|
|
48
85
|
const res = await fetch(endpoint, {
|
|
49
86
|
method: 'POST',
|
|
50
87
|
credentials: 'include',
|
|
@@ -57,9 +94,9 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
57
94
|
|
|
58
95
|
const ct = res.headers.get('content-type') || ''
|
|
59
96
|
let data = {}
|
|
60
|
-
if (ct.includes('application/json')) data = await res.json()
|
|
97
|
+
if (ct.includes('application/json')) data = await res.json().catch(() => ({}))
|
|
61
98
|
else {
|
|
62
|
-
const text = await res.text()
|
|
99
|
+
const text = await res.text().catch(() => '')
|
|
63
100
|
throw new Error(`Unexpected response (status ${res.status}): ${text.slice(0, 200)}`)
|
|
64
101
|
}
|
|
65
102
|
|
|
@@ -84,36 +121,33 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
84
121
|
try { onSuccess(data) } catch (_) {}
|
|
85
122
|
}
|
|
86
123
|
|
|
87
|
-
|
|
124
|
+
showToast('success', 'Signed in. Redirecting...')
|
|
88
125
|
if (redirect) {
|
|
89
|
-
setTimeout(() => {
|
|
126
|
+
redirectTimer.current = setTimeout(() => {
|
|
90
127
|
if (typeof redirectTo === 'function') redirectTo(redirect)
|
|
91
128
|
else if (typeof window !== 'undefined') window.location.assign(redirect)
|
|
92
129
|
}, 250)
|
|
93
130
|
}
|
|
94
131
|
} catch (err) {
|
|
95
|
-
|
|
132
|
+
showToast('error', err?.message || 'Network error')
|
|
133
|
+
console.error('Signin error:', err)
|
|
96
134
|
} finally {
|
|
97
135
|
setLoading(false)
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
|
|
101
|
-
// --- OAuth start flow (Google / GitHub) ---
|
|
102
139
|
async function startOAuthFlow(provider) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
// prevent double start
|
|
141
|
+
if (loading || loadingOauth[provider]) return
|
|
142
|
+
setLoadingOauth(prev => ({ ...prev, [provider]: true }))
|
|
106
143
|
try {
|
|
107
144
|
const rid =
|
|
108
145
|
(typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
|
|
109
146
|
? crypto.randomUUID()
|
|
110
147
|
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const sdkBase = (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL)
|
|
116
|
-
|| baseUrl || 'http://localhost:3001'
|
|
149
|
+
const callbackUrl = encodeURIComponent(`${typeof window !== 'undefined' ? window.location.origin : ''}/signin`)
|
|
150
|
+
const sdkBase = baseUrl || (typeof window !== 'undefined' ? window.location.origin.replace(/\/+$/, '') : '')
|
|
117
151
|
const startUrl = `${sdkBase.replace(/\/+$/, '')}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
|
|
118
152
|
|
|
119
153
|
if (!publishableKey) {
|
|
@@ -130,11 +164,11 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
130
164
|
if (!res.ok) throw new Error(data?.error || `OAuth start failed (${res.status})`)
|
|
131
165
|
if (!data?.oauthUrl) throw new Error('SDK start did not return oauthUrl')
|
|
132
166
|
|
|
133
|
-
window.location.href = data.oauthUrl
|
|
167
|
+
if (typeof window !== 'undefined') window.location.href = data.oauthUrl
|
|
134
168
|
} catch (err) {
|
|
169
|
+
showToast('error', err?.message || 'OAuth start failed')
|
|
135
170
|
console.error('OAuth start error:', err)
|
|
136
|
-
|
|
137
|
-
setLoading(false)
|
|
171
|
+
setLoadingOauth(prev => ({ ...prev, [provider]: false }))
|
|
138
172
|
}
|
|
139
173
|
}
|
|
140
174
|
|
|
@@ -150,6 +184,28 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
150
184
|
|
|
151
185
|
return (
|
|
152
186
|
<div style={overlay}>
|
|
187
|
+
{/* Toasts */}
|
|
188
|
+
<div style={toastContainer} aria-live="polite" aria-atomic="true">
|
|
189
|
+
{toasts.map(t => (
|
|
190
|
+
<div
|
|
191
|
+
key={t.id}
|
|
192
|
+
role="status"
|
|
193
|
+
style={{
|
|
194
|
+
...toastBase,
|
|
195
|
+
...(t.type === 'error' ? toastError : t.type === 'success' ? toastSuccess : toastInfo)
|
|
196
|
+
}}
|
|
197
|
+
onMouseEnter={() => { if (t._timer) clearTimeout(t._timer) }}
|
|
198
|
+
onMouseLeave={() => {
|
|
199
|
+
const timer = setTimeout(() => removeToast(t.id), 3000)
|
|
200
|
+
setToasts(prev => prev.map(x => x.id === t.id ? { ...x, _timer: timer } : x))
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div style={{ flex: 1 }}>{t.message}</div>
|
|
204
|
+
<button aria-label="Dismiss" onClick={() => removeToast(t.id)} style={toastCloseBtn}>✕</button>
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
153
209
|
<div style={modal}>
|
|
154
210
|
<h2 style={title}>Sign in</h2>
|
|
155
211
|
<p style={subtitle}>Welcome back — enter your credentials.</p>
|
|
@@ -180,17 +236,23 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
180
236
|
</div>
|
|
181
237
|
|
|
182
238
|
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
|
183
|
-
<button type="button" onClick={handleGoogle} style={oauthButtonGoogle} disabled={loading}>
|
|
184
|
-
|
|
239
|
+
<button type="button" onClick={handleGoogle} style={oauthButtonGoogle} disabled={loading || loadingOauth.google}>
|
|
240
|
+
<svg width={18} style={{ marginRight: 8 }} viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" fill="#000000" aria-hidden>
|
|
241
|
+
<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>
|
|
242
|
+
<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>
|
|
243
|
+
<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>
|
|
244
|
+
<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>
|
|
245
|
+
</svg>
|
|
246
|
+
<span>{loadingOauth.google ? 'Loading...' : 'Google'}</span>
|
|
185
247
|
</button>
|
|
186
248
|
|
|
187
|
-
<button type="button" onClick={handleGithub} style={oauthButtonGithub} disabled={loading}>
|
|
188
|
-
|
|
249
|
+
<button type="button" onClick={handleGithub} style={oauthButtonGithub} disabled={loading || loadingOauth.github}>
|
|
250
|
+
<svg width={18} style={{ marginRight: 8 }} xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 20 20" aria-hidden>
|
|
251
|
+
<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" />
|
|
252
|
+
</svg>
|
|
253
|
+
<span>{loadingOauth.github ? 'Loading...' : 'GitHub'}</span>
|
|
189
254
|
</button>
|
|
190
255
|
</div>
|
|
191
|
-
|
|
192
|
-
{error && <div style={errorBox}>{error}</div>}
|
|
193
|
-
{message && <div style={successBox}>{message}</div>}
|
|
194
256
|
</form>
|
|
195
257
|
</div>
|
|
196
258
|
</div>
|
|
@@ -198,15 +260,116 @@ export default function SignIn({ onSuccess } = {}) {
|
|
|
198
260
|
}
|
|
199
261
|
|
|
200
262
|
/* styles */
|
|
201
|
-
const overlay = {
|
|
202
|
-
|
|
263
|
+
const overlay = {
|
|
264
|
+
position: 'fixed',
|
|
265
|
+
inset: 0,
|
|
266
|
+
display: 'block', // allow page scroll
|
|
267
|
+
padding: 20,
|
|
268
|
+
background: 'linear-gradient(180deg, rgba(2,6,23,0.22), rgba(2,6,23,0.32))',
|
|
269
|
+
backdropFilter: 'blur(6px)',
|
|
270
|
+
overflowY: 'auto',
|
|
271
|
+
WebkitOverflowScrolling: 'touch',
|
|
272
|
+
minHeight: '100vh',
|
|
273
|
+
zIndex: 9999
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const modal = {
|
|
277
|
+
width: '100%',
|
|
278
|
+
maxWidth: 460,
|
|
279
|
+
margin: '40px auto',
|
|
280
|
+
borderRadius: 14,
|
|
281
|
+
background: 'linear-gradient(180deg, rgba(15,19,24,0.85), rgba(10,12,16,0.85))',
|
|
282
|
+
border: '1px solid rgba(99,102,106,0.12)',
|
|
283
|
+
boxShadow: '0 18px 48px rgba(2,6,23,0.55), inset 0 1px 0 rgba(255,255,255,0.02)',
|
|
284
|
+
padding: 22,
|
|
285
|
+
color: '#fff'
|
|
286
|
+
}
|
|
287
|
+
|
|
203
288
|
const title = { margin: 0, fontSize: 20, fontWeight: 600 }
|
|
204
289
|
const subtitle = { marginTop: 6, marginBottom: 14, color: '#cbd5e1', fontSize: 13 }
|
|
205
290
|
const label = { display: 'block', color: '#cbd5e1', fontSize: 13, marginTop: 8 }
|
|
206
|
-
const input = {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
291
|
+
const input = {
|
|
292
|
+
width: '100%',
|
|
293
|
+
padding: '10px 12px',
|
|
294
|
+
marginTop: 6,
|
|
295
|
+
borderRadius: 10,
|
|
296
|
+
border: '1px solid rgba(148,163,184,0.10)',
|
|
297
|
+
background: 'rgba(255,255,255,0.02)',
|
|
298
|
+
color: '#e6e6e6',
|
|
299
|
+
boxSizing: 'border-box'
|
|
300
|
+
}
|
|
301
|
+
const button = {
|
|
302
|
+
width: '100%',
|
|
303
|
+
padding: '10px 12px',
|
|
304
|
+
borderRadius: 10,
|
|
305
|
+
background: 'linear-gradient(90deg,#06b6d4,#2563eb)',
|
|
306
|
+
color: '#0b1220',
|
|
307
|
+
border: 'none',
|
|
308
|
+
fontWeight: 700,
|
|
309
|
+
cursor: 'pointer'
|
|
310
|
+
}
|
|
311
|
+
const oauthButtonGoogle = {
|
|
312
|
+
flex: 1,
|
|
313
|
+
padding: '10px 12px',
|
|
314
|
+
borderRadius: 10,
|
|
315
|
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))',
|
|
316
|
+
color: '#fff',
|
|
317
|
+
border: '1px solid rgba(148,163,184,0.08)',
|
|
318
|
+
cursor: 'pointer',
|
|
319
|
+
display: 'inline-flex',
|
|
320
|
+
alignItems: 'center',
|
|
321
|
+
justifyContent: 'center',
|
|
322
|
+
gap: 8
|
|
323
|
+
}
|
|
324
|
+
const oauthButtonGithub = {
|
|
325
|
+
flex: 1,
|
|
326
|
+
padding: '10px 12px',
|
|
327
|
+
borderRadius: 10,
|
|
328
|
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00))',
|
|
329
|
+
color: '#fff',
|
|
330
|
+
border: '1px solid rgba(148,163,184,0.08)',
|
|
331
|
+
cursor: 'pointer',
|
|
332
|
+
display: 'inline-flex',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
justifyContent: 'center',
|
|
335
|
+
gap: 8
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Toast styles (black) */
|
|
339
|
+
const toastContainer = {
|
|
340
|
+
position: 'fixed',
|
|
341
|
+
top: 18,
|
|
342
|
+
right: 18,
|
|
343
|
+
width: 360,
|
|
344
|
+
maxWidth: 'calc(100% - 36px)',
|
|
345
|
+
display: 'flex',
|
|
346
|
+
flexDirection: 'column',
|
|
347
|
+
gap: 10,
|
|
348
|
+
zIndex: 60000
|
|
349
|
+
}
|
|
212
350
|
|
|
351
|
+
const toastBase = {
|
|
352
|
+
display: 'flex',
|
|
353
|
+
gap: 10,
|
|
354
|
+
alignItems: 'center',
|
|
355
|
+
padding: '10px 12px',
|
|
356
|
+
borderRadius: 10,
|
|
357
|
+
boxShadow: '0 8px 20px rgba(2,6,23,0.6)',
|
|
358
|
+
color: '#fff',
|
|
359
|
+
fontSize: 13,
|
|
360
|
+
minWidth: 120
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const toastError = { background: '#000', border: '1px solid rgba(255,255,255,0.06)' }
|
|
364
|
+
const toastSuccess = { background: '#000', border: '1px solid rgba(255,255,255,0.06)' }
|
|
365
|
+
const toastInfo = { background: '#000', border: '1px solid rgba(255,255,255,0.06)' }
|
|
366
|
+
|
|
367
|
+
const toastCloseBtn = {
|
|
368
|
+
marginLeft: 8,
|
|
369
|
+
background: 'transparent',
|
|
370
|
+
border: 'none',
|
|
371
|
+
color: 'rgba(255,255,255,0.7)',
|
|
372
|
+
cursor: 'pointer',
|
|
373
|
+
fontSize: 14,
|
|
374
|
+
lineHeight: 1
|
|
375
|
+
}
|