flowlink-auth 2.8.2 → 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/package.json +1 -1
- package/src/SignIn.jsx +206 -43
package/dist/SignIn.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useState } from "react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
|
+
import Link from "next/link";
|
|
3
4
|
import { useAuth } from "./provider.js";
|
|
4
5
|
function SignIn({ onSuccess } = {}) {
|
|
5
6
|
const {
|
|
@@ -12,12 +13,45 @@ function SignIn({ onSuccess } = {}) {
|
|
|
12
13
|
completeLogin,
|
|
13
14
|
fetchMe,
|
|
14
15
|
setUser
|
|
15
|
-
} = useAuth();
|
|
16
|
+
} = (typeof useAuth === "function" ? useAuth() : {}) || {};
|
|
16
17
|
const [email, setEmail] = useState("");
|
|
17
18
|
const [password, setPassword] = useState("");
|
|
18
19
|
const [loading, setLoading] = useState(false);
|
|
19
|
-
const [
|
|
20
|
-
const
|
|
20
|
+
const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false });
|
|
21
|
+
const redirectTimer = useRef(null);
|
|
22
|
+
const toastId = useRef(0);
|
|
23
|
+
const [toasts, setToasts] = useState([]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const meta = document.createElement("meta");
|
|
26
|
+
meta.name = "viewport";
|
|
27
|
+
meta.content = "width=device-width, initial-scale=1, maximum-scale=1";
|
|
28
|
+
document.head.appendChild(meta);
|
|
29
|
+
return () => {
|
|
30
|
+
if (redirectTimer.current) clearTimeout(redirectTimer.current);
|
|
31
|
+
const existing = document.querySelector('meta[name="viewport"]');
|
|
32
|
+
if (existing && existing.content === meta.content) document.head.removeChild(existing);
|
|
33
|
+
toasts.forEach((t) => {
|
|
34
|
+
if (t._timer) clearTimeout(t._timer);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
function showToast(type, message, ms = 5e3) {
|
|
39
|
+
const id = ++toastId.current;
|
|
40
|
+
const t = { id, type, message, _timer: null };
|
|
41
|
+
setToasts((prev) => [t, ...prev].slice(0, 6));
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
setToasts((prev) => prev.filter((x) => x.id !== id));
|
|
44
|
+
}, ms);
|
|
45
|
+
t._timer = timer;
|
|
46
|
+
}
|
|
47
|
+
function removeToast(id) {
|
|
48
|
+
setToasts((prev) => {
|
|
49
|
+
prev.forEach((t) => {
|
|
50
|
+
if (t.id === id && t._timer) clearTimeout(t._timer);
|
|
51
|
+
});
|
|
52
|
+
return prev.filter((x) => x.id !== id);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
21
55
|
if (loadingUser) return null;
|
|
22
56
|
if (user && redirect) {
|
|
23
57
|
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
@@ -26,15 +60,15 @@ function SignIn({ onSuccess } = {}) {
|
|
|
26
60
|
}
|
|
27
61
|
async function submit(e) {
|
|
28
62
|
e.preventDefault();
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
if (loading) return;
|
|
64
|
+
setLoading(true);
|
|
31
65
|
if (!email || !password) {
|
|
32
|
-
|
|
66
|
+
showToast("error", "Email and password are required");
|
|
67
|
+
setLoading(false);
|
|
33
68
|
return;
|
|
34
69
|
}
|
|
35
|
-
|
|
70
|
+
const endpoint = `${(baseUrl || "").replace(/\/+$/, "")}/api/sdk/login`;
|
|
36
71
|
try {
|
|
37
|
-
const endpoint = `${(baseUrl || "").replace(/\/+$/, "")}/api/sdk/login`;
|
|
38
72
|
const res = await fetch(endpoint, {
|
|
39
73
|
method: "POST",
|
|
40
74
|
credentials: "include",
|
|
@@ -46,9 +80,9 @@ function SignIn({ onSuccess } = {}) {
|
|
|
46
80
|
});
|
|
47
81
|
const ct = res.headers.get("content-type") || "";
|
|
48
82
|
let data = {};
|
|
49
|
-
if (ct.includes("application/json")) data = await res.json();
|
|
83
|
+
if (ct.includes("application/json")) data = await res.json().catch(() => ({}));
|
|
50
84
|
else {
|
|
51
|
-
const text = await res.text();
|
|
85
|
+
const text = await res.text().catch(() => "");
|
|
52
86
|
throw new Error(`Unexpected response (status ${res.status}): ${text.slice(0, 200)}`);
|
|
53
87
|
}
|
|
54
88
|
if (!res.ok) throw new Error(data.error || data.message || `Login failed (status ${res.status})`);
|
|
@@ -74,26 +108,27 @@ function SignIn({ onSuccess } = {}) {
|
|
|
74
108
|
} catch (_) {
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
|
-
|
|
111
|
+
showToast("success", "Signed in. Redirecting...");
|
|
78
112
|
if (redirect) {
|
|
79
|
-
setTimeout(() => {
|
|
113
|
+
redirectTimer.current = setTimeout(() => {
|
|
80
114
|
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
81
115
|
else if (typeof window !== "undefined") window.location.assign(redirect);
|
|
82
116
|
}, 250);
|
|
83
117
|
}
|
|
84
118
|
} catch (err) {
|
|
85
|
-
|
|
119
|
+
showToast("error", err?.message || "Network error");
|
|
120
|
+
console.error("Signin error:", err);
|
|
86
121
|
} finally {
|
|
87
122
|
setLoading(false);
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
125
|
async function startOAuthFlow(provider) {
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
if (loading || loadingOauth[provider]) return;
|
|
127
|
+
setLoadingOauth((prev) => ({ ...prev, [provider]: true }));
|
|
93
128
|
try {
|
|
94
129
|
const rid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
95
|
-
const callbackUrl = encodeURIComponent(`${window.location.origin}/signin`);
|
|
96
|
-
const sdkBase = typeof
|
|
130
|
+
const callbackUrl = encodeURIComponent(`${typeof window !== "undefined" ? window.location.origin : ""}/signin`);
|
|
131
|
+
const sdkBase = baseUrl || (typeof window !== "undefined" ? window.location.origin.replace(/\/+$/, "") : "");
|
|
97
132
|
const startUrl = `${sdkBase.replace(/\/+$/, "")}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`;
|
|
98
133
|
if (!publishableKey) {
|
|
99
134
|
throw new Error("Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.");
|
|
@@ -106,11 +141,11 @@ function SignIn({ onSuccess } = {}) {
|
|
|
106
141
|
const data = await res.json().catch(() => null);
|
|
107
142
|
if (!res.ok) throw new Error(data?.error || `OAuth start failed (${res.status})`);
|
|
108
143
|
if (!data?.oauthUrl) throw new Error("SDK start did not return oauthUrl");
|
|
109
|
-
window.location.href = data.oauthUrl;
|
|
144
|
+
if (typeof window !== "undefined") window.location.href = data.oauthUrl;
|
|
110
145
|
} catch (err) {
|
|
146
|
+
showToast("error", err?.message || "OAuth start failed");
|
|
111
147
|
console.error("OAuth start error:", err);
|
|
112
|
-
|
|
113
|
-
setLoading(false);
|
|
148
|
+
setLoadingOauth((prev) => ({ ...prev, [provider]: false }));
|
|
114
149
|
}
|
|
115
150
|
}
|
|
116
151
|
const handleGoogle = (e) => {
|
|
@@ -121,7 +156,26 @@ function SignIn({ onSuccess } = {}) {
|
|
|
121
156
|
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
122
157
|
startOAuthFlow("github");
|
|
123
158
|
};
|
|
124
|
-
return /* @__PURE__ */ React.createElement("div", { style: overlay }, /* @__PURE__ */ React.createElement("div", { style:
|
|
159
|
+
return /* @__PURE__ */ React.createElement("div", { style: overlay }, /* @__PURE__ */ React.createElement("div", { style: toastContainer, "aria-live": "polite", "aria-atomic": "true" }, toasts.map((t) => /* @__PURE__ */ React.createElement(
|
|
160
|
+
"div",
|
|
161
|
+
{
|
|
162
|
+
key: t.id,
|
|
163
|
+
role: "status",
|
|
164
|
+
style: {
|
|
165
|
+
...toastBase,
|
|
166
|
+
...t.type === "error" ? toastError : t.type === "success" ? toastSuccess : toastInfo
|
|
167
|
+
},
|
|
168
|
+
onMouseEnter: () => {
|
|
169
|
+
if (t._timer) clearTimeout(t._timer);
|
|
170
|
+
},
|
|
171
|
+
onMouseLeave: () => {
|
|
172
|
+
const timer = setTimeout(() => removeToast(t.id), 3e3);
|
|
173
|
+
setToasts((prev) => prev.map((x) => x.id === t.id ? { ...x, _timer: timer } : x));
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
/* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, t.message),
|
|
177
|
+
/* @__PURE__ */ React.createElement("button", { "aria-label": "Dismiss", onClick: () => removeToast(t.id), style: toastCloseBtn }, "\u2715")
|
|
178
|
+
))), /* @__PURE__ */ React.createElement("div", { style: modal }, /* @__PURE__ */ React.createElement("h2", { style: title }, "Sign in"), /* @__PURE__ */ React.createElement("p", { style: subtitle }, "Welcome back \u2014 enter your credentials."), /* @__PURE__ */ React.createElement("form", { onSubmit: submit, style: { width: "100%" } }, /* @__PURE__ */ React.createElement("label", { style: label }, "Email"), /* @__PURE__ */ React.createElement(
|
|
125
179
|
"input",
|
|
126
180
|
{
|
|
127
181
|
style: input,
|
|
@@ -139,19 +193,115 @@ function SignIn({ onSuccess } = {}) {
|
|
|
139
193
|
onChange: (e) => setPassword(e.target.value),
|
|
140
194
|
required: true
|
|
141
195
|
}
|
|
142
|
-
), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 12 } }, /* @__PURE__ */ React.createElement("button", { style: button, type: "submit", disabled: loading }, loading ? "Signing in\u2026" : "Sign in")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 8, marginTop: 16 } }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: handleGoogle, style: oauthButtonGoogle, disabled: loading }, "
|
|
196
|
+
), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 12 } }, /* @__PURE__ */ React.createElement("button", { style: button, type: "submit", disabled: loading }, loading ? "Signing in\u2026" : "Sign in")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 8, marginTop: 16 } }, /* @__PURE__ */ React.createElement("button", { type: "button", onClick: handleGoogle, style: oauthButtonGoogle, disabled: loading || loadingOauth.google }, /* @__PURE__ */ React.createElement("svg", { width: 18, style: { marginRight: 8 }, viewBox: "-3 0 262 262", xmlns: "http://www.w3.org/2000/svg", fill: "#000000", "aria-hidden": true }, /* @__PURE__ */ React.createElement("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" }), /* @__PURE__ */ React.createElement("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" }), /* @__PURE__ */ React.createElement("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" }), /* @__PURE__ */ React.createElement("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" })), /* @__PURE__ */ React.createElement("span", null, loadingOauth.google ? "Loading..." : "Google")), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: handleGithub, style: oauthButtonGithub, disabled: loading || loadingOauth.github }, /* @__PURE__ */ React.createElement("svg", { width: 18, style: { marginRight: 8 }, xmlns: "http://www.w3.org/2000/svg", fill: "white", viewBox: "0 0 20 20", "aria-hidden": true }, /* @__PURE__ */ React.createElement("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" })), /* @__PURE__ */ React.createElement("span", null, loadingOauth.github ? "Loading..." : "GitHub"))))));
|
|
143
197
|
}
|
|
144
|
-
const overlay = {
|
|
145
|
-
|
|
198
|
+
const overlay = {
|
|
199
|
+
position: "fixed",
|
|
200
|
+
inset: 0,
|
|
201
|
+
display: "block",
|
|
202
|
+
// allow page scroll
|
|
203
|
+
padding: 20,
|
|
204
|
+
background: "linear-gradient(180deg, rgba(2,6,23,0.22), rgba(2,6,23,0.32))",
|
|
205
|
+
backdropFilter: "blur(6px)",
|
|
206
|
+
overflowY: "auto",
|
|
207
|
+
WebkitOverflowScrolling: "touch",
|
|
208
|
+
minHeight: "100vh",
|
|
209
|
+
zIndex: 9999
|
|
210
|
+
};
|
|
211
|
+
const modal = {
|
|
212
|
+
width: "100%",
|
|
213
|
+
maxWidth: 460,
|
|
214
|
+
margin: "40px auto",
|
|
215
|
+
borderRadius: 14,
|
|
216
|
+
background: "linear-gradient(180deg, rgba(15,19,24,0.85), rgba(10,12,16,0.85))",
|
|
217
|
+
border: "1px solid rgba(99,102,106,0.12)",
|
|
218
|
+
boxShadow: "0 18px 48px rgba(2,6,23,0.55), inset 0 1px 0 rgba(255,255,255,0.02)",
|
|
219
|
+
padding: 22,
|
|
220
|
+
color: "#fff"
|
|
221
|
+
};
|
|
146
222
|
const title = { margin: 0, fontSize: 20, fontWeight: 600 };
|
|
147
223
|
const subtitle = { marginTop: 6, marginBottom: 14, color: "#cbd5e1", fontSize: 13 };
|
|
148
224
|
const label = { display: "block", color: "#cbd5e1", fontSize: 13, marginTop: 8 };
|
|
149
|
-
const input = {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
225
|
+
const input = {
|
|
226
|
+
width: "100%",
|
|
227
|
+
padding: "10px 12px",
|
|
228
|
+
marginTop: 6,
|
|
229
|
+
borderRadius: 10,
|
|
230
|
+
border: "1px solid rgba(148,163,184,0.10)",
|
|
231
|
+
background: "rgba(255,255,255,0.02)",
|
|
232
|
+
color: "#e6e6e6",
|
|
233
|
+
boxSizing: "border-box"
|
|
234
|
+
};
|
|
235
|
+
const button = {
|
|
236
|
+
width: "100%",
|
|
237
|
+
padding: "10px 12px",
|
|
238
|
+
borderRadius: 10,
|
|
239
|
+
background: "linear-gradient(90deg,#06b6d4,#2563eb)",
|
|
240
|
+
color: "#0b1220",
|
|
241
|
+
border: "none",
|
|
242
|
+
fontWeight: 700,
|
|
243
|
+
cursor: "pointer"
|
|
244
|
+
};
|
|
245
|
+
const oauthButtonGoogle = {
|
|
246
|
+
flex: 1,
|
|
247
|
+
padding: "10px 12px",
|
|
248
|
+
borderRadius: 10,
|
|
249
|
+
background: "linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))",
|
|
250
|
+
color: "#fff",
|
|
251
|
+
border: "1px solid rgba(148,163,184,0.08)",
|
|
252
|
+
cursor: "pointer",
|
|
253
|
+
display: "inline-flex",
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
justifyContent: "center",
|
|
256
|
+
gap: 8
|
|
257
|
+
};
|
|
258
|
+
const oauthButtonGithub = {
|
|
259
|
+
flex: 1,
|
|
260
|
+
padding: "10px 12px",
|
|
261
|
+
borderRadius: 10,
|
|
262
|
+
background: "linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00))",
|
|
263
|
+
color: "#fff",
|
|
264
|
+
border: "1px solid rgba(148,163,184,0.08)",
|
|
265
|
+
cursor: "pointer",
|
|
266
|
+
display: "inline-flex",
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
justifyContent: "center",
|
|
269
|
+
gap: 8
|
|
270
|
+
};
|
|
271
|
+
const toastContainer = {
|
|
272
|
+
position: "fixed",
|
|
273
|
+
top: 18,
|
|
274
|
+
right: 18,
|
|
275
|
+
width: 360,
|
|
276
|
+
maxWidth: "calc(100% - 36px)",
|
|
277
|
+
display: "flex",
|
|
278
|
+
flexDirection: "column",
|
|
279
|
+
gap: 10,
|
|
280
|
+
zIndex: 6e4
|
|
281
|
+
};
|
|
282
|
+
const toastBase = {
|
|
283
|
+
display: "flex",
|
|
284
|
+
gap: 10,
|
|
285
|
+
alignItems: "center",
|
|
286
|
+
padding: "10px 12px",
|
|
287
|
+
borderRadius: 10,
|
|
288
|
+
boxShadow: "0 8px 20px rgba(2,6,23,0.6)",
|
|
289
|
+
color: "#fff",
|
|
290
|
+
fontSize: 13,
|
|
291
|
+
minWidth: 120
|
|
292
|
+
};
|
|
293
|
+
const toastError = { background: "#000", border: "1px solid rgba(255,255,255,0.06)" };
|
|
294
|
+
const toastSuccess = { background: "#000", border: "1px solid rgba(255,255,255,0.06)" };
|
|
295
|
+
const toastInfo = { background: "#000", border: "1px solid rgba(255,255,255,0.06)" };
|
|
296
|
+
const toastCloseBtn = {
|
|
297
|
+
marginLeft: 8,
|
|
298
|
+
background: "transparent",
|
|
299
|
+
border: "none",
|
|
300
|
+
color: "rgba(255,255,255,0.7)",
|
|
301
|
+
cursor: "pointer",
|
|
302
|
+
fontSize: 14,
|
|
303
|
+
lineHeight: 1
|
|
304
|
+
};
|
|
155
305
|
export {
|
|
156
306
|
SignIn as default
|
|
157
307
|
};
|
package/package.json
CHANGED
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
|
+
}
|