flowlink-auth 2.8.5 → 2.8.7

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 +189 -96
  2. package/package.json +1 -1
  3. package/src/SignIn.jsx +290 -150
package/dist/SignIn.js CHANGED
@@ -1,8 +1,13 @@
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";
4
5
  import { useAuth } from "./provider.js";
5
- function SignIn({ onSuccess } = {}) {
6
+ function SignIn({
7
+ agency = { name: "chest", logo: "/logo.png" },
8
+ onSuccess,
9
+ onError
10
+ } = {}) {
6
11
  const {
7
12
  publishableKey,
8
13
  baseUrl,
@@ -14,8 +19,7 @@ function SignIn({ onSuccess } = {}) {
14
19
  fetchMe,
15
20
  setUser
16
21
  } = (typeof useAuth === "function" ? useAuth() : {}) || {};
17
- const [email, setEmail] = useState("");
18
- const [password, setPassword] = useState("");
22
+ const [form2, setForm] = useState({ email: "", password: "" });
19
23
  const [loading, setLoading] = useState(false);
20
24
  const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false });
21
25
  const redirectTimer = useRef(null);
@@ -35,7 +39,20 @@ function SignIn({ onSuccess } = {}) {
35
39
  });
36
40
  };
37
41
  }, []);
38
- function showToast(type, message, ms = 5e3) {
42
+ function showLocalToast(type, message, ms = 3500) {
43
+ if (type === "error" && typeof onError === "function") {
44
+ try {
45
+ onError(message);
46
+ } catch (_) {
47
+ }
48
+ return;
49
+ }
50
+ if (type === "success" && typeof onSuccess === "function") {
51
+ try {
52
+ onSuccess(message);
53
+ } catch (_) {
54
+ }
55
+ }
39
56
  const id = ++toastId.current;
40
57
  const t = { id, type, message, _timer: null };
41
58
  setToasts((prev) => [t, ...prev].slice(0, 6));
@@ -44,7 +61,7 @@ function SignIn({ onSuccess } = {}) {
44
61
  }, ms);
45
62
  t._timer = timer;
46
63
  }
47
- function removeToast(id) {
64
+ function removeLocalToast(id) {
48
65
  setToasts((prev) => {
49
66
  prev.forEach((t) => {
50
67
  if (t.id === id && t._timer) clearTimeout(t._timer);
@@ -58,12 +75,15 @@ function SignIn({ onSuccess } = {}) {
58
75
  else if (typeof window !== "undefined") window.location.assign(redirect);
59
76
  return null;
60
77
  }
78
+ function handleChange(e) {
79
+ setForm((prev) => ({ ...prev, [e.target.id]: e.target.value }));
80
+ }
61
81
  async function submit(e) {
62
82
  e.preventDefault();
63
83
  if (loading) return;
64
84
  setLoading(true);
65
- if (!email || !password) {
66
- showToast("error", "Email and password are required");
85
+ if (!form2.email || !form2.password) {
86
+ showLocalToast("error", "Email and password are required");
67
87
  setLoading(false);
68
88
  return;
69
89
  }
@@ -76,7 +96,7 @@ function SignIn({ onSuccess } = {}) {
76
96
  "Content-Type": "application/json",
77
97
  "x-publishable-key": publishableKey || ""
78
98
  },
79
- body: JSON.stringify({ email, password })
99
+ body: JSON.stringify({ email: form2.email, password: form2.password })
80
100
  });
81
101
  const ct = res.headers.get("content-type") || "";
82
102
  let data = {};
@@ -102,13 +122,13 @@ function SignIn({ onSuccess } = {}) {
102
122
  } else if (typeof fetchMe === "function") {
103
123
  await fetchMe();
104
124
  }
105
- if (onSuccess) {
125
+ showLocalToast("success", "Signed in. Redirecting...");
126
+ if (typeof onSuccess === "function") {
106
127
  try {
107
128
  onSuccess(data);
108
129
  } catch (_) {
109
130
  }
110
131
  }
111
- showToast("success", "Signed in. Redirecting...");
112
132
  if (redirect) {
113
133
  redirectTimer.current = setTimeout(() => {
114
134
  if (typeof redirectTo === "function") redirectTo(redirect);
@@ -116,7 +136,8 @@ function SignIn({ onSuccess } = {}) {
116
136
  }, 250);
117
137
  }
118
138
  } catch (err) {
119
- showToast("error", err?.message || "Network error");
139
+ const message = err?.message || "Network error";
140
+ showLocalToast("error", message);
120
141
  console.error("Signin error:", err);
121
142
  } finally {
122
143
  setLoading(false);
@@ -130,9 +151,7 @@ function SignIn({ onSuccess } = {}) {
130
151
  const callbackUrl = encodeURIComponent(`${typeof window !== "undefined" ? window.location.origin : ""}/signin`);
131
152
  const sdkBase = baseUrl || (typeof window !== "undefined" ? window.location.origin.replace(/\/+$/, "") : "");
132
153
  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
- }
154
+ if (!publishableKey) throw new Error("Missing publishable key (client side).");
136
155
  const res = await fetch(startUrl, {
137
156
  method: "GET",
138
157
  headers: { "x-publishable-key": publishableKey },
@@ -143,131 +162,215 @@ function SignIn({ onSuccess } = {}) {
143
162
  if (!data?.oauthUrl) throw new Error("SDK start did not return oauthUrl");
144
163
  if (typeof window !== "undefined") window.location.href = data.oauthUrl;
145
164
  } catch (err) {
146
- showToast("error", err?.message || "OAuth start failed");
165
+ showLocalToast("error", err?.message || "OAuth start failed");
147
166
  console.error("OAuth start error:", err);
148
167
  setLoadingOauth((prev) => ({ ...prev, [provider]: false }));
149
168
  }
150
169
  }
151
170
  const handleGoogle = (e) => {
152
- if (e && typeof e.preventDefault === "function") e.preventDefault();
171
+ if (e?.preventDefault) e.preventDefault();
153
172
  startOAuthFlow("google");
154
173
  };
155
174
  const handleGithub = (e) => {
156
- if (e && typeof e.preventDefault === "function") e.preventDefault();
175
+ if (e?.preventDefault) e.preventDefault();
157
176
  startOAuthFlow("github");
158
177
  };
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(
178
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: toastContainer, "aria-live": "polite", "aria-atomic": "true" }, toasts.map((t) => /* @__PURE__ */ React.createElement(
160
179
  "div",
161
180
  {
162
181
  key: t.id,
163
182
  role: "status",
164
183
  style: {
165
184
  ...toastBase,
166
- ...t.type === "error" ? toastError : t.type === "success" ? toastSuccess : toastInfo
185
+ ...t.type === "error" ? toastError : toastSuccess
167
186
  },
168
187
  onMouseEnter: () => {
169
188
  if (t._timer) clearTimeout(t._timer);
170
189
  },
171
190
  onMouseLeave: () => {
172
- const timer = setTimeout(() => removeToast(t.id), 3e3);
191
+ const timer = setTimeout(() => removeLocalToast(t.id), 2500);
173
192
  setToasts((prev) => prev.map((x) => x.id === t.id ? { ...x, _timer: timer } : x));
174
193
  }
175
194
  },
176
195
  /* @__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(
196
+ /* @__PURE__ */ React.createElement("button", { "aria-label": "Dismiss", onClick: () => removeLocalToast(t.id), style: toastCloseBtn }, "\u2715")
197
+ ))), /* @__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(
198
+ "button",
199
+ {
200
+ onClick: handleGoogle,
201
+ type: "button",
202
+ style: { ...oauthButton, ...oauthGoogle },
203
+ disabled: loading || loadingOauth.google,
204
+ "aria-disabled": loading || loadingOauth.google
205
+ },
206
+ /* @__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" })),
207
+ /* @__PURE__ */ React.createElement("span", null, loadingOauth.google ? "Loading..." : "Google")
208
+ ), /* @__PURE__ */ React.createElement(
209
+ "button",
210
+ {
211
+ onClick: handleGithub,
212
+ type: "button",
213
+ style: { ...oauthButton, ...oauthGithub },
214
+ disabled: loading || loadingOauth.github,
215
+ "aria-disabled": loading || loadingOauth.github
216
+ },
217
+ /* @__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" })),
218
+ /* @__PURE__ */ React.createElement("span", null, loadingOauth.github ? "Loading..." : "Github")
219
+ )), /* @__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
220
  "input",
180
221
  {
181
- style: input,
182
- value: email,
183
- onChange: (e) => setEmail(e.target.value),
222
+ id: "email",
184
223
  type: "email",
185
- required: true
224
+ onChange: handleChange,
225
+ value: form2.email,
226
+ required: true,
227
+ placeholder: "you@example.com",
228
+ style: input
186
229
  }
187
- ), /* @__PURE__ */ React.createElement("label", { style: label }, "Password"), /* @__PURE__ */ React.createElement(
230
+ )), /* @__PURE__ */ React.createElement("label", { style: label }, /* @__PURE__ */ React.createElement("span", { style: labelSmall }, "Password"), /* @__PURE__ */ React.createElement(
188
231
  "input",
189
232
  {
190
- style: input,
233
+ id: "password",
191
234
  type: "password",
192
- value: password,
193
- onChange: (e) => setPassword(e.target.value),
194
- required: true
235
+ onChange: handleChange,
236
+ value: form2.password,
237
+ required: true,
238
+ placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
239
+ style: input
195
240
  }
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"))))));
241
+ )), /* @__PURE__ */ React.createElement(
242
+ "button",
243
+ {
244
+ type: "submit",
245
+ style: submitBtn,
246
+ "aria-disabled": loading,
247
+ disabled: loading
248
+ },
249
+ loading ? "Signing in..." : "Sign In"
250
+ ))), /* @__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
251
  }
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",
252
+ const page = {
208
253
  minHeight: "100vh",
209
- zIndex: 9999
254
+ display: "flex",
255
+ alignItems: "center",
256
+ justifyContent: "center",
257
+ padding: "24px",
258
+ background: "linear-gradient(to bottom, #0f1724, #0b1220 40%, #02040a 100%)",
259
+ color: "#fef3c7"
210
260
  };
211
- const modal = {
261
+ const cardWrap = {
212
262
  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,
263
+ maxWidth: 420
264
+ };
265
+ const card = {
266
+ width: "100%",
267
+ borderRadius: 20,
268
+ background: "linear-gradient(180deg, rgba(15,17,20,0.88), rgba(6,8,12,0.92))",
269
+ border: "1px solid rgba(148,163,184,0.06)",
270
+ backdropFilter: "blur(8px)",
271
+ boxShadow: "0 18px 50px rgba(2,6,23,0.6)",
272
+ overflow: "hidden",
273
+ display: "flex",
274
+ flexDirection: "column"
275
+ };
276
+ const cardInner = {
277
+ padding: "28px",
278
+ display: "flex",
279
+ flexDirection: "column",
280
+ gap: 14,
281
+ background: "linear-gradient(180deg, rgba(12,14,18,0.02), rgba(0,0,0,0.02))",
282
+ borderRadius: "20px"
283
+ };
284
+ const brand = {
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ gap: 8,
288
+ alignItems: "center",
289
+ textAlign: "center"
290
+ };
291
+ const brandRow = {
292
+ display: "flex",
293
+ alignItems: "center",
294
+ gap: 10,
295
+ justifyContent: "center"
296
+ };
297
+ const logoWrap = {
298
+ width: 30,
299
+ height: 30,
300
+ borderRadius: 999,
301
+ overflow: "hidden",
302
+ display: "flex",
303
+ alignItems: "center",
304
+ justifyContent: "center",
305
+ background: "linear-gradient(135deg,#2b313a,#0f1724)"
306
+ };
307
+ const logoImgStyle = { borderRadius: 999, objectFit: "cover" };
308
+ const brandTitle = { margin: 0, fontSize: 20, fontWeight: 500, color: "#fff" };
309
+ const brandLead = { fontSize: 15, color: "#f8fafc" };
310
+ const brandSub = { fontSize: 13, color: "rgba(255,255,255,0.7)", marginTop: 2 };
311
+ const brandText = { display: "flex", flexDirection: "column", gap: 2, alignItems: "center" };
312
+ const oauthRow = { display: "flex", gap: 10, marginTop: 10 };
313
+ const oauthButton = {
314
+ flex: 1,
315
+ display: "inline-flex",
316
+ alignItems: "center",
317
+ justifyContent: "center",
318
+ padding: "10px 12px",
319
+ borderRadius: 12,
320
+ border: "1px solid rgba(148,163,184,0.06)",
321
+ fontSize: 14,
322
+ cursor: "pointer",
323
+ userSelect: "none",
324
+ gap: 8,
325
+ minHeight: 40,
326
+ background: "transparent",
220
327
  color: "#fff"
221
328
  };
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 };
329
+ const oauthGoogle = { background: "rgba(255,255,255,0.02)" };
330
+ const oauthGithub = { background: "rgba(255,255,255,0.01)" };
331
+ const dividerRow = { display: "flex", alignItems: "center", gap: 12, marginTop: 14 };
332
+ const line = { flex: 1, height: 1, background: "rgba(148,163,184,0.04)" };
333
+ const orText = { fontSize: 13, color: "rgba(255,255,255,0.65)", padding: "0 8px" };
334
+ const form = { display: "flex", flexDirection: "column", gap: 12, width: "100%", marginTop: 4 };
335
+ const label = { display: "flex", flexDirection: "column", gap: 6 };
336
+ const labelSmall = { fontSize: 13, color: "rgba(255,255,255,0.75)" };
225
337
  const input = {
226
338
  width: "100%",
227
339
  padding: "10px 12px",
228
- marginTop: 6,
229
- borderRadius: 10,
230
- border: "1px solid rgba(148,163,184,0.10)",
340
+ borderRadius: 12,
231
341
  background: "rgba(255,255,255,0.02)",
232
- color: "#e6e6e6",
342
+ color: "#fef3c7",
343
+ border: "1px solid rgba(148,163,184,0.06)",
344
+ fontSize: 14,
345
+ outline: "none",
233
346
  boxSizing: "border-box"
234
347
  };
235
- const button = {
348
+ const submitBtn = {
349
+ marginTop: 6,
236
350
  width: "100%",
237
351
  padding: "10px 12px",
238
- borderRadius: 10,
239
- background: "linear-gradient(90deg,#06b6d4,#2563eb)",
240
- color: "#0b1220",
352
+ borderRadius: 12,
353
+ background: "linear-gradient(180deg,#2563eb,#60a5fa)",
241
354
  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
355
  color: "#fff",
251
- border: "1px solid rgba(148,163,184,0.08)",
356
+ fontWeight: 600,
252
357
  cursor: "pointer",
358
+ fontSize: 15,
253
359
  display: "inline-flex",
254
360
  alignItems: "center",
255
361
  justifyContent: "center",
256
- gap: 8
362
+ minHeight: 44
257
363
  };
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
364
+ const cardFooter = {
365
+ padding: "18px",
366
+ borderTop: "1px solid rgba(148,163,184,0.03)",
367
+ background: "transparent"
270
368
  };
369
+ const smallText = { textAlign: "center", fontSize: 13 };
370
+ const muted = { color: "rgba(255,255,255,0.75)" };
371
+ const link = { color: "#60a5fa", textDecoration: "none", fontWeight: 600 };
372
+ const thinDivider = { height: 1, background: "rgba(148,163,184,0.04)", margin: "12px 0" };
373
+ const securedText = { color: "rgba(255,255,255,0.9)", fontWeight: 600 };
271
374
  const toastContainer = {
272
375
  position: "fixed",
273
376
  top: 18,
@@ -287,21 +390,11 @@ const toastBase = {
287
390
  borderRadius: 10,
288
391
  boxShadow: "0 8px 20px rgba(2,6,23,0.6)",
289
392
  color: "#fff",
290
- fontSize: 13,
291
- minWidth: 120
393
+ fontSize: 13
292
394
  };
293
395
  const toastError = { background: "#000", border: "1px solid rgba(255,255,255,0.06)" };
294
396
  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
- };
397
+ const toastCloseBtn = { marginLeft: 8, background: "transparent", border: "none", color: "rgba(255,255,255,0.7)", cursor: "pointer", fontSize: 14, lineHeight: 1 };
305
398
  export {
306
399
  SignIn as default
307
400
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowlink-auth",
3
- "version": "2.8.5",
3
+ "version": "2.8.7",
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,26 @@
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'
4
6
  import { useAuth } from './provider.js'
5
7
 
6
- export default function SignIn({ onSuccess } = {}) {
8
+ /**
9
+ * SignIn component (SDK-friendly)
10
+ * - Does NOT depend on react-toastify or any external UI lib
11
+ * - Uses a small local black toast system (same pattern as signup)
12
+ * - Exposes optional onSuccess/onError callbacks for host apps
13
+ *
14
+ * Props:
15
+ * - agency: { name, logo } (optional)
16
+ * - onSuccess: function(data) optional callback
17
+ * - onError: function(message) optional callback
18
+ */
19
+ export default function SignIn({
20
+ agency = { name: 'chest', logo: '/logo.png' },
21
+ onSuccess,
22
+ onError
23
+ } = {}) {
7
24
  const {
8
25
  publishableKey,
9
26
  baseUrl,
@@ -16,17 +33,17 @@ export default function SignIn({ onSuccess } = {}) {
16
33
  setUser
17
34
  } = (typeof useAuth === 'function' ? useAuth() : {}) || {}
18
35
 
19
- const [email, setEmail] = useState('')
20
- const [password, setPassword] = useState('')
36
+ const [form, setForm] = useState({ email: '', password: '' })
21
37
  const [loading, setLoading] = useState(false)
22
38
  const [loadingOauth, setLoadingOauth] = useState({ google: false, github: false })
23
-
24
39
  const redirectTimer = useRef(null)
40
+
41
+ // local (SDK) toasts — black background — used only if host doesn't provide onError/onSuccess
25
42
  const toastId = useRef(0)
26
43
  const [toasts, setToasts] = useState([])
27
44
 
28
45
  useEffect(() => {
29
- // soft-disable pinch-zoom on mobile while mounted
46
+ // Soft-disable pinch-zoom on mobile while mounted (SDK-safe)
30
47
  const meta = document.createElement('meta')
31
48
  meta.name = 'viewport'
32
49
  meta.content = 'width=device-width, initial-scale=1, maximum-scale=1'
@@ -36,14 +53,24 @@ export default function SignIn({ onSuccess } = {}) {
36
53
  if (redirectTimer.current) clearTimeout(redirectTimer.current)
37
54
  const existing = document.querySelector('meta[name="viewport"]')
38
55
  if (existing && existing.content === meta.content) document.head.removeChild(existing)
39
- // clear timers for toasts
56
+ // clear toast timers
40
57
  toasts.forEach(t => { if (t._timer) clearTimeout(t._timer) })
41
58
  }
42
59
  // eslint-disable-next-line react-hooks/exhaustive-deps
43
60
  }, [])
44
61
 
45
- // Toast helpers (black background)
46
- function showToast(type, message, ms = 5000) {
62
+ // ---------- toast helpers ----------
63
+ function showLocalToast(type, message, ms = 3500) {
64
+ // if host provided onError/onSuccess, prefer that
65
+ if (type === 'error' && typeof onError === 'function') {
66
+ try { onError(message) } catch (_) {}
67
+ return
68
+ }
69
+ if (type === 'success' && typeof onSuccess === 'function') {
70
+ try { onSuccess(message) } catch (_) {}
71
+ // still show local toast for feedback (optional)
72
+ }
73
+
47
74
  const id = ++toastId.current
48
75
  const t = { id, type, message, _timer: null }
49
76
  setToasts(prev => [t, ...prev].slice(0, 6))
@@ -53,7 +80,7 @@ export default function SignIn({ onSuccess } = {}) {
53
80
  t._timer = timer
54
81
  }
55
82
 
56
- function removeToast(id) {
83
+ function removeLocalToast(id) {
57
84
  setToasts(prev => {
58
85
  prev.forEach(t => { if (t.id === id && t._timer) clearTimeout(t._timer) })
59
86
  return prev.filter(x => x.id !== id)
@@ -68,13 +95,18 @@ export default function SignIn({ onSuccess } = {}) {
68
95
  return null
69
96
  }
70
97
 
98
+ function handleChange(e) {
99
+ setForm(prev => ({ ...prev, [e.target.id]: e.target.value }))
100
+ }
101
+
102
+ // ---------- submit login ----------
71
103
  async function submit(e) {
72
104
  e.preventDefault()
73
105
  if (loading) return
74
106
  setLoading(true)
75
107
 
76
- if (!email || !password) {
77
- showToast('error', 'Email and password are required')
108
+ if (!form.email || !form.password) {
109
+ showLocalToast('error', 'Email and password are required')
78
110
  setLoading(false)
79
111
  return
80
112
  }
@@ -89,7 +121,7 @@ export default function SignIn({ onSuccess } = {}) {
89
121
  'Content-Type': 'application/json',
90
122
  'x-publishable-key': publishableKey || ''
91
123
  },
92
- body: JSON.stringify({ email, password })
124
+ body: JSON.stringify({ email: form.email, password: form.password })
93
125
  })
94
126
 
95
127
  const ct = res.headers.get('content-type') || ''
@@ -117,11 +149,12 @@ export default function SignIn({ onSuccess } = {}) {
117
149
  await fetchMe()
118
150
  }
119
151
 
120
- if (onSuccess) {
152
+ // callback & local success toast (host will receive onSuccess if provided)
153
+ showLocalToast('success', 'Signed in. Redirecting...')
154
+ if (typeof onSuccess === 'function') {
121
155
  try { onSuccess(data) } catch (_) {}
122
156
  }
123
157
 
124
- showToast('success', 'Signed in. Redirecting...')
125
158
  if (redirect) {
126
159
  redirectTimer.current = setTimeout(() => {
127
160
  if (typeof redirectTo === 'function') redirectTo(redirect)
@@ -129,17 +162,19 @@ export default function SignIn({ onSuccess } = {}) {
129
162
  }, 250)
130
163
  }
131
164
  } catch (err) {
132
- showToast('error', err?.message || 'Network error')
165
+ const message = err?.message || 'Network error'
166
+ showLocalToast('error', message)
133
167
  console.error('Signin error:', err)
134
168
  } finally {
135
169
  setLoading(false)
136
170
  }
137
171
  }
138
172
 
173
+ // ---------- OAuth start ----------
139
174
  async function startOAuthFlow(provider) {
140
- // prevent double start
141
175
  if (loading || loadingOauth[provider]) return
142
176
  setLoadingOauth(prev => ({ ...prev, [provider]: true }))
177
+
143
178
  try {
144
179
  const rid =
145
180
  (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
@@ -150,9 +185,7 @@ export default function SignIn({ onSuccess } = {}) {
150
185
  const sdkBase = baseUrl || (typeof window !== 'undefined' ? window.location.origin.replace(/\/+$/, '') : '')
151
186
  const startUrl = `${sdkBase.replace(/\/+$/, '')}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`
152
187
 
153
- if (!publishableKey) {
154
- throw new Error('Missing publishable key (client side). Set NEXT_PUBLIC_FLOWLINK_PUBLISHABLE_KEY or provide publishableKey in provider.')
155
- }
188
+ if (!publishableKey) throw new Error('Missing publishable key (client side).')
156
189
 
157
190
  const res = await fetch(startUrl, {
158
191
  method: 'GET',
@@ -164,27 +197,22 @@ export default function SignIn({ onSuccess } = {}) {
164
197
  if (!res.ok) throw new Error(data?.error || `OAuth start failed (${res.status})`)
165
198
  if (!data?.oauthUrl) throw new Error('SDK start did not return oauthUrl')
166
199
 
200
+ // navigate to provider (page unloads)
167
201
  if (typeof window !== 'undefined') window.location.href = data.oauthUrl
168
202
  } catch (err) {
169
- showToast('error', err?.message || 'OAuth start failed')
203
+ showLocalToast('error', err?.message || 'OAuth start failed')
170
204
  console.error('OAuth start error:', err)
171
205
  setLoadingOauth(prev => ({ ...prev, [provider]: false }))
172
206
  }
173
207
  }
174
208
 
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
- }
209
+ const handleGoogle = (e) => { if (e?.preventDefault) e.preventDefault(); startOAuthFlow('google') }
210
+ const handleGithub = (e) => { if (e?.preventDefault) e.preventDefault(); startOAuthFlow('github') }
184
211
 
212
+ // ---------- render ----------
185
213
  return (
186
- <div style={overlay}>
187
- {/* Toasts */}
214
+ <>
215
+ {/* Local toast container (black style) */}
188
216
  <div style={toastContainer} aria-live="polite" aria-atomic="true">
189
217
  {toasts.map(t => (
190
218
  <div
@@ -192,150 +220,275 @@ export default function SignIn({ onSuccess } = {}) {
192
220
  role="status"
193
221
  style={{
194
222
  ...toastBase,
195
- ...(t.type === 'error' ? toastError : t.type === 'success' ? toastSuccess : toastInfo)
223
+ ...(t.type === 'error' ? toastError : toastSuccess)
196
224
  }}
197
225
  onMouseEnter={() => { if (t._timer) clearTimeout(t._timer) }}
198
226
  onMouseLeave={() => {
199
- const timer = setTimeout(() => removeToast(t.id), 3000)
227
+ const timer = setTimeout(() => removeLocalToast(t.id), 2500)
200
228
  setToasts(prev => prev.map(x => x.id === t.id ? { ...x, _timer: timer } : x))
201
229
  }}
202
230
  >
203
231
  <div style={{ flex: 1 }}>{t.message}</div>
204
- <button aria-label="Dismiss" onClick={() => removeToast(t.id)} style={toastCloseBtn}>✕</button>
232
+ <button aria-label="Dismiss" onClick={() => removeLocalToast(t.id)} style={toastCloseBtn}>✕</button>
205
233
  </div>
206
234
  ))}
207
235
  </div>
208
236
 
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>
237
+ <div style={page}>
238
+ <div style={cardWrap}>
239
+ <div style={card}>
240
+ <div style={cardInner}>
241
+ <div style={brand}>
242
+ <div style={brandRow}>
243
+ <div style={logoWrap}>
244
+ <Image src={agency.logo} width={30} height={30} alt={agency.name} style={logoImgStyle} />
245
+ </div>
246
+ <h1 style={brandTitle}>{agency.name}</h1>
247
+ </div>
248
+ <div style={brandText}>
249
+ <div style={brandLead}>Sign in to {agency.name}</div>
250
+ <div style={brandSub}>Welcome back! Let's get you signed in.</div>
251
+ </div>
252
+ </div>
253
+
254
+ <div style={oauthRow}>
255
+ <button
256
+ onClick={handleGoogle}
257
+ type="button"
258
+ style={{ ...oauthButton, ...oauthGoogle }}
259
+ disabled={loading || loadingOauth.google}
260
+ aria-disabled={loading || loadingOauth.google}
261
+ >
262
+ <svg width={18} style={{ marginRight: 10 }} viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" fill="#000000" aria-hidden>
263
+ <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>
264
+ <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>
265
+ <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>
266
+ <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>
267
+ </svg>
268
+ <span>{loadingOauth.google ? 'Loading...' : 'Google'}</span>
269
+ </button>
270
+
271
+ <button
272
+ onClick={handleGithub}
273
+ type="button"
274
+ style={{ ...oauthButton, ...oauthGithub }}
275
+ disabled={loading || loadingOauth.github}
276
+ aria-disabled={loading || loadingOauth.github}
277
+ >
278
+ <svg width={18} style={{ marginRight: 10 }} xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 20 20" aria-hidden>
279
+ <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" />
280
+ </svg>
281
+ <span>{loadingOauth.github ? 'Loading...' : 'Github'}</span>
282
+ </button>
283
+ </div>
284
+
285
+ <div style={dividerRow}>
286
+ <div style={line} />
287
+ <div style={orText}>or</div>
288
+ <div style={line} />
289
+ </div>
290
+
291
+ <form onSubmit={submit} style={form}>
292
+ <label style={label}>
293
+ <span style={labelSmall}>Email address</span>
294
+ <input
295
+ id="email"
296
+ type="email"
297
+ onChange={handleChange}
298
+ value={form.email}
299
+ required
300
+ placeholder="you@example.com"
301
+ style={input}
302
+ />
303
+ </label>
304
+
305
+ <label style={label}>
306
+ <span style={labelSmall}>Password</span>
307
+ <input
308
+ id="password"
309
+ type="password"
310
+ onChange={handleChange}
311
+ value={form.password}
312
+ required
313
+ placeholder="••••••••"
314
+ style={input}
315
+ />
316
+ </label>
317
+
318
+ <button
319
+ type="submit"
320
+ style={submitBtn}
321
+ aria-disabled={loading}
322
+ disabled={loading}
323
+ >
324
+ {loading ? 'Signing in...' : 'Sign In'}
325
+ </button>
326
+ </form>
327
+ </div>
328
+
329
+ <div style={cardFooter}>
330
+ <div style={smallText}>
331
+ <span style={muted}>Don't have an account? </span>
332
+ <Link href="/signup" style={link}>Create one</Link>
333
+ </div>
334
+
335
+ <div style={thinDivider} />
336
+
337
+ <div style={{ textAlign: 'center', marginTop: 8 }}>
338
+ <div style={securedText}>Secured by auth</div>
339
+ </div>
340
+ </div>
255
341
  </div>
256
- </form>
342
+ </div>
257
343
  </div>
258
- </div>
344
+ </>
259
345
  )
260
346
  }
261
347
 
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',
348
+ /* ---------- styles (JS objects) to match the provided UI exactly ---------- */
349
+
350
+ const page = {
272
351
  minHeight: '100vh',
273
- zIndex: 9999
352
+ display: 'flex',
353
+ alignItems: 'center',
354
+ justifyContent: 'center',
355
+ padding: '24px',
356
+ background: 'linear-gradient(to bottom, #0f1724, #0b1220 40%, #02040a 100%)',
357
+ color: '#fef3c7'
358
+ }
359
+
360
+ const cardWrap = {
361
+ width: '100%',
362
+ maxWidth: 420
274
363
  }
275
364
 
276
- const modal = {
365
+ const card = {
277
366
  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,
367
+ borderRadius: 20,
368
+ background: 'linear-gradient(180deg, rgba(15,17,20,0.88), rgba(6,8,12,0.92))',
369
+ border: '1px solid rgba(148,163,184,0.06)',
370
+ backdropFilter: 'blur(8px)',
371
+ boxShadow: '0 18px 50px rgba(2,6,23,0.6)',
372
+ overflow: 'hidden',
373
+ display: 'flex',
374
+ flexDirection: 'column'
375
+ }
376
+
377
+ const cardInner = {
378
+ padding: '28px',
379
+ display: 'flex',
380
+ flexDirection: 'column',
381
+ gap: 14,
382
+ background: 'linear-gradient(180deg, rgba(12,14,18,0.02), rgba(0,0,0,0.02))',
383
+ borderRadius: '20px'
384
+ }
385
+
386
+ const brand = {
387
+ display: 'flex',
388
+ flexDirection: 'column',
389
+ gap: 8,
390
+ alignItems: 'center',
391
+ textAlign: 'center'
392
+ }
393
+
394
+ const brandRow = {
395
+ display: 'flex',
396
+ alignItems: 'center',
397
+ gap: 10,
398
+ justifyContent: 'center'
399
+ }
400
+
401
+ const logoWrap = {
402
+ width: 30,
403
+ height: 30,
404
+ borderRadius: 999,
405
+ overflow: 'hidden',
406
+ display: 'flex',
407
+ alignItems: 'center',
408
+ justifyContent: 'center',
409
+ background: 'linear-gradient(135deg,#2b313a,#0f1724)'
410
+ }
411
+
412
+ const logoImgStyle = { borderRadius: 999, objectFit: 'cover' }
413
+
414
+ const brandTitle = { margin: 0, fontSize: 20, fontWeight: 500, color: '#fff' }
415
+ const brandLead = { fontSize: 15, color: '#f8fafc' }
416
+ const brandSub = { fontSize: 13, color: 'rgba(255,255,255,0.7)', marginTop: 2 }
417
+ const brandText = { display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }
418
+
419
+ const oauthRow = { display: 'flex', gap: 10, marginTop: 10 }
420
+
421
+ const oauthButton = {
422
+ flex: 1,
423
+ display: 'inline-flex',
424
+ alignItems: 'center',
425
+ justifyContent: 'center',
426
+ padding: '10px 12px',
427
+ borderRadius: 12,
428
+ border: '1px solid rgba(148,163,184,0.06)',
429
+ fontSize: 14,
430
+ cursor: 'pointer',
431
+ userSelect: 'none',
432
+ gap: 8,
433
+ minHeight: 40,
434
+ background: 'transparent',
285
435
  color: '#fff'
286
436
  }
287
437
 
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 }
438
+ const oauthGoogle = { background: 'rgba(255,255,255,0.02)' }
439
+ const oauthGithub = { background: 'rgba(255,255,255,0.01)' }
440
+
441
+ const dividerRow = { display: 'flex', alignItems: 'center', gap: 12, marginTop: 14 }
442
+ const line = { flex: 1, height: 1, background: 'rgba(148,163,184,0.04)' }
443
+ const orText = { fontSize: 13, color: 'rgba(255,255,255,0.65)', padding: '0 8px' }
444
+
445
+ const form = { display: 'flex', flexDirection: 'column', gap: 12, width: '100%', marginTop: 4 }
446
+ const label = { display: 'flex', flexDirection: 'column', gap: 6 }
447
+ const labelSmall = { fontSize: 13, color: 'rgba(255,255,255,0.75)' }
448
+
291
449
  const input = {
292
450
  width: '100%',
293
451
  padding: '10px 12px',
294
- marginTop: 6,
295
- borderRadius: 10,
296
- border: '1px solid rgba(148,163,184,0.10)',
452
+ borderRadius: 12,
297
453
  background: 'rgba(255,255,255,0.02)',
298
- color: '#e6e6e6',
454
+ color: '#fef3c7',
455
+ border: '1px solid rgba(148,163,184,0.06)',
456
+ fontSize: 14,
457
+ outline: 'none',
299
458
  boxSizing: 'border-box'
300
459
  }
301
- const button = {
460
+
461
+ const submitBtn = {
462
+ marginTop: 6,
302
463
  width: '100%',
303
464
  padding: '10px 12px',
304
- borderRadius: 10,
305
- background: 'linear-gradient(90deg,#06b6d4,#2563eb)',
306
- color: '#0b1220',
465
+ borderRadius: 12,
466
+ background: 'linear-gradient(180deg,#2563eb,#60a5fa)',
307
467
  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
468
  color: '#fff',
317
- border: '1px solid rgba(148,163,184,0.08)',
469
+ fontWeight: 600,
318
470
  cursor: 'pointer',
471
+ fontSize: 15,
319
472
  display: 'inline-flex',
320
473
  alignItems: 'center',
321
474
  justifyContent: 'center',
322
- gap: 8
475
+ minHeight: 44
323
476
  }
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
477
+
478
+ /* card footer */
479
+ const cardFooter = {
480
+ padding: '18px',
481
+ borderTop: '1px solid rgba(148,163,184,0.03)',
482
+ background: 'transparent'
336
483
  }
337
484
 
338
- /* Toast styles (black) */
485
+ const smallText = { textAlign: 'center', fontSize: 13 }
486
+ const muted = { color: 'rgba(255,255,255,0.75)' }
487
+ const link = { color: '#60a5fa', textDecoration: 'none', fontWeight: 600 }
488
+ const thinDivider = { height: 1, background: 'rgba(148,163,184,0.04)', margin: '12px 0' }
489
+ const securedText = { color: 'rgba(255,255,255,0.9)', fontWeight: 600 }
490
+
491
+ /* local toast styles */
339
492
  const toastContainer = {
340
493
  position: 'fixed',
341
494
  top: 18,
@@ -347,7 +500,6 @@ const toastContainer = {
347
500
  gap: 10,
348
501
  zIndex: 60000
349
502
  }
350
-
351
503
  const toastBase = {
352
504
  display: 'flex',
353
505
  gap: 10,
@@ -356,20 +508,8 @@ const toastBase = {
356
508
  borderRadius: 10,
357
509
  boxShadow: '0 8px 20px rgba(2,6,23,0.6)',
358
510
  color: '#fff',
359
- fontSize: 13,
360
- minWidth: 120
511
+ fontSize: 13
361
512
  }
362
-
363
513
  const toastError = { background: '#000', border: '1px solid rgba(255,255,255,0.06)' }
364
514
  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
- }
515
+ const toastCloseBtn = { marginLeft: 8, background: 'transparent', border: 'none', color: 'rgba(255,255,255,0.7)', cursor: 'pointer', fontSize: 14, lineHeight: 1 }