@authon/js 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +182 -77
- package/dist/index.mjs +182 -77
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -97,9 +97,11 @@ var ModalRenderer = class {
|
|
|
97
97
|
theme;
|
|
98
98
|
branding;
|
|
99
99
|
enabledProviders = [];
|
|
100
|
+
currentView = "signIn";
|
|
100
101
|
onProviderClick;
|
|
101
102
|
onEmailSubmit;
|
|
102
103
|
onClose;
|
|
104
|
+
escHandler = null;
|
|
103
105
|
constructor(options) {
|
|
104
106
|
this.mode = options.mode;
|
|
105
107
|
this.theme = options.theme || "auto";
|
|
@@ -118,10 +120,18 @@ var ModalRenderer = class {
|
|
|
118
120
|
this.branding = { ...DEFAULT_BRANDING, ...branding };
|
|
119
121
|
}
|
|
120
122
|
open(view = "signIn") {
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
+
if (this.shadowRoot && this.hostElement) {
|
|
124
|
+
this.switchView(view);
|
|
125
|
+
} else {
|
|
126
|
+
this.currentView = view;
|
|
127
|
+
this.render(view);
|
|
128
|
+
}
|
|
123
129
|
}
|
|
124
130
|
close() {
|
|
131
|
+
if (this.escHandler) {
|
|
132
|
+
document.removeEventListener("keydown", this.escHandler);
|
|
133
|
+
this.escHandler = null;
|
|
134
|
+
}
|
|
125
135
|
if (this.hostElement) {
|
|
126
136
|
this.hostElement.remove();
|
|
127
137
|
this.hostElement = null;
|
|
@@ -143,6 +153,21 @@ var ModalRenderer = class {
|
|
|
143
153
|
errorEl.appendChild(errDiv);
|
|
144
154
|
}
|
|
145
155
|
}
|
|
156
|
+
showBanner(message, type = "error") {
|
|
157
|
+
if (!this.shadowRoot) return;
|
|
158
|
+
this.clearBanner();
|
|
159
|
+
const inner = this.shadowRoot.getElementById("modal-inner");
|
|
160
|
+
if (!inner) return;
|
|
161
|
+
const banner = document.createElement("div");
|
|
162
|
+
banner.id = "authon-banner";
|
|
163
|
+
banner.className = type === "warning" ? "banner-warning" : "error-msg";
|
|
164
|
+
banner.textContent = message;
|
|
165
|
+
inner.insertBefore(banner, inner.firstChild);
|
|
166
|
+
}
|
|
167
|
+
clearBanner() {
|
|
168
|
+
if (!this.shadowRoot) return;
|
|
169
|
+
this.shadowRoot.getElementById("authon-banner")?.remove();
|
|
170
|
+
}
|
|
146
171
|
clearError() {
|
|
147
172
|
if (!this.shadowRoot) return;
|
|
148
173
|
this.shadowRoot.getElementById("authon-error-msg")?.remove();
|
|
@@ -166,6 +191,24 @@ var ModalRenderer = class {
|
|
|
166
191
|
if (!this.shadowRoot) return;
|
|
167
192
|
this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
|
|
168
193
|
}
|
|
194
|
+
// ── Smooth view switch (no flicker) ──
|
|
195
|
+
switchView(view) {
|
|
196
|
+
if (!this.shadowRoot || view === this.currentView) return;
|
|
197
|
+
this.currentView = view;
|
|
198
|
+
const isSignUp = view === "signUp";
|
|
199
|
+
const inner = this.shadowRoot.getElementById("modal-inner");
|
|
200
|
+
if (!inner) return;
|
|
201
|
+
inner.style.opacity = "0";
|
|
202
|
+
inner.style.transform = "translateY(-4px)";
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
inner.innerHTML = this.buildInnerContent(view);
|
|
205
|
+
this.attachInnerEvents(view);
|
|
206
|
+
void inner.offsetHeight;
|
|
207
|
+
inner.style.opacity = "1";
|
|
208
|
+
inner.style.transform = "translateY(0)";
|
|
209
|
+
}, 140);
|
|
210
|
+
}
|
|
211
|
+
// ── Render ──
|
|
169
212
|
render(view) {
|
|
170
213
|
const host = document.createElement("div");
|
|
171
214
|
host.setAttribute("data-authon-modal", "");
|
|
@@ -176,10 +219,26 @@ var ModalRenderer = class {
|
|
|
176
219
|
this.containerElement.appendChild(host);
|
|
177
220
|
}
|
|
178
221
|
this.shadowRoot = host.attachShadow({ mode: "open" });
|
|
179
|
-
this.shadowRoot.innerHTML = this.
|
|
180
|
-
this.
|
|
222
|
+
this.shadowRoot.innerHTML = this.buildShell(view);
|
|
223
|
+
this.attachInnerEvents(view);
|
|
224
|
+
this.attachShellEvents();
|
|
225
|
+
}
|
|
226
|
+
// ── HTML builders ──
|
|
227
|
+
/** Shell = style + backdrop + modal-container (stable across view switches) */
|
|
228
|
+
buildShell(view) {
|
|
229
|
+
const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
|
|
230
|
+
return `
|
|
231
|
+
<style>${this.buildCSS()}</style>
|
|
232
|
+
${popupWrapper}
|
|
233
|
+
<div class="modal-container" role="dialog" aria-modal="true">
|
|
234
|
+
<div id="modal-inner" class="modal-inner">
|
|
235
|
+
${this.buildInnerContent(view)}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`;
|
|
181
239
|
}
|
|
182
|
-
|
|
240
|
+
/** Inner content = everything inside the modal that changes per view */
|
|
241
|
+
buildInnerContent(view) {
|
|
183
242
|
const b = this.branding;
|
|
184
243
|
const isSignUp = view === "signUp";
|
|
185
244
|
const title = isSignUp ? "Create your account" : "Welcome back";
|
|
@@ -198,8 +257,8 @@ var ModalRenderer = class {
|
|
|
198
257
|
}).join("");
|
|
199
258
|
const divider = b.showDivider !== false && b.showEmailPassword !== false ? `<div class="divider"><span>or</span></div>` : "";
|
|
200
259
|
const emailForm = b.showEmailPassword !== false ? `<form class="email-form" id="email-form">
|
|
201
|
-
<input type="email" placeholder="Email address" name="email" required class="input" />
|
|
202
|
-
<input type="password" placeholder="Password" name="password" required class="input" />
|
|
260
|
+
<input type="email" placeholder="Email address" name="email" required class="input" autocomplete="email" />
|
|
261
|
+
<input type="password" placeholder="Password" name="password" required class="input" autocomplete="${isSignUp ? "new-password" : "current-password"}" />
|
|
203
262
|
<button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
|
|
204
263
|
</form>` : "";
|
|
205
264
|
const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
|
|
@@ -207,21 +266,16 @@ var ModalRenderer = class {
|
|
|
207
266
|
${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
|
|
208
267
|
${b.privacyUrl ? `<a href="${b.privacyUrl}" target="_blank">Privacy Policy</a>` : ""}
|
|
209
268
|
</div>` : "";
|
|
210
|
-
const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
|
|
211
269
|
return `
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
|
|
222
|
-
${footer}
|
|
223
|
-
${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <span class="secured-brand">Authon</span></div>` : ""}
|
|
224
|
-
</div>
|
|
270
|
+
${b.logoDataUrl ? `<img src="${b.logoDataUrl}" alt="Logo" class="logo" />` : ""}
|
|
271
|
+
<h2 class="title">${title}</h2>
|
|
272
|
+
${b.brandName ? `<p class="brand-name">${b.brandName}</p>` : ""}
|
|
273
|
+
<div class="providers">${providerButtons}</div>
|
|
274
|
+
${divider}
|
|
275
|
+
${emailForm}
|
|
276
|
+
<p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
|
|
277
|
+
${footer}
|
|
278
|
+
${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <a href="https://authon.dev" target="_blank" rel="noopener noreferrer" class="secured-link">Authon</a></div>` : ""}
|
|
225
279
|
`;
|
|
226
280
|
}
|
|
227
281
|
isDark() {
|
|
@@ -272,6 +326,9 @@ var ModalRenderer = class {
|
|
|
272
326
|
position: ${this.mode === "popup" ? "fixed" : "relative"};
|
|
273
327
|
${this.mode === "popup" ? `box-shadow: 0 25px 50px -12px rgba(0,0,0,${dark ? "0.5" : "0.25"}); animation: slideIn 0.3s ease;` : ""}
|
|
274
328
|
}
|
|
329
|
+
.modal-inner {
|
|
330
|
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
|
331
|
+
}
|
|
275
332
|
.logo { display: block; margin: 0 auto 16px; max-height: 48px; }
|
|
276
333
|
.title { text-align: center; font-size: 24px; font-weight: 700; margin-bottom: 8px; color: var(--authon-text); }
|
|
277
334
|
.brand-name { text-align: center; font-size: 14px; color: var(--authon-muted); margin-bottom: 24px; }
|
|
@@ -320,18 +377,25 @@ var ModalRenderer = class {
|
|
|
320
377
|
font-size: 13px; color: #ef4444; text-align: center;
|
|
321
378
|
animation: fadeIn 0.15s ease;
|
|
322
379
|
}
|
|
380
|
+
.banner-warning {
|
|
381
|
+
margin-bottom: 16px; padding: 10px 14px;
|
|
382
|
+
background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3);
|
|
383
|
+
border-radius: calc(var(--authon-radius) * 0.33);
|
|
384
|
+
font-size: 13px; color: #f59e0b; text-align: center;
|
|
385
|
+
animation: fadeIn 0.15s ease;
|
|
386
|
+
}
|
|
323
387
|
.switch-view { text-align: center; margin-top: 16px; font-size: 13px; color: var(--authon-muted); }
|
|
324
388
|
.switch-view a { color: var(--authon-primary-start); text-decoration: none; font-weight: 500; }
|
|
325
389
|
.switch-view a:hover { text-decoration: underline; }
|
|
326
|
-
.footer { text-align: center; margin-top:
|
|
390
|
+
.footer { text-align: center; margin-top: 12px; font-size: 12px; color: var(--authon-dim); }
|
|
327
391
|
.footer a { color: var(--authon-dim); text-decoration: none; }
|
|
328
392
|
.footer a:hover { text-decoration: underline; }
|
|
329
393
|
.secured-by {
|
|
330
|
-
text-align: center; margin-top:
|
|
331
|
-
border-top: 1px solid var(--authon-divider);
|
|
394
|
+
text-align: center; margin-top: 16px;
|
|
332
395
|
font-size: 11px; color: var(--authon-dim);
|
|
333
396
|
}
|
|
334
|
-
.secured-
|
|
397
|
+
.secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
|
|
398
|
+
.secured-link:hover { text-decoration: underline; }
|
|
335
399
|
/* Loading overlay */
|
|
336
400
|
#authon-loading-overlay {
|
|
337
401
|
position: absolute; inset: 0; z-index: 10;
|
|
@@ -370,7 +434,26 @@ var ModalRenderer = class {
|
|
|
370
434
|
${b.customCss || ""}
|
|
371
435
|
`;
|
|
372
436
|
}
|
|
373
|
-
|
|
437
|
+
// ── Event binding ──
|
|
438
|
+
/** Attach events to shell elements (backdrop, ESC) — called once */
|
|
439
|
+
attachShellEvents() {
|
|
440
|
+
if (!this.shadowRoot) return;
|
|
441
|
+
const backdrop = this.shadowRoot.getElementById("backdrop");
|
|
442
|
+
if (backdrop) {
|
|
443
|
+
backdrop.addEventListener("click", () => this.onClose());
|
|
444
|
+
}
|
|
445
|
+
if (this.escHandler) {
|
|
446
|
+
document.removeEventListener("keydown", this.escHandler);
|
|
447
|
+
}
|
|
448
|
+
if (this.mode === "popup") {
|
|
449
|
+
this.escHandler = (e) => {
|
|
450
|
+
if (e.key === "Escape") this.onClose();
|
|
451
|
+
};
|
|
452
|
+
document.addEventListener("keydown", this.escHandler);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/** Attach events to inner content (buttons, form, switch link) — called on each view */
|
|
456
|
+
attachInnerEvents(view) {
|
|
374
457
|
if (!this.shadowRoot) return;
|
|
375
458
|
this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
|
|
376
459
|
btn.addEventListener("click", () => {
|
|
@@ -397,19 +480,6 @@ var ModalRenderer = class {
|
|
|
397
480
|
this.open(view === "signIn" ? "signUp" : "signIn");
|
|
398
481
|
});
|
|
399
482
|
}
|
|
400
|
-
const backdrop = this.shadowRoot.getElementById("backdrop");
|
|
401
|
-
if (backdrop) {
|
|
402
|
-
backdrop.addEventListener("click", () => this.onClose());
|
|
403
|
-
}
|
|
404
|
-
if (this.mode === "popup") {
|
|
405
|
-
const handler = (e) => {
|
|
406
|
-
if (e.key === "Escape") {
|
|
407
|
-
this.onClose();
|
|
408
|
-
document.removeEventListener("keydown", handler);
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
document.addEventListener("keydown", handler);
|
|
412
|
-
}
|
|
413
483
|
}
|
|
414
484
|
};
|
|
415
485
|
|
|
@@ -619,7 +689,7 @@ var Authon = class {
|
|
|
619
689
|
async startOAuthFlow(provider) {
|
|
620
690
|
try {
|
|
621
691
|
const redirectUri = `${this.config.apiUrl}/v1/auth/oauth/redirect`;
|
|
622
|
-
const { url } = await this.apiGet(
|
|
692
|
+
const { url, state } = await this.apiGet(
|
|
623
693
|
`/v1/auth/oauth/${provider}/url?redirectUri=${encodeURIComponent(redirectUri)}`
|
|
624
694
|
);
|
|
625
695
|
this.modal?.showLoading();
|
|
@@ -632,54 +702,89 @@ var Authon = class {
|
|
|
632
702
|
"authon-oauth",
|
|
633
703
|
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no`
|
|
634
704
|
);
|
|
635
|
-
|
|
705
|
+
if (!popup || popup.closed) {
|
|
706
|
+
this.modal?.hideLoading();
|
|
707
|
+
this.modal?.showBanner(
|
|
708
|
+
"Pop-up blocked. Please allow pop-ups for this site and try again.",
|
|
709
|
+
"warning"
|
|
710
|
+
);
|
|
711
|
+
this.emit("error", new Error("Popup was blocked by the browser"));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
let resolved = false;
|
|
636
715
|
let cleaned = false;
|
|
716
|
+
const resolve = (tokens) => {
|
|
717
|
+
if (resolved) return;
|
|
718
|
+
resolved = true;
|
|
719
|
+
cleanup();
|
|
720
|
+
try {
|
|
721
|
+
if (!popup.closed) popup.close();
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
this.session.setSession(tokens);
|
|
725
|
+
this.modal?.close();
|
|
726
|
+
this.emit("signedIn", tokens.user);
|
|
727
|
+
};
|
|
728
|
+
const handleError = (msg) => {
|
|
729
|
+
if (resolved) return;
|
|
730
|
+
cleanup();
|
|
731
|
+
this.modal?.hideLoading();
|
|
732
|
+
this.modal?.showError(msg);
|
|
733
|
+
this.emit("error", new Error(msg));
|
|
734
|
+
};
|
|
637
735
|
const cleanup = () => {
|
|
638
736
|
if (cleaned) return;
|
|
639
737
|
cleaned = true;
|
|
640
|
-
window.removeEventListener("message",
|
|
641
|
-
|
|
738
|
+
window.removeEventListener("message", messageHandler);
|
|
739
|
+
if (apiPollTimer) clearInterval(apiPollTimer);
|
|
740
|
+
if (closePollTimer) clearInterval(closePollTimer);
|
|
642
741
|
if (maxTimer) clearTimeout(maxTimer);
|
|
643
742
|
};
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
this.modal?.hideLoading();
|
|
650
|
-
}, 1500);
|
|
743
|
+
const messageHandler = (e) => {
|
|
744
|
+
if (e.data?.type !== "authon-oauth-callback") return;
|
|
745
|
+
if (e.data.tokens) {
|
|
746
|
+
resolve(e.data.tokens);
|
|
747
|
+
}
|
|
651
748
|
};
|
|
652
|
-
window.addEventListener("
|
|
749
|
+
window.addEventListener("message", messageHandler);
|
|
750
|
+
const apiPollTimer = setInterval(async () => {
|
|
751
|
+
if (resolved || cleaned) return;
|
|
752
|
+
try {
|
|
753
|
+
const result = await this.apiGet(
|
|
754
|
+
`/v1/auth/oauth/poll?state=${encodeURIComponent(state)}`
|
|
755
|
+
);
|
|
756
|
+
if (result.status === "completed" && result.accessToken) {
|
|
757
|
+
resolve({
|
|
758
|
+
accessToken: result.accessToken,
|
|
759
|
+
refreshToken: result.refreshToken,
|
|
760
|
+
expiresIn: result.expiresIn,
|
|
761
|
+
user: result.user
|
|
762
|
+
});
|
|
763
|
+
} else if (result.status === "error") {
|
|
764
|
+
handleError(result.message || "Authentication failed");
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}, 1500);
|
|
769
|
+
const closePollTimer = setInterval(() => {
|
|
770
|
+
if (resolved || cleaned) return;
|
|
771
|
+
try {
|
|
772
|
+
if (popup.closed) {
|
|
773
|
+
clearInterval(closePollTimer);
|
|
774
|
+
setTimeout(() => {
|
|
775
|
+
if (resolved || cleaned) return;
|
|
776
|
+
cleanup();
|
|
777
|
+
this.modal?.hideLoading();
|
|
778
|
+
}, 3e3);
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}, 500);
|
|
653
783
|
const maxTimer = setTimeout(() => {
|
|
654
|
-
if (
|
|
784
|
+
if (resolved || cleaned) return;
|
|
655
785
|
cleanup();
|
|
656
786
|
this.modal?.hideLoading();
|
|
657
787
|
}, 18e4);
|
|
658
|
-
if (!popup) {
|
|
659
|
-
cleanup();
|
|
660
|
-
this.modal?.hideLoading();
|
|
661
|
-
this.emit("error", new Error("Popup was blocked by the browser"));
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const handler = async (e) => {
|
|
665
|
-
if (e.data?.type !== "authon-oauth-callback") return;
|
|
666
|
-
callbackReceived = true;
|
|
667
|
-
cleanup();
|
|
668
|
-
try {
|
|
669
|
-
const tokens = await this.apiPost("/v1/auth/oauth/callback", {
|
|
670
|
-
code: e.data.code,
|
|
671
|
-
state: e.data.state,
|
|
672
|
-
provider
|
|
673
|
-
});
|
|
674
|
-
this.session.setSession(tokens);
|
|
675
|
-
this.modal?.close();
|
|
676
|
-
this.emit("signedIn", tokens.user);
|
|
677
|
-
} catch (err) {
|
|
678
|
-
this.modal?.hideLoading();
|
|
679
|
-
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
window.addEventListener("message", handler);
|
|
683
788
|
} catch (err) {
|
|
684
789
|
this.modal?.hideLoading();
|
|
685
790
|
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
package/dist/index.mjs
CHANGED
|
@@ -70,9 +70,11 @@ var ModalRenderer = class {
|
|
|
70
70
|
theme;
|
|
71
71
|
branding;
|
|
72
72
|
enabledProviders = [];
|
|
73
|
+
currentView = "signIn";
|
|
73
74
|
onProviderClick;
|
|
74
75
|
onEmailSubmit;
|
|
75
76
|
onClose;
|
|
77
|
+
escHandler = null;
|
|
76
78
|
constructor(options) {
|
|
77
79
|
this.mode = options.mode;
|
|
78
80
|
this.theme = options.theme || "auto";
|
|
@@ -91,10 +93,18 @@ var ModalRenderer = class {
|
|
|
91
93
|
this.branding = { ...DEFAULT_BRANDING, ...branding };
|
|
92
94
|
}
|
|
93
95
|
open(view = "signIn") {
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
+
if (this.shadowRoot && this.hostElement) {
|
|
97
|
+
this.switchView(view);
|
|
98
|
+
} else {
|
|
99
|
+
this.currentView = view;
|
|
100
|
+
this.render(view);
|
|
101
|
+
}
|
|
96
102
|
}
|
|
97
103
|
close() {
|
|
104
|
+
if (this.escHandler) {
|
|
105
|
+
document.removeEventListener("keydown", this.escHandler);
|
|
106
|
+
this.escHandler = null;
|
|
107
|
+
}
|
|
98
108
|
if (this.hostElement) {
|
|
99
109
|
this.hostElement.remove();
|
|
100
110
|
this.hostElement = null;
|
|
@@ -116,6 +126,21 @@ var ModalRenderer = class {
|
|
|
116
126
|
errorEl.appendChild(errDiv);
|
|
117
127
|
}
|
|
118
128
|
}
|
|
129
|
+
showBanner(message, type = "error") {
|
|
130
|
+
if (!this.shadowRoot) return;
|
|
131
|
+
this.clearBanner();
|
|
132
|
+
const inner = this.shadowRoot.getElementById("modal-inner");
|
|
133
|
+
if (!inner) return;
|
|
134
|
+
const banner = document.createElement("div");
|
|
135
|
+
banner.id = "authon-banner";
|
|
136
|
+
banner.className = type === "warning" ? "banner-warning" : "error-msg";
|
|
137
|
+
banner.textContent = message;
|
|
138
|
+
inner.insertBefore(banner, inner.firstChild);
|
|
139
|
+
}
|
|
140
|
+
clearBanner() {
|
|
141
|
+
if (!this.shadowRoot) return;
|
|
142
|
+
this.shadowRoot.getElementById("authon-banner")?.remove();
|
|
143
|
+
}
|
|
119
144
|
clearError() {
|
|
120
145
|
if (!this.shadowRoot) return;
|
|
121
146
|
this.shadowRoot.getElementById("authon-error-msg")?.remove();
|
|
@@ -139,6 +164,24 @@ var ModalRenderer = class {
|
|
|
139
164
|
if (!this.shadowRoot) return;
|
|
140
165
|
this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
|
|
141
166
|
}
|
|
167
|
+
// ── Smooth view switch (no flicker) ──
|
|
168
|
+
switchView(view) {
|
|
169
|
+
if (!this.shadowRoot || view === this.currentView) return;
|
|
170
|
+
this.currentView = view;
|
|
171
|
+
const isSignUp = view === "signUp";
|
|
172
|
+
const inner = this.shadowRoot.getElementById("modal-inner");
|
|
173
|
+
if (!inner) return;
|
|
174
|
+
inner.style.opacity = "0";
|
|
175
|
+
inner.style.transform = "translateY(-4px)";
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
inner.innerHTML = this.buildInnerContent(view);
|
|
178
|
+
this.attachInnerEvents(view);
|
|
179
|
+
void inner.offsetHeight;
|
|
180
|
+
inner.style.opacity = "1";
|
|
181
|
+
inner.style.transform = "translateY(0)";
|
|
182
|
+
}, 140);
|
|
183
|
+
}
|
|
184
|
+
// ── Render ──
|
|
142
185
|
render(view) {
|
|
143
186
|
const host = document.createElement("div");
|
|
144
187
|
host.setAttribute("data-authon-modal", "");
|
|
@@ -149,10 +192,26 @@ var ModalRenderer = class {
|
|
|
149
192
|
this.containerElement.appendChild(host);
|
|
150
193
|
}
|
|
151
194
|
this.shadowRoot = host.attachShadow({ mode: "open" });
|
|
152
|
-
this.shadowRoot.innerHTML = this.
|
|
153
|
-
this.
|
|
195
|
+
this.shadowRoot.innerHTML = this.buildShell(view);
|
|
196
|
+
this.attachInnerEvents(view);
|
|
197
|
+
this.attachShellEvents();
|
|
198
|
+
}
|
|
199
|
+
// ── HTML builders ──
|
|
200
|
+
/** Shell = style + backdrop + modal-container (stable across view switches) */
|
|
201
|
+
buildShell(view) {
|
|
202
|
+
const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
|
|
203
|
+
return `
|
|
204
|
+
<style>${this.buildCSS()}</style>
|
|
205
|
+
${popupWrapper}
|
|
206
|
+
<div class="modal-container" role="dialog" aria-modal="true">
|
|
207
|
+
<div id="modal-inner" class="modal-inner">
|
|
208
|
+
${this.buildInnerContent(view)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
`;
|
|
154
212
|
}
|
|
155
|
-
|
|
213
|
+
/** Inner content = everything inside the modal that changes per view */
|
|
214
|
+
buildInnerContent(view) {
|
|
156
215
|
const b = this.branding;
|
|
157
216
|
const isSignUp = view === "signUp";
|
|
158
217
|
const title = isSignUp ? "Create your account" : "Welcome back";
|
|
@@ -171,8 +230,8 @@ var ModalRenderer = class {
|
|
|
171
230
|
}).join("");
|
|
172
231
|
const divider = b.showDivider !== false && b.showEmailPassword !== false ? `<div class="divider"><span>or</span></div>` : "";
|
|
173
232
|
const emailForm = b.showEmailPassword !== false ? `<form class="email-form" id="email-form">
|
|
174
|
-
<input type="email" placeholder="Email address" name="email" required class="input" />
|
|
175
|
-
<input type="password" placeholder="Password" name="password" required class="input" />
|
|
233
|
+
<input type="email" placeholder="Email address" name="email" required class="input" autocomplete="email" />
|
|
234
|
+
<input type="password" placeholder="Password" name="password" required class="input" autocomplete="${isSignUp ? "new-password" : "current-password"}" />
|
|
176
235
|
<button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
|
|
177
236
|
</form>` : "";
|
|
178
237
|
const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
|
|
@@ -180,21 +239,16 @@ var ModalRenderer = class {
|
|
|
180
239
|
${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
|
|
181
240
|
${b.privacyUrl ? `<a href="${b.privacyUrl}" target="_blank">Privacy Policy</a>` : ""}
|
|
182
241
|
</div>` : "";
|
|
183
|
-
const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
|
|
184
242
|
return `
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
|
|
195
|
-
${footer}
|
|
196
|
-
${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <span class="secured-brand">Authon</span></div>` : ""}
|
|
197
|
-
</div>
|
|
243
|
+
${b.logoDataUrl ? `<img src="${b.logoDataUrl}" alt="Logo" class="logo" />` : ""}
|
|
244
|
+
<h2 class="title">${title}</h2>
|
|
245
|
+
${b.brandName ? `<p class="brand-name">${b.brandName}</p>` : ""}
|
|
246
|
+
<div class="providers">${providerButtons}</div>
|
|
247
|
+
${divider}
|
|
248
|
+
${emailForm}
|
|
249
|
+
<p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
|
|
250
|
+
${footer}
|
|
251
|
+
${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <a href="https://authon.dev" target="_blank" rel="noopener noreferrer" class="secured-link">Authon</a></div>` : ""}
|
|
198
252
|
`;
|
|
199
253
|
}
|
|
200
254
|
isDark() {
|
|
@@ -245,6 +299,9 @@ var ModalRenderer = class {
|
|
|
245
299
|
position: ${this.mode === "popup" ? "fixed" : "relative"};
|
|
246
300
|
${this.mode === "popup" ? `box-shadow: 0 25px 50px -12px rgba(0,0,0,${dark ? "0.5" : "0.25"}); animation: slideIn 0.3s ease;` : ""}
|
|
247
301
|
}
|
|
302
|
+
.modal-inner {
|
|
303
|
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
|
304
|
+
}
|
|
248
305
|
.logo { display: block; margin: 0 auto 16px; max-height: 48px; }
|
|
249
306
|
.title { text-align: center; font-size: 24px; font-weight: 700; margin-bottom: 8px; color: var(--authon-text); }
|
|
250
307
|
.brand-name { text-align: center; font-size: 14px; color: var(--authon-muted); margin-bottom: 24px; }
|
|
@@ -293,18 +350,25 @@ var ModalRenderer = class {
|
|
|
293
350
|
font-size: 13px; color: #ef4444; text-align: center;
|
|
294
351
|
animation: fadeIn 0.15s ease;
|
|
295
352
|
}
|
|
353
|
+
.banner-warning {
|
|
354
|
+
margin-bottom: 16px; padding: 10px 14px;
|
|
355
|
+
background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3);
|
|
356
|
+
border-radius: calc(var(--authon-radius) * 0.33);
|
|
357
|
+
font-size: 13px; color: #f59e0b; text-align: center;
|
|
358
|
+
animation: fadeIn 0.15s ease;
|
|
359
|
+
}
|
|
296
360
|
.switch-view { text-align: center; margin-top: 16px; font-size: 13px; color: var(--authon-muted); }
|
|
297
361
|
.switch-view a { color: var(--authon-primary-start); text-decoration: none; font-weight: 500; }
|
|
298
362
|
.switch-view a:hover { text-decoration: underline; }
|
|
299
|
-
.footer { text-align: center; margin-top:
|
|
363
|
+
.footer { text-align: center; margin-top: 12px; font-size: 12px; color: var(--authon-dim); }
|
|
300
364
|
.footer a { color: var(--authon-dim); text-decoration: none; }
|
|
301
365
|
.footer a:hover { text-decoration: underline; }
|
|
302
366
|
.secured-by {
|
|
303
|
-
text-align: center; margin-top:
|
|
304
|
-
border-top: 1px solid var(--authon-divider);
|
|
367
|
+
text-align: center; margin-top: 16px;
|
|
305
368
|
font-size: 11px; color: var(--authon-dim);
|
|
306
369
|
}
|
|
307
|
-
.secured-
|
|
370
|
+
.secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
|
|
371
|
+
.secured-link:hover { text-decoration: underline; }
|
|
308
372
|
/* Loading overlay */
|
|
309
373
|
#authon-loading-overlay {
|
|
310
374
|
position: absolute; inset: 0; z-index: 10;
|
|
@@ -343,7 +407,26 @@ var ModalRenderer = class {
|
|
|
343
407
|
${b.customCss || ""}
|
|
344
408
|
`;
|
|
345
409
|
}
|
|
346
|
-
|
|
410
|
+
// ── Event binding ──
|
|
411
|
+
/** Attach events to shell elements (backdrop, ESC) — called once */
|
|
412
|
+
attachShellEvents() {
|
|
413
|
+
if (!this.shadowRoot) return;
|
|
414
|
+
const backdrop = this.shadowRoot.getElementById("backdrop");
|
|
415
|
+
if (backdrop) {
|
|
416
|
+
backdrop.addEventListener("click", () => this.onClose());
|
|
417
|
+
}
|
|
418
|
+
if (this.escHandler) {
|
|
419
|
+
document.removeEventListener("keydown", this.escHandler);
|
|
420
|
+
}
|
|
421
|
+
if (this.mode === "popup") {
|
|
422
|
+
this.escHandler = (e) => {
|
|
423
|
+
if (e.key === "Escape") this.onClose();
|
|
424
|
+
};
|
|
425
|
+
document.addEventListener("keydown", this.escHandler);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Attach events to inner content (buttons, form, switch link) — called on each view */
|
|
429
|
+
attachInnerEvents(view) {
|
|
347
430
|
if (!this.shadowRoot) return;
|
|
348
431
|
this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
|
|
349
432
|
btn.addEventListener("click", () => {
|
|
@@ -370,19 +453,6 @@ var ModalRenderer = class {
|
|
|
370
453
|
this.open(view === "signIn" ? "signUp" : "signIn");
|
|
371
454
|
});
|
|
372
455
|
}
|
|
373
|
-
const backdrop = this.shadowRoot.getElementById("backdrop");
|
|
374
|
-
if (backdrop) {
|
|
375
|
-
backdrop.addEventListener("click", () => this.onClose());
|
|
376
|
-
}
|
|
377
|
-
if (this.mode === "popup") {
|
|
378
|
-
const handler = (e) => {
|
|
379
|
-
if (e.key === "Escape") {
|
|
380
|
-
this.onClose();
|
|
381
|
-
document.removeEventListener("keydown", handler);
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
document.addEventListener("keydown", handler);
|
|
385
|
-
}
|
|
386
456
|
}
|
|
387
457
|
};
|
|
388
458
|
|
|
@@ -592,7 +662,7 @@ var Authon = class {
|
|
|
592
662
|
async startOAuthFlow(provider) {
|
|
593
663
|
try {
|
|
594
664
|
const redirectUri = `${this.config.apiUrl}/v1/auth/oauth/redirect`;
|
|
595
|
-
const { url } = await this.apiGet(
|
|
665
|
+
const { url, state } = await this.apiGet(
|
|
596
666
|
`/v1/auth/oauth/${provider}/url?redirectUri=${encodeURIComponent(redirectUri)}`
|
|
597
667
|
);
|
|
598
668
|
this.modal?.showLoading();
|
|
@@ -605,54 +675,89 @@ var Authon = class {
|
|
|
605
675
|
"authon-oauth",
|
|
606
676
|
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no`
|
|
607
677
|
);
|
|
608
|
-
|
|
678
|
+
if (!popup || popup.closed) {
|
|
679
|
+
this.modal?.hideLoading();
|
|
680
|
+
this.modal?.showBanner(
|
|
681
|
+
"Pop-up blocked. Please allow pop-ups for this site and try again.",
|
|
682
|
+
"warning"
|
|
683
|
+
);
|
|
684
|
+
this.emit("error", new Error("Popup was blocked by the browser"));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
let resolved = false;
|
|
609
688
|
let cleaned = false;
|
|
689
|
+
const resolve = (tokens) => {
|
|
690
|
+
if (resolved) return;
|
|
691
|
+
resolved = true;
|
|
692
|
+
cleanup();
|
|
693
|
+
try {
|
|
694
|
+
if (!popup.closed) popup.close();
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
this.session.setSession(tokens);
|
|
698
|
+
this.modal?.close();
|
|
699
|
+
this.emit("signedIn", tokens.user);
|
|
700
|
+
};
|
|
701
|
+
const handleError = (msg) => {
|
|
702
|
+
if (resolved) return;
|
|
703
|
+
cleanup();
|
|
704
|
+
this.modal?.hideLoading();
|
|
705
|
+
this.modal?.showError(msg);
|
|
706
|
+
this.emit("error", new Error(msg));
|
|
707
|
+
};
|
|
610
708
|
const cleanup = () => {
|
|
611
709
|
if (cleaned) return;
|
|
612
710
|
cleaned = true;
|
|
613
|
-
window.removeEventListener("message",
|
|
614
|
-
|
|
711
|
+
window.removeEventListener("message", messageHandler);
|
|
712
|
+
if (apiPollTimer) clearInterval(apiPollTimer);
|
|
713
|
+
if (closePollTimer) clearInterval(closePollTimer);
|
|
615
714
|
if (maxTimer) clearTimeout(maxTimer);
|
|
616
715
|
};
|
|
617
|
-
const
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
this.modal?.hideLoading();
|
|
623
|
-
}, 1500);
|
|
716
|
+
const messageHandler = (e) => {
|
|
717
|
+
if (e.data?.type !== "authon-oauth-callback") return;
|
|
718
|
+
if (e.data.tokens) {
|
|
719
|
+
resolve(e.data.tokens);
|
|
720
|
+
}
|
|
624
721
|
};
|
|
625
|
-
window.addEventListener("
|
|
722
|
+
window.addEventListener("message", messageHandler);
|
|
723
|
+
const apiPollTimer = setInterval(async () => {
|
|
724
|
+
if (resolved || cleaned) return;
|
|
725
|
+
try {
|
|
726
|
+
const result = await this.apiGet(
|
|
727
|
+
`/v1/auth/oauth/poll?state=${encodeURIComponent(state)}`
|
|
728
|
+
);
|
|
729
|
+
if (result.status === "completed" && result.accessToken) {
|
|
730
|
+
resolve({
|
|
731
|
+
accessToken: result.accessToken,
|
|
732
|
+
refreshToken: result.refreshToken,
|
|
733
|
+
expiresIn: result.expiresIn,
|
|
734
|
+
user: result.user
|
|
735
|
+
});
|
|
736
|
+
} else if (result.status === "error") {
|
|
737
|
+
handleError(result.message || "Authentication failed");
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
}, 1500);
|
|
742
|
+
const closePollTimer = setInterval(() => {
|
|
743
|
+
if (resolved || cleaned) return;
|
|
744
|
+
try {
|
|
745
|
+
if (popup.closed) {
|
|
746
|
+
clearInterval(closePollTimer);
|
|
747
|
+
setTimeout(() => {
|
|
748
|
+
if (resolved || cleaned) return;
|
|
749
|
+
cleanup();
|
|
750
|
+
this.modal?.hideLoading();
|
|
751
|
+
}, 3e3);
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
}
|
|
755
|
+
}, 500);
|
|
626
756
|
const maxTimer = setTimeout(() => {
|
|
627
|
-
if (
|
|
757
|
+
if (resolved || cleaned) return;
|
|
628
758
|
cleanup();
|
|
629
759
|
this.modal?.hideLoading();
|
|
630
760
|
}, 18e4);
|
|
631
|
-
if (!popup) {
|
|
632
|
-
cleanup();
|
|
633
|
-
this.modal?.hideLoading();
|
|
634
|
-
this.emit("error", new Error("Popup was blocked by the browser"));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const handler = async (e) => {
|
|
638
|
-
if (e.data?.type !== "authon-oauth-callback") return;
|
|
639
|
-
callbackReceived = true;
|
|
640
|
-
cleanup();
|
|
641
|
-
try {
|
|
642
|
-
const tokens = await this.apiPost("/v1/auth/oauth/callback", {
|
|
643
|
-
code: e.data.code,
|
|
644
|
-
state: e.data.state,
|
|
645
|
-
provider
|
|
646
|
-
});
|
|
647
|
-
this.session.setSession(tokens);
|
|
648
|
-
this.modal?.close();
|
|
649
|
-
this.emit("signedIn", tokens.user);
|
|
650
|
-
} catch (err) {
|
|
651
|
-
this.modal?.hideLoading();
|
|
652
|
-
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
window.addEventListener("message", handler);
|
|
656
761
|
} catch (err) {
|
|
657
762
|
this.modal?.hideLoading();
|
|
658
763
|
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|