flowlink-auth 2.8.4 → 2.8.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.
Files changed (3) hide show
  1. package/dist/SignIn.js +174 -150
  2. package/package.json +1 -1
  3. package/src/SignIn.jsx +257 -208
package/dist/SignIn.js CHANGED
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
  import React, { useEffect, useRef, useState } from "react";
3
+ import Image from "next/image";
3
4
  import Link from "next/link";
5
+ import { ToastContainer, toast } from "react-toastify";
6
+ import "react-toastify/dist/ReactToastify.css";
4
7
  import { useAuth } from "./provider.js";
5
- function SignIn({ onSuccess } = {}) {
8
+ function SignIn({ agency = { name: "chest", logo: "/logo.png" }, onSuccess } = {}) {
6
9
  const {
7
10
  publishableKey,
8
11
  baseUrl,
@@ -14,13 +17,10 @@ function SignIn({ onSuccess } = {}) {
14
17
  fetchMe,
15
18
  setUser
16
19
  } = (typeof useAuth === "function" ? useAuth() : {}) || {};
17
- const [email, setEmail] = useState("");
18
- const [password, setPassword] = useState("");
20
+ const [form2, setForm] = useState({ email: "", password: "" });
19
21
  const [loading, setLoading] = useState(false);
20
22
  const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false });
21
23
  const redirectTimer = useRef(null);
22
- const toastId = useRef(0);
23
- const [toasts, setToasts] = useState([]);
24
24
  useEffect(() => {
25
25
  const meta = document.createElement("meta");
26
26
  meta.name = "viewport";
@@ -30,40 +30,23 @@ function SignIn({ onSuccess } = {}) {
30
30
  if (redirectTimer.current) clearTimeout(redirectTimer.current);
31
31
  const existing = document.querySelector('meta[name="viewport"]');
32
32
  if (existing && existing.content === meta.content) document.head.removeChild(existing);
33
- toasts.forEach((t) => {
34
- if (t._timer) clearTimeout(t._timer);
35
- });
36
33
  };
37
34
  }, []);
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
- }
55
35
  if (loadingUser) return null;
56
36
  if (user && redirect) {
57
37
  if (typeof redirectTo === "function") redirectTo(redirect);
58
38
  else if (typeof window !== "undefined") window.location.assign(redirect);
59
39
  return null;
60
40
  }
41
+ function handleChange(e) {
42
+ setForm((prev) => ({ ...prev, [e.target.id]: e.target.value }));
43
+ }
61
44
  async function submit(e) {
62
45
  e.preventDefault();
63
46
  if (loading) return;
64
47
  setLoading(true);
65
- if (!email || !password) {
66
- showToast("error", "Email and password are required");
48
+ if (!form2.email || !form2.password) {
49
+ toast.error("Email and password are required", { toastId: "missing-creds", toastStyle: { background: "#000", color: "#fff" } });
67
50
  setLoading(false);
68
51
  return;
69
52
  }
@@ -76,7 +59,7 @@ function SignIn({ onSuccess } = {}) {
76
59
  "Content-Type": "application/json",
77
60
  "x-publishable-key": publishableKey || ""
78
61
  },
79
- body: JSON.stringify({ email, password })
62
+ body: JSON.stringify({ email: form2.email, password: form2.password })
80
63
  });
81
64
  const ct = res.headers.get("content-type") || "";
82
65
  let data = {};
@@ -108,7 +91,7 @@ function SignIn({ onSuccess } = {}) {
108
91
  } catch (_) {
109
92
  }
110
93
  }
111
- showToast("success", "Signed in. Redirecting...");
94
+ toast.success("Signed in. Redirecting...", { toastStyle: { background: "#000", color: "#fff" } });
112
95
  if (redirect) {
113
96
  redirectTimer.current = setTimeout(() => {
114
97
  if (typeof redirectTo === "function") redirectTo(redirect);
@@ -116,7 +99,7 @@ function SignIn({ onSuccess } = {}) {
116
99
  }, 250);
117
100
  }
118
101
  } catch (err) {
119
- showToast("error", err?.message || "Network error");
102
+ toast.error(err?.message || "Network error", { toastStyle: { background: "#000", color: "#fff" } });
120
103
  console.error("Signin error:", err);
121
104
  } finally {
122
105
  setLoading(false);
@@ -130,9 +113,7 @@ function SignIn({ onSuccess } = {}) {
130
113
  const callbackUrl = encodeURIComponent(`${typeof window !== "undefined" ? window.location.origin : ""}/signin`);
131
114
  const sdkBase = baseUrl || (typeof window !== "undefined" ? window.location.origin.replace(/\/+$/, "") : "");
132
115
  const startUrl = `${sdkBase.replace(/\/+$/, "")}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`;
133
- if (!publishableKey) {
134
- throw new Error("Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.");
135
- }
116
+ if (!publishableKey) throw new Error("Missing publishable key (client side).");
136
117
  const res = await fetch(startUrl, {
137
118
  method: "GET",
138
119
  headers: { "x-publishable-key": publishableKey },
@@ -143,165 +124,208 @@ function SignIn({ onSuccess } = {}) {
143
124
  if (!data?.oauthUrl) throw new Error("SDK start did not return oauthUrl");
144
125
  if (typeof window !== "undefined") window.location.href = data.oauthUrl;
145
126
  } catch (err) {
146
- showToast("error", err?.message || "OAuth start failed");
127
+ toast.error(err?.message || "OAuth start failed", { toastStyle: { background: "#000", color: "#fff" } });
147
128
  console.error("OAuth start error:", err);
148
129
  setLoadingOauth((prev) => ({ ...prev, [provider]: false }));
149
130
  }
150
131
  }
151
132
  const handleGoogle = (e) => {
152
- if (e && typeof e.preventDefault === "function") e.preventDefault();
133
+ if (e?.preventDefault) e.preventDefault();
153
134
  startOAuthFlow("google");
154
135
  };
155
136
  const handleGithub = (e) => {
156
- if (e && typeof e.preventDefault === "function") e.preventDefault();
137
+ if (e?.preventDefault) e.preventDefault();
157
138
  startOAuthFlow("github");
158
139
  };
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",
140
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
141
+ ToastContainer,
161
142
  {
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
- }
143
+ position: "top-right",
144
+ newestOnTop: true,
145
+ autoClose: 3e3,
146
+ hideProgressBar: false,
147
+ closeOnClick: true,
148
+ pauseOnHover: true,
149
+ draggable: true,
150
+ theme: "dark"
151
+ }
152
+ ), /* @__PURE__ */ React.createElement("div", { style: page }, /* @__PURE__ */ React.createElement("div", { style: cardWrap }, /* @__PURE__ */ React.createElement("div", { style: card }, /* @__PURE__ */ React.createElement("div", { style: cardInner }, /* @__PURE__ */ React.createElement("div", { style: brand }, /* @__PURE__ */ React.createElement("div", { style: brandRow }, /* @__PURE__ */ React.createElement("div", { style: logoWrap }, /* @__PURE__ */ React.createElement(Image, { src: agency.logo, width: 30, height: 30, alt: agency.name, style: logoImgStyle })), /* @__PURE__ */ React.createElement("h1", { style: brandTitle }, agency.name)), /* @__PURE__ */ React.createElement("div", { style: brandText }, /* @__PURE__ */ React.createElement("div", { style: brandLead }, "Sign in to ", agency.name), /* @__PURE__ */ React.createElement("div", { style: brandSub }, "Welcome back! Let's get you signed in."))), /* @__PURE__ */ React.createElement("div", { style: oauthRow }, /* @__PURE__ */ React.createElement(
153
+ "button",
154
+ {
155
+ onClick: handleGoogle,
156
+ type: "button",
157
+ style: { ...oauthButton, ...oauthGoogle },
158
+ disabled: loading || loadingOauth.google,
159
+ "aria-disabled": loading || loadingOauth.google
175
160
  },
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(
161
+ /* @__PURE__ */ React.createElement("svg", { width: 18, style: { marginRight: 10 }, 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" })),
162
+ /* @__PURE__ */ React.createElement("span", null, loadingOauth.google ? "Loading..." : "Google")
163
+ ), /* @__PURE__ */ React.createElement(
164
+ "button",
165
+ {
166
+ onClick: handleGithub,
167
+ type: "button",
168
+ style: { ...oauthButton, ...oauthGithub },
169
+ disabled: loading || loadingOauth.github,
170
+ "aria-disabled": loading || loadingOauth.github
171
+ },
172
+ /* @__PURE__ */ React.createElement("svg", { width: 18, style: { marginRight: 10 }, 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" })),
173
+ /* @__PURE__ */ React.createElement("span", null, loadingOauth.github ? "Loading..." : "Github")
174
+ )), /* @__PURE__ */ React.createElement("div", { style: dividerRow }, /* @__PURE__ */ React.createElement("div", { style: line }), /* @__PURE__ */ React.createElement("div", { style: orText }, "or"), /* @__PURE__ */ React.createElement("div", { style: line })), /* @__PURE__ */ React.createElement("form", { onSubmit: submit, style: form2 }, /* @__PURE__ */ React.createElement("label", { style: label }, /* @__PURE__ */ React.createElement("span", { style: labelSmall }, "Email address"), /* @__PURE__ */ React.createElement(
179
175
  "input",
180
176
  {
181
- style: input,
182
- value: email,
183
- onChange: (e) => setEmail(e.target.value),
177
+ id: "email",
184
178
  type: "email",
185
- required: true
179
+ onChange: handleChange,
180
+ value: form2.email,
181
+ required: true,
182
+ placeholder: "you@example.com",
183
+ style: input
186
184
  }
187
- ), /* @__PURE__ */ React.createElement("label", { style: label }, "Password"), /* @__PURE__ */ React.createElement(
185
+ )), /* @__PURE__ */ React.createElement("label", { style: label }, /* @__PURE__ */ React.createElement("span", { style: labelSmall }, "Password"), /* @__PURE__ */ React.createElement(
188
186
  "input",
189
187
  {
190
- style: input,
188
+ id: "password",
191
189
  type: "password",
192
- value: password,
193
- onChange: (e) => setPassword(e.target.value),
194
- required: true
190
+ onChange: handleChange,
191
+ value: form2.password,
192
+ required: true,
193
+ placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
194
+ style: input
195
195
  }
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"))))));
196
+ )), /* @__PURE__ */ React.createElement(
197
+ "button",
198
+ {
199
+ type: "submit",
200
+ style: submitBtn,
201
+ "aria-disabled": loading,
202
+ disabled: loading
203
+ },
204
+ loading ? "Signing in..." : "Sign In"
205
+ ))), /* @__PURE__ */ React.createElement("div", { style: cardFooter }, /* @__PURE__ */ React.createElement("div", { style: smallText }, /* @__PURE__ */ React.createElement("span", { style: muted }, "Don't have an account? "), /* @__PURE__ */ React.createElement(Link, { href: "/signup", style: link }, "Create one")), /* @__PURE__ */ React.createElement("div", { style: thinDivider }), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center", marginTop: 8 } }, /* @__PURE__ */ React.createElement("div", { style: securedText }, "Secured by auth")))))));
197
206
  }
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",
207
+ const page = {
208
208
  minHeight: "100vh",
209
- zIndex: 9999
209
+ display: "flex",
210
+ alignItems: "center",
211
+ justifyContent: "center",
212
+ padding: "24px",
213
+ background: "linear-gradient(to bottom, #0f1724, #0b1220 40%, #02040a 100%)",
214
+ color: "#fef3c7"
215
+ };
216
+ const cardWrap = {
217
+ width: "100%",
218
+ maxWidth: 420
210
219
  };
211
- const modal = {
220
+ const card = {
212
221
  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,
222
+ borderRadius: 20,
223
+ background: "linear-gradient(180deg, rgba(15,17,20,0.88), rgba(6,8,12,0.92))",
224
+ border: "1px solid rgba(148,163,184,0.06)",
225
+ backdropFilter: "blur(8px)",
226
+ boxShadow: "0 18px 50px rgba(2,6,23,0.6)",
227
+ overflow: "hidden",
228
+ display: "flex",
229
+ flexDirection: "column"
230
+ };
231
+ const cardInner = {
232
+ padding: "28px",
233
+ display: "flex",
234
+ flexDirection: "column",
235
+ gap: 14,
236
+ background: "linear-gradient(180deg, rgba(12,14,18,0.02), rgba(0,0,0,0.02))",
237
+ borderRadius: "20px"
238
+ };
239
+ const brand = {
240
+ display: "flex",
241
+ flexDirection: "column",
242
+ gap: 8,
243
+ alignItems: "center",
244
+ textAlign: "center"
245
+ };
246
+ const brandRow = {
247
+ display: "flex",
248
+ alignItems: "center",
249
+ gap: 10,
250
+ justifyContent: "center"
251
+ };
252
+ const logoWrap = {
253
+ width: 30,
254
+ height: 30,
255
+ borderRadius: 999,
256
+ overflow: "hidden",
257
+ display: "flex",
258
+ alignItems: "center",
259
+ justifyContent: "center",
260
+ background: "linear-gradient(135deg,#2b313a,#0f1724)"
261
+ };
262
+ const logoImgStyle = { borderRadius: 999, objectFit: "cover" };
263
+ const brandTitle = { margin: 0, fontSize: 20, fontWeight: 500, color: "#fff" };
264
+ const brandLead = { fontSize: 15, color: "#f8fafc" };
265
+ const brandSub = { fontSize: 13, color: "rgba(255,255,255,0.7)", marginTop: 2 };
266
+ const brandText = { display: "flex", flexDirection: "column", gap: 2, alignItems: "center" };
267
+ const oauthRow = { display: "flex", gap: 10, marginTop: 10 };
268
+ const oauthButton = {
269
+ flex: 1,
270
+ display: "inline-flex",
271
+ alignItems: "center",
272
+ justifyContent: "center",
273
+ padding: "10px 12px",
274
+ borderRadius: 12,
275
+ border: "1px solid rgba(148,163,184,0.06)",
276
+ fontSize: 14,
277
+ cursor: "pointer",
278
+ userSelect: "none",
279
+ gap: 8,
280
+ minHeight: 40,
281
+ background: "transparent",
220
282
  color: "#fff"
221
283
  };
222
- const title = { margin: 0, fontSize: 20, fontWeight: 600 };
223
- const subtitle = { marginTop: 6, marginBottom: 14, color: "#cbd5e1", fontSize: 13 };
224
- const label = { display: "block", color: "#cbd5e1", fontSize: 13, marginTop: 8 };
284
+ const oauthGoogle = { background: "rgba(255,255,255,0.02)" };
285
+ const oauthGithub = { background: "rgba(255,255,255,0.01)" };
286
+ const dividerRow = { display: "flex", alignItems: "center", gap: 12, marginTop: 14 };
287
+ const line = { flex: 1, height: 1, background: "rgba(148,163,184,0.04)" };
288
+ const orText = { fontSize: 13, color: "rgba(255,255,255,0.65)", padding: "0 8px" };
289
+ const form = { display: "flex", flexDirection: "column", gap: 12, width: "100%", marginTop: 4 };
290
+ const label = { display: "flex", flexDirection: "column", gap: 6 };
291
+ const labelSmall = { fontSize: 13, color: "rgba(255,255,255,0.75)" };
225
292
  const input = {
226
293
  width: "100%",
227
294
  padding: "10px 12px",
228
- marginTop: 6,
229
- borderRadius: 10,
230
- border: "1px solid rgba(148,163,184,0.10)",
295
+ borderRadius: 12,
231
296
  background: "rgba(255,255,255,0.02)",
232
- color: "#e6e6e6",
297
+ color: "#fef3c7",
298
+ border: "1px solid rgba(148,163,184,0.06)",
299
+ fontSize: 14,
300
+ outline: "none",
233
301
  boxSizing: "border-box"
234
302
  };
235
- const button = {
303
+ const submitBtn = {
304
+ marginTop: 6,
236
305
  width: "100%",
237
306
  padding: "10px 12px",
238
- borderRadius: 10,
239
- background: "linear-gradient(90deg,#06b6d4,#2563eb)",
240
- color: "#0b1220",
307
+ borderRadius: 12,
308
+ background: "linear-gradient(180deg,#2563eb,#60a5fa)",
241
309
  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
310
  color: "#fff",
251
- border: "1px solid rgba(148,163,184,0.08)",
311
+ fontWeight: 600,
252
312
  cursor: "pointer",
313
+ fontSize: 15,
253
314
  display: "inline-flex",
254
315
  alignItems: "center",
255
316
  justifyContent: "center",
256
- gap: 8
317
+ minHeight: 44
257
318
  };
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
319
+ const cardFooter = {
320
+ padding: "18px",
321
+ borderTop: "1px solid rgba(148,163,184,0.03)",
322
+ background: "transparent"
304
323
  };
324
+ const smallText = { textAlign: "center", fontSize: 13 };
325
+ const muted = { color: "rgba(255,255,255,0.75)" };
326
+ const link = { color: "#60a5fa", textDecoration: "none", fontWeight: 600 };
327
+ const thinDivider = { height: 1, background: "rgba(148,163,184,0.04)", margin: "12px 0" };
328
+ const securedText = { color: "rgba(255,255,255,0.9)", fontWeight: 600 };
305
329
  export {
306
330
  SignIn as default
307
331
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowlink-auth",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "description": "Custom auth library",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.d.ts",
package/src/SignIn.jsx CHANGED
@@ -1,9 +1,13 @@
1
+ // src/signin.jsx
1
2
  'use client'
2
3
  import React, { useEffect, useRef, useState } from 'react'
4
+ import Image from 'next/image'
3
5
  import Link from 'next/link'
6
+ import { ToastContainer, toast } from 'react-toastify'
7
+ import 'react-toastify/dist/ReactToastify.css'
4
8
  import { useAuth } from './provider.js'
5
9
 
6
- export default function SignIn({ onSuccess } = {}) {
10
+ export default function SignIn({ agency = { name: 'chest', logo: '/logo.png' }, onSuccess } = {}) {
7
11
  const {
8
12
  publishableKey,
9
13
  baseUrl,
@@ -16,50 +20,24 @@ export default function SignIn({ onSuccess } = {}) {
16
20
  setUser
17
21
  } = (typeof useAuth === 'function' ? useAuth() : {}) || {}
18
22
 
19
- const [email, setEmail] = useState('')
20
- const [password, setPassword] = useState('')
23
+ const [form, setForm] = useState({ email: '', password: '' })
21
24
  const [loading, setLoading] = useState(false)
22
25
  const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false })
23
-
24
26
  const redirectTimer = useRef(null)
25
- const toastId = useRef(0)
26
- const [toasts, setToasts] = useState([])
27
27
 
28
+ // soft-disable pinch-zoom on mobile while mounted (SDK-safe)
28
29
  useEffect(() => {
29
- // soft-disable pinch-zoom on mobile while mounted
30
30
  const meta = document.createElement('meta')
31
31
  meta.name = 'viewport'
32
32
  meta.content = 'width=device-width, initial-scale=1, maximum-scale=1'
33
33
  document.head.appendChild(meta)
34
-
35
34
  return () => {
36
35
  if (redirectTimer.current) clearTimeout(redirectTimer.current)
37
36
  const existing = document.querySelector('meta[name="viewport"]')
38
37
  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
38
  }
42
- // eslint-disable-next-line react-hooks/exhaustive-deps
43
39
  }, [])
44
40
 
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
- }
62
-
63
41
  if (loadingUser) return null
64
42
 
65
43
  if (user && redirect) {
@@ -68,13 +46,17 @@ export default function SignIn({ onSuccess } = {}) {
68
46
  return null
69
47
  }
70
48
 
49
+ function handleChange(e) {
50
+ setForm(prev => ({ ...prev, [e.target.id]: e.target.value }))
51
+ }
52
+
71
53
  async function submit(e) {
72
54
  e.preventDefault()
73
55
  if (loading) return
74
56
  setLoading(true)
75
57
 
76
- if (!email || !password) {
77
- showToast('error', 'Email and password are required')
58
+ if (!form.email || !form.password) {
59
+ toast.error('Email and password are required', { toastId: 'missing-creds', toastStyle: { background: '#000', color: '#fff' } })
78
60
  setLoading(false)
79
61
  return
80
62
  }
@@ -89,7 +71,7 @@ export default function SignIn({ onSuccess } = {}) {
89
71
  'Content-Type': 'application/json',
90
72
  'x-publishable-key': publishableKey || ''
91
73
  },
92
- body: JSON.stringify({ email, password })
74
+ body: JSON.stringify({ email: form.email, password: form.password })
93
75
  })
94
76
 
95
77
  const ct = res.headers.get('content-type') || ''
@@ -121,7 +103,8 @@ export default function SignIn({ onSuccess } = {}) {
121
103
  try { onSuccess(data) } catch (_) {}
122
104
  }
123
105
 
124
- showToast('success', 'Signed in. Redirecting...')
106
+ toast.success('Signed in. Redirecting...', { toastStyle: { background: '#000', color: '#fff' } })
107
+
125
108
  if (redirect) {
126
109
  redirectTimer.current = setTimeout(() => {
127
110
  if (typeof redirectTo === 'function') redirectTo(redirect)
@@ -129,7 +112,7 @@ export default function SignIn({ onSuccess } = {}) {
129
112
  }, 250)
130
113
  }
131
114
  } catch (err) {
132
- showToast('error', err?.message || 'Network error')
115
+ toast.error(err?.message || 'Network error', { toastStyle: { background: '#000', color: '#fff' } })
133
116
  console.error('Signin error:', err)
134
117
  } finally {
135
118
  setLoading(false)
@@ -137,9 +120,9 @@ export default function SignIn({ onSuccess } = {}) {
137
120
  }
138
121
 
139
122
  async function startOAuthFlow(provider) {
140
- // prevent double start
141
123
  if (loading || loadingOauth[provider]) return
142
124
  setLoadingOauth(prev => ({ ...prev, [provider]: true }))
125
+
143
126
  try {
144
127
  const rid =
145
128
  (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
@@ -150,9 +133,7 @@ export default function SignIn({ onSuccess } = {}) {
150
133
  const sdkBase = baseUrl || (typeof window !== 'undefined' ? window.location.origin.replace(/\/+$/, '') : '')
151
134
  const startUrl = `${sdkBase.replace(/\/+$/, '')}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
152
135
 
153
- if (!publishableKey) {
154
- throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
155
- }
136
+ if (!publishableKey) throw new Error('Missing publishable key (client side).')
156
137
 
157
138
  const res = await fetch(startUrl, {
158
139
  method: 'GET',
@@ -164,212 +145,280 @@ export default function SignIn({ onSuccess } = {}) {
164
145
  if (!res.ok) throw new Error(data?.error || `OAuth start failed (${res.status})`)
165
146
  if (!data?.oauthUrl) throw new Error('SDK start did not return oauthUrl')
166
147
 
148
+ // navigate to provider (unloads page, so no need to unset loading)
167
149
  if (typeof window !== 'undefined') window.location.href = data.oauthUrl
168
150
  } catch (err) {
169
- showToast('error', err?.message || 'OAuth start failed')
151
+ toast.error(err?.message || 'OAuth start failed', { toastStyle: { background: '#000', color: '#fff' } })
170
152
  console.error('OAuth start error:', err)
171
153
  setLoadingOauth(prev => ({ ...prev, [provider]: false }))
172
154
  }
173
155
  }
174
156
 
175
- const handleGoogle = (e) => {
176
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
177
- startOAuthFlow('google')
178
- }
179
-
180
- const handleGithub = (e) => {
181
- if (e && typeof e.preventDefault === 'function') e.preventDefault()
182
- startOAuthFlow('github')
183
- }
157
+ const handleGoogle = (e) => { if (e?.preventDefault) e.preventDefault(); startOAuthFlow('google') }
158
+ const handleGithub = (e) => { if (e?.preventDefault) e.preventDefault(); startOAuthFlow('github') }
184
159
 
185
160
  return (
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
-
209
- <div style={modal}>
210
- <h2 style={title}>Sign in</h2>
211
- <p style={subtitle}>Welcome back enter your credentials.</p>
212
-
213
- <form onSubmit={submit} style={{ width: '100%' }}>
214
- <label style={label}>Email</label>
215
- <input
216
- style={input}
217
- value={email}
218
- onChange={e => setEmail(e.target.value)}
219
- type="email"
220
- required
221
- />
222
-
223
- <label style={label}>Password</label>
224
- <input
225
- style={input}
226
- type="password"
227
- value={password}
228
- onChange={e => setPassword(e.target.value)}
229
- required
230
- />
231
-
232
- <div style={{ marginTop: 12 }}>
233
- <button style={button} type="submit" disabled={loading}>
234
- {loading ? 'Signing in…' : 'Sign in'}
235
- </button>
236
- </div>
237
-
238
- <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
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>
247
- </button>
248
-
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>
254
- </button>
161
+ <>
162
+ <ToastContainer
163
+ position="top-right"
164
+ newestOnTop
165
+ autoClose={3000}
166
+ hideProgressBar={false}
167
+ closeOnClick
168
+ pauseOnHover
169
+ draggable
170
+ theme="dark"
171
+ />
172
+
173
+ <div style={page}>
174
+ <div style={cardWrap}>
175
+ <div style={card}>
176
+ <div style={cardInner}>
177
+ <div style={brand}>
178
+ <div style={brandRow}>
179
+ <div style={logoWrap}>
180
+ <Image src={agency.logo} width={30} height={30} alt={agency.name} style={logoImgStyle} />
181
+ </div>
182
+ <h1 style={brandTitle}>{agency.name}</h1>
183
+ </div>
184
+ <div style={brandText}>
185
+ <div style={brandLead}>Sign in to {agency.name}</div>
186
+ <div style={brandSub}>Welcome back! Let's get you signed in.</div>
187
+ </div>
188
+ </div>
189
+
190
+ <div style={oauthRow}>
191
+ <button
192
+ onClick={handleGoogle}
193
+ type="button"
194
+ style={{ ...oauthButton, ...oauthGoogle }}
195
+ disabled={loading || loadingOauth.google}
196
+ aria-disabled={loading || loadingOauth.google}
197
+ >
198
+ <svg width={18} style={{ marginRight: 10 }} viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" fill="#000000" aria-hidden>
199
+ <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>
200
+ <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>
201
+ <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>
202
+ <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>
203
+ </svg>
204
+ <span>{loadingOauth.google ? 'Loading...' : 'Google'}</span>
205
+ </button>
206
+
207
+ <button
208
+ onClick={handleGithub}
209
+ type="button"
210
+ style={{ ...oauthButton, ...oauthGithub }}
211
+ disabled={loading || loadingOauth.github}
212
+ aria-disabled={loading || loadingOauth.github}
213
+ >
214
+ <svg width={18} style={{ marginRight: 10 }} xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 20 20" aria-hidden>
215
+ <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" />
216
+ </svg>
217
+ <span>{loadingOauth.github ? 'Loading...' : 'Github'}</span>
218
+ </button>
219
+ </div>
220
+
221
+ <div style={dividerRow}>
222
+ <div style={line} />
223
+ <div style={orText}>or</div>
224
+ <div style={line} />
225
+ </div>
226
+
227
+ <form onSubmit={submit} style={form}>
228
+ <label style={label}>
229
+ <span style={labelSmall}>Email address</span>
230
+ <input
231
+ id="email"
232
+ type="email"
233
+ onChange={handleChange}
234
+ value={form.email}
235
+ required
236
+ placeholder="you@example.com"
237
+ style={input}
238
+ />
239
+ </label>
240
+
241
+ <label style={label}>
242
+ <span style={labelSmall}>Password</span>
243
+ <input
244
+ id="password"
245
+ type="password"
246
+ onChange={handleChange}
247
+ value={form.password}
248
+ required
249
+ placeholder="••••••••"
250
+ style={input}
251
+ />
252
+ </label>
253
+
254
+ <button
255
+ type="submit"
256
+ style={submitBtn}
257
+ aria-disabled={loading}
258
+ disabled={loading}
259
+ >
260
+ {loading ? 'Signing in...' : 'Sign In'}
261
+ </button>
262
+ </form>
263
+ </div>
264
+
265
+ <div style={cardFooter}>
266
+ <div style={smallText}>
267
+ <span style={muted}>Don't have an account? </span>
268
+ <Link href="/signup" style={link}>Create one</Link>
269
+ </div>
270
+
271
+ <div style={thinDivider} />
272
+
273
+ <div style={{ textAlign: 'center', marginTop: 8 }}>
274
+ <div style={securedText}>Secured by auth</div>
275
+ </div>
276
+ </div>
255
277
  </div>
256
- </form>
278
+ </div>
257
279
  </div>
258
- </div>
280
+ </>
259
281
  )
260
282
  }
261
283
 
262
- /* styles */
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',
284
+ /* ---------- styles (JS objects) to match the provided UI exactly ---------- */
285
+
286
+ const page = {
272
287
  minHeight: '100vh',
273
- zIndex: 9999
288
+ display: 'flex',
289
+ alignItems: 'center',
290
+ justifyContent: 'center',
291
+ padding: '24px',
292
+ background: 'linear-gradient(to bottom, #0f1724, #0b1220 40%, #02040a 100%)',
293
+ color: '#fef3c7'
274
294
  }
275
295
 
276
- const modal = {
296
+ const cardWrap = {
277
297
  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'
298
+ maxWidth: 420
286
299
  }
287
300
 
288
- const title = { margin: 0, fontSize: 20, fontWeight: 600 }
289
- const subtitle = { marginTop: 6, marginBottom: 14, color: '#cbd5e1', fontSize: 13 }
290
- const label = { display: 'block', color: '#cbd5e1', fontSize: 13, marginTop: 8 }
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 = {
301
+ const card = {
302
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'
303
+ borderRadius: 20,
304
+ background: 'linear-gradient(180deg, rgba(15,17,20,0.88), rgba(6,8,12,0.92))',
305
+ border: '1px solid rgba(148,163,184,0.06)',
306
+ backdropFilter: 'blur(8px)',
307
+ boxShadow: '0 18px 50px rgba(2,6,23,0.6)',
308
+ overflow: 'hidden',
309
+ display: 'flex',
310
+ flexDirection: 'column'
310
311
  }
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
312
+
313
+ const cardInner = {
314
+ padding: '28px',
315
+ display: 'flex',
316
+ flexDirection: 'column',
317
+ gap: 14,
318
+ background: 'linear-gradient(180deg, rgba(12,14,18,0.02), rgba(0,0,0,0.02))',
319
+ borderRadius: '20px'
323
320
  }
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',
321
+
322
+ const brand = {
323
+ display: 'flex',
324
+ flexDirection: 'column',
325
+ gap: 8,
333
326
  alignItems: 'center',
334
- justifyContent: 'center',
335
- gap: 8
327
+ textAlign: 'center'
336
328
  }
337
329
 
338
- /* Toast styles (black) */
339
- const toastContainer = {
340
- position: 'fixed',
341
- top: 18,
342
- right: 18,
343
- width: 360,
344
- maxWidth: 'calc(100% - 36px)',
330
+ const brandRow = {
345
331
  display: 'flex',
346
- flexDirection: 'column',
332
+ alignItems: 'center',
347
333
  gap: 10,
348
- zIndex: 60000
334
+ justifyContent: 'center'
349
335
  }
350
336
 
351
- const toastBase = {
337
+ const logoWrap = {
338
+ width: 30,
339
+ height: 30,
340
+ borderRadius: 999,
341
+ overflow: 'hidden',
352
342
  display: 'flex',
353
- gap: 10,
354
343
  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
344
+ justifyContent: 'center',
345
+ background: 'linear-gradient(135deg,#2b313a,#0f1724)'
361
346
  }
362
347
 
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)' }
348
+ const logoImgStyle = { borderRadius: 999, objectFit: 'cover' }
366
349
 
367
- const toastCloseBtn = {
368
- marginLeft: 8,
350
+ const brandTitle = { margin: 0, fontSize: 20, fontWeight: 500, color: '#fff' }
351
+ const brandLead = { fontSize: 15, color: '#f8fafc' }
352
+ const brandSub = { fontSize: 13, color: 'rgba(255,255,255,0.7)', marginTop: 2 }
353
+ const brandText = { display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }
354
+
355
+ const oauthRow = { display: 'flex', gap: 10, marginTop: 10 }
356
+
357
+ const oauthButton = {
358
+ flex: 1,
359
+ display: 'inline-flex',
360
+ alignItems: 'center',
361
+ justifyContent: 'center',
362
+ padding: '10px 12px',
363
+ borderRadius: 12,
364
+ border: '1px solid rgba(148,163,184,0.06)',
365
+ fontSize: 14,
366
+ cursor: 'pointer',
367
+ userSelect: 'none',
368
+ gap: 8,
369
+ minHeight: 40,
369
370
  background: 'transparent',
371
+ color: '#fff'
372
+ }
373
+
374
+ const oauthGoogle = { background: 'rgba(255,255,255,0.02)' }
375
+ const oauthGithub = { background: 'rgba(255,255,255,0.01)' }
376
+
377
+ const dividerRow = { display: 'flex', alignItems: 'center', gap: 12, marginTop: 14 }
378
+ const line = { flex: 1, height: 1, background: 'rgba(148,163,184,0.04)' }
379
+ const orText = { fontSize: 13, color: 'rgba(255,255,255,0.65)', padding: '0 8px' }
380
+
381
+ const form = { display: 'flex', flexDirection: 'column', gap: 12, width: '100%', marginTop: 4 }
382
+ const label = { display: 'flex', flexDirection: 'column', gap: 6 }
383
+ const labelSmall = { fontSize: 13, color: 'rgba(255,255,255,0.75)' }
384
+
385
+ const input = {
386
+ width: '100%',
387
+ padding: '10px 12px',
388
+ borderRadius: 12,
389
+ background: 'rgba(255,255,255,0.02)',
390
+ color: '#fef3c7',
391
+ border: '1px solid rgba(148,163,184,0.06)',
392
+ fontSize: 14,
393
+ outline: 'none',
394
+ boxSizing: 'border-box'
395
+ }
396
+
397
+ const submitBtn = {
398
+ marginTop: 6,
399
+ width: '100%',
400
+ padding: '10px 12px',
401
+ borderRadius: 12,
402
+ background: 'linear-gradient(180deg,#2563eb,#60a5fa)',
370
403
  border: 'none',
371
- color: 'rgba(255,255,255,0.7)',
404
+ color: '#fff',
405
+ fontWeight: 600,
372
406
  cursor: 'pointer',
373
- fontSize: 14,
374
- lineHeight: 1
407
+ fontSize: 15,
408
+ display: 'inline-flex',
409
+ alignItems: 'center',
410
+ justifyContent: 'center',
411
+ minHeight: 44
375
412
  }
413
+
414
+ const cardFooter = {
415
+ padding: '18px',
416
+ borderTop: '1px solid rgba(148,163,184,0.03)',
417
+ background: 'transparent'
418
+ }
419
+
420
+ const smallText = { textAlign: 'center', fontSize: 13 }
421
+ const muted = { color: 'rgba(255,255,255,0.75)' }
422
+ const link = { color: '#60a5fa', textDecoration: 'none', fontWeight: 600 }
423
+ const thinDivider = { height: 1, background: 'rgba(148,163,184,0.04)', margin: '12px 0' }
424
+ const securedText = { color: 'rgba(255,255,255,0.9)', fontWeight: 600 }