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 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 [error, setError] = useState(null);
20
- const [message, setMessage] = useState(null);
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
- setError(null);
30
- setMessage(null);
63
+ if (loading) return;
64
+ setLoading(true);
31
65
  if (!email || !password) {
32
- setError("Email and password are required");
66
+ showToast("error", "Email and password are required");
67
+ setLoading(false);
33
68
  return;
34
69
  }
35
- setLoading(true);
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
- setMessage("Signed in. Redirecting...");
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
- setError(err.message || "Network error");
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
- setError(null);
92
- setLoading(true);
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 process !== "undefined" && process.env && process.env.NEXT_PUBLIC_FLOWLINK_BASE_URL || baseUrl || "http://localhost:3001";
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
- setError(err?.message || "OAuth start failed");
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: 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(
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 }, "Continue with Google"), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: handleGithub, style: oauthButtonGithub, disabled: loading }, "Continue with GitHub")), error && /* @__PURE__ */ React.createElement("div", { style: errorBox }, error), message && /* @__PURE__ */ React.createElement("div", { style: successBox }, message))));
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 = { position: "fixed", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.45)", zIndex: 9999, padding: 20 };
145
- const modal = { width: "100%", maxWidth: 420, background: "#0f1724", color: "#fff", borderRadius: 12, padding: 22, boxShadow: "0 10px 30px rgba(2,6,23,0.6)", border: "1px solid rgba(255,255,255,0.04)" };
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 = { width: "100%", padding: "10px 12px", marginTop: 6, borderRadius: 8, border: "1px solid rgba(255,255,255,0.06)", background: "#0b1220", color: "#fff", boxSizing: "border-box" };
150
- const button = { width: "100%", padding: "10px 12px", borderRadius: 8, background: "linear-gradient(90deg,#06b6d4,#2563eb)", color: "#0b1220", border: "none", fontWeight: 700, cursor: "pointer" };
151
- const oauthButtonGoogle = { flex: 1, padding: "10px 12px", borderRadius: 8, background: "#db4437", color: "#fff", border: "none", cursor: "pointer" };
152
- const oauthButtonGithub = { flex: 1, padding: "10px 12px", borderRadius: 8, background: "#24292f", color: "#fff", border: "none", cursor: "pointer" };
153
- const errorBox = { marginTop: 10, color: "#ffb4b4", fontSize: 13 };
154
- const successBox = { marginTop: 10, color: "#bef264", fontSize: 13 };
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
  };