@authon/js 0.1.11 → 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.
Files changed (3) hide show
  1. package/dist/index.js +135 -76
  2. package/dist/index.mjs +135 -76
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -97,6 +97,7 @@ var ModalRenderer = class {
97
97
  theme;
98
98
  branding;
99
99
  enabledProviders = [];
100
+ currentView = "signIn";
100
101
  onProviderClick;
101
102
  onEmailSubmit;
102
103
  onClose;
@@ -120,9 +121,9 @@ var ModalRenderer = class {
120
121
  }
121
122
  open(view = "signIn") {
122
123
  if (this.shadowRoot && this.hostElement) {
123
- this.shadowRoot.innerHTML = this.buildHTML(view);
124
- this.attachEvents(view);
124
+ this.switchView(view);
125
125
  } else {
126
+ this.currentView = view;
126
127
  this.render(view);
127
128
  }
128
129
  }
@@ -155,13 +156,13 @@ var ModalRenderer = class {
155
156
  showBanner(message, type = "error") {
156
157
  if (!this.shadowRoot) return;
157
158
  this.clearBanner();
158
- const container = this.shadowRoot.querySelector(".modal-container");
159
- if (!container) return;
159
+ const inner = this.shadowRoot.getElementById("modal-inner");
160
+ if (!inner) return;
160
161
  const banner = document.createElement("div");
161
162
  banner.id = "authon-banner";
162
163
  banner.className = type === "warning" ? "banner-warning" : "error-msg";
163
164
  banner.textContent = message;
164
- container.insertBefore(banner, container.firstChild);
165
+ inner.insertBefore(banner, inner.firstChild);
165
166
  }
166
167
  clearBanner() {
167
168
  if (!this.shadowRoot) return;
@@ -190,6 +191,24 @@ var ModalRenderer = class {
190
191
  if (!this.shadowRoot) return;
191
192
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
192
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 ──
193
212
  render(view) {
194
213
  const host = document.createElement("div");
195
214
  host.setAttribute("data-authon-modal", "");
@@ -200,10 +219,26 @@ var ModalRenderer = class {
200
219
  this.containerElement.appendChild(host);
201
220
  }
202
221
  this.shadowRoot = host.attachShadow({ mode: "open" });
203
- this.shadowRoot.innerHTML = this.buildHTML(view);
204
- this.attachEvents(view);
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
+ `;
205
239
  }
206
- buildHTML(view) {
240
+ /** Inner content = everything inside the modal that changes per view */
241
+ buildInnerContent(view) {
207
242
  const b = this.branding;
208
243
  const isSignUp = view === "signUp";
209
244
  const title = isSignUp ? "Create your account" : "Welcome back";
@@ -231,21 +266,16 @@ var ModalRenderer = class {
231
266
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
232
267
  ${b.privacyUrl ? `<a href="${b.privacyUrl}" target="_blank">Privacy Policy</a>` : ""}
233
268
  </div>` : "";
234
- const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
235
269
  return `
236
- <style>${this.buildCSS()}</style>
237
- ${popupWrapper}
238
- <div class="modal-container" role="dialog" aria-modal="true">
239
- ${b.logoDataUrl ? `<img src="${b.logoDataUrl}" alt="Logo" class="logo" />` : ""}
240
- <h2 class="title">${title}</h2>
241
- ${b.brandName ? `<p class="brand-name">${b.brandName}</p>` : ""}
242
- <div class="providers">${providerButtons}</div>
243
- ${divider}
244
- ${emailForm}
245
- <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
246
- ${footer}
247
- ${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <span class="secured-brand">Authon</span></div>` : ""}
248
- </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>` : ""}
249
279
  `;
250
280
  }
251
281
  isDark() {
@@ -296,6 +326,9 @@ var ModalRenderer = class {
296
326
  position: ${this.mode === "popup" ? "fixed" : "relative"};
297
327
  ${this.mode === "popup" ? `box-shadow: 0 25px 50px -12px rgba(0,0,0,${dark ? "0.5" : "0.25"}); animation: slideIn 0.3s ease;` : ""}
298
328
  }
329
+ .modal-inner {
330
+ transition: opacity 0.14s ease, transform 0.14s ease;
331
+ }
299
332
  .logo { display: block; margin: 0 auto 16px; max-height: 48px; }
300
333
  .title { text-align: center; font-size: 24px; font-weight: 700; margin-bottom: 8px; color: var(--authon-text); }
301
334
  .brand-name { text-align: center; font-size: 14px; color: var(--authon-muted); margin-bottom: 24px; }
@@ -354,15 +387,15 @@ var ModalRenderer = class {
354
387
  .switch-view { text-align: center; margin-top: 16px; font-size: 13px; color: var(--authon-muted); }
355
388
  .switch-view a { color: var(--authon-primary-start); text-decoration: none; font-weight: 500; }
356
389
  .switch-view a:hover { text-decoration: underline; }
357
- .footer { text-align: center; margin-top: 16px; font-size: 12px; color: var(--authon-dim); }
390
+ .footer { text-align: center; margin-top: 12px; font-size: 12px; color: var(--authon-dim); }
358
391
  .footer a { color: var(--authon-dim); text-decoration: none; }
359
392
  .footer a:hover { text-decoration: underline; }
360
393
  .secured-by {
361
- text-align: center; margin-top: 20px; padding-top: 16px;
362
- border-top: 1px solid var(--authon-divider);
394
+ text-align: center; margin-top: 16px;
363
395
  font-size: 11px; color: var(--authon-dim);
364
396
  }
365
- .secured-brand { font-weight: 600; color: var(--authon-muted); }
397
+ .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
398
+ .secured-link:hover { text-decoration: underline; }
366
399
  /* Loading overlay */
367
400
  #authon-loading-overlay {
368
401
  position: absolute; inset: 0; z-index: 10;
@@ -401,7 +434,26 @@ var ModalRenderer = class {
401
434
  ${b.customCss || ""}
402
435
  `;
403
436
  }
404
- attachEvents(view) {
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) {
405
457
  if (!this.shadowRoot) return;
406
458
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
407
459
  btn.addEventListener("click", () => {
@@ -428,20 +480,6 @@ var ModalRenderer = class {
428
480
  this.open(view === "signIn" ? "signUp" : "signIn");
429
481
  });
430
482
  }
431
- const backdrop = this.shadowRoot.getElementById("backdrop");
432
- if (backdrop) {
433
- backdrop.addEventListener("click", () => this.onClose());
434
- }
435
- if (this.escHandler) {
436
- document.removeEventListener("keydown", this.escHandler);
437
- this.escHandler = null;
438
- }
439
- if (this.mode === "popup") {
440
- this.escHandler = (e) => {
441
- if (e.key === "Escape") this.onClose();
442
- };
443
- document.addEventListener("keydown", this.escHandler);
444
- }
445
483
  }
446
484
  };
447
485
 
@@ -651,7 +689,7 @@ var Authon = class {
651
689
  async startOAuthFlow(provider) {
652
690
  try {
653
691
  const redirectUri = `${this.config.apiUrl}/v1/auth/oauth/redirect`;
654
- const { url } = await this.apiGet(
692
+ const { url, state } = await this.apiGet(
655
693
  `/v1/auth/oauth/${provider}/url?redirectUri=${encodeURIComponent(redirectUri)}`
656
694
  );
657
695
  this.modal?.showLoading();
@@ -673,59 +711,80 @@ var Authon = class {
673
711
  this.emit("error", new Error("Popup was blocked by the browser"));
674
712
  return;
675
713
  }
676
- let callbackReceived = false;
714
+ let resolved = false;
677
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
+ };
678
735
  const cleanup = () => {
679
736
  if (cleaned) return;
680
737
  cleaned = true;
681
- window.removeEventListener("message", handler);
682
- if (pollTimer) clearInterval(pollTimer);
738
+ window.removeEventListener("message", messageHandler);
739
+ if (apiPollTimer) clearInterval(apiPollTimer);
740
+ if (closePollTimer) clearInterval(closePollTimer);
683
741
  if (maxTimer) clearTimeout(maxTimer);
684
742
  };
685
- const pollTimer = setInterval(() => {
686
- if (callbackReceived || cleaned) return;
743
+ const messageHandler = (e) => {
744
+ if (e.data?.type !== "authon-oauth-callback") return;
745
+ if (e.data.tokens) {
746
+ resolve(e.data.tokens);
747
+ }
748
+ };
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;
687
771
  try {
688
772
  if (popup.closed) {
773
+ clearInterval(closePollTimer);
689
774
  setTimeout(() => {
690
- if (callbackReceived || cleaned) return;
775
+ if (resolved || cleaned) return;
691
776
  cleanup();
692
777
  this.modal?.hideLoading();
693
- }, 500);
694
- clearInterval(pollTimer);
778
+ }, 3e3);
695
779
  }
696
780
  } catch {
697
781
  }
698
782
  }, 500);
699
783
  const maxTimer = setTimeout(() => {
700
- if (callbackReceived || cleaned) return;
784
+ if (resolved || cleaned) return;
701
785
  cleanup();
702
786
  this.modal?.hideLoading();
703
787
  }, 18e4);
704
- const handler = async (e) => {
705
- if (e.data?.type !== "authon-oauth-callback") return;
706
- callbackReceived = true;
707
- cleanup();
708
- try {
709
- if (!popup.closed) popup.close();
710
- } catch {
711
- }
712
- try {
713
- const tokens = await this.apiPost("/v1/auth/oauth/callback", {
714
- code: e.data.code,
715
- state: e.data.state,
716
- provider
717
- });
718
- this.session.setSession(tokens);
719
- this.modal?.close();
720
- this.emit("signedIn", tokens.user);
721
- } catch (err) {
722
- this.modal?.hideLoading();
723
- const msg = err instanceof Error ? err.message : String(err);
724
- this.modal?.showError(msg.includes("500") ? "Authentication failed. Please try again." : msg);
725
- this.emit("error", err instanceof Error ? err : new Error(msg));
726
- }
727
- };
728
- window.addEventListener("message", handler);
729
788
  } catch (err) {
730
789
  this.modal?.hideLoading();
731
790
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
package/dist/index.mjs CHANGED
@@ -70,6 +70,7 @@ var ModalRenderer = class {
70
70
  theme;
71
71
  branding;
72
72
  enabledProviders = [];
73
+ currentView = "signIn";
73
74
  onProviderClick;
74
75
  onEmailSubmit;
75
76
  onClose;
@@ -93,9 +94,9 @@ var ModalRenderer = class {
93
94
  }
94
95
  open(view = "signIn") {
95
96
  if (this.shadowRoot && this.hostElement) {
96
- this.shadowRoot.innerHTML = this.buildHTML(view);
97
- this.attachEvents(view);
97
+ this.switchView(view);
98
98
  } else {
99
+ this.currentView = view;
99
100
  this.render(view);
100
101
  }
101
102
  }
@@ -128,13 +129,13 @@ var ModalRenderer = class {
128
129
  showBanner(message, type = "error") {
129
130
  if (!this.shadowRoot) return;
130
131
  this.clearBanner();
131
- const container = this.shadowRoot.querySelector(".modal-container");
132
- if (!container) return;
132
+ const inner = this.shadowRoot.getElementById("modal-inner");
133
+ if (!inner) return;
133
134
  const banner = document.createElement("div");
134
135
  banner.id = "authon-banner";
135
136
  banner.className = type === "warning" ? "banner-warning" : "error-msg";
136
137
  banner.textContent = message;
137
- container.insertBefore(banner, container.firstChild);
138
+ inner.insertBefore(banner, inner.firstChild);
138
139
  }
139
140
  clearBanner() {
140
141
  if (!this.shadowRoot) return;
@@ -163,6 +164,24 @@ var ModalRenderer = class {
163
164
  if (!this.shadowRoot) return;
164
165
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
165
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 ──
166
185
  render(view) {
167
186
  const host = document.createElement("div");
168
187
  host.setAttribute("data-authon-modal", "");
@@ -173,10 +192,26 @@ var ModalRenderer = class {
173
192
  this.containerElement.appendChild(host);
174
193
  }
175
194
  this.shadowRoot = host.attachShadow({ mode: "open" });
176
- this.shadowRoot.innerHTML = this.buildHTML(view);
177
- this.attachEvents(view);
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
+ `;
178
212
  }
179
- buildHTML(view) {
213
+ /** Inner content = everything inside the modal that changes per view */
214
+ buildInnerContent(view) {
180
215
  const b = this.branding;
181
216
  const isSignUp = view === "signUp";
182
217
  const title = isSignUp ? "Create your account" : "Welcome back";
@@ -204,21 +239,16 @@ var ModalRenderer = class {
204
239
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
205
240
  ${b.privacyUrl ? `<a href="${b.privacyUrl}" target="_blank">Privacy Policy</a>` : ""}
206
241
  </div>` : "";
207
- const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
208
242
  return `
209
- <style>${this.buildCSS()}</style>
210
- ${popupWrapper}
211
- <div class="modal-container" role="dialog" aria-modal="true">
212
- ${b.logoDataUrl ? `<img src="${b.logoDataUrl}" alt="Logo" class="logo" />` : ""}
213
- <h2 class="title">${title}</h2>
214
- ${b.brandName ? `<p class="brand-name">${b.brandName}</p>` : ""}
215
- <div class="providers">${providerButtons}</div>
216
- ${divider}
217
- ${emailForm}
218
- <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
219
- ${footer}
220
- ${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <span class="secured-brand">Authon</span></div>` : ""}
221
- </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>` : ""}
222
252
  `;
223
253
  }
224
254
  isDark() {
@@ -269,6 +299,9 @@ var ModalRenderer = class {
269
299
  position: ${this.mode === "popup" ? "fixed" : "relative"};
270
300
  ${this.mode === "popup" ? `box-shadow: 0 25px 50px -12px rgba(0,0,0,${dark ? "0.5" : "0.25"}); animation: slideIn 0.3s ease;` : ""}
271
301
  }
302
+ .modal-inner {
303
+ transition: opacity 0.14s ease, transform 0.14s ease;
304
+ }
272
305
  .logo { display: block; margin: 0 auto 16px; max-height: 48px; }
273
306
  .title { text-align: center; font-size: 24px; font-weight: 700; margin-bottom: 8px; color: var(--authon-text); }
274
307
  .brand-name { text-align: center; font-size: 14px; color: var(--authon-muted); margin-bottom: 24px; }
@@ -327,15 +360,15 @@ var ModalRenderer = class {
327
360
  .switch-view { text-align: center; margin-top: 16px; font-size: 13px; color: var(--authon-muted); }
328
361
  .switch-view a { color: var(--authon-primary-start); text-decoration: none; font-weight: 500; }
329
362
  .switch-view a:hover { text-decoration: underline; }
330
- .footer { text-align: center; margin-top: 16px; font-size: 12px; color: var(--authon-dim); }
363
+ .footer { text-align: center; margin-top: 12px; font-size: 12px; color: var(--authon-dim); }
331
364
  .footer a { color: var(--authon-dim); text-decoration: none; }
332
365
  .footer a:hover { text-decoration: underline; }
333
366
  .secured-by {
334
- text-align: center; margin-top: 20px; padding-top: 16px;
335
- border-top: 1px solid var(--authon-divider);
367
+ text-align: center; margin-top: 16px;
336
368
  font-size: 11px; color: var(--authon-dim);
337
369
  }
338
- .secured-brand { font-weight: 600; color: var(--authon-muted); }
370
+ .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
371
+ .secured-link:hover { text-decoration: underline; }
339
372
  /* Loading overlay */
340
373
  #authon-loading-overlay {
341
374
  position: absolute; inset: 0; z-index: 10;
@@ -374,7 +407,26 @@ var ModalRenderer = class {
374
407
  ${b.customCss || ""}
375
408
  `;
376
409
  }
377
- attachEvents(view) {
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) {
378
430
  if (!this.shadowRoot) return;
379
431
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
380
432
  btn.addEventListener("click", () => {
@@ -401,20 +453,6 @@ var ModalRenderer = class {
401
453
  this.open(view === "signIn" ? "signUp" : "signIn");
402
454
  });
403
455
  }
404
- const backdrop = this.shadowRoot.getElementById("backdrop");
405
- if (backdrop) {
406
- backdrop.addEventListener("click", () => this.onClose());
407
- }
408
- if (this.escHandler) {
409
- document.removeEventListener("keydown", this.escHandler);
410
- this.escHandler = null;
411
- }
412
- if (this.mode === "popup") {
413
- this.escHandler = (e) => {
414
- if (e.key === "Escape") this.onClose();
415
- };
416
- document.addEventListener("keydown", this.escHandler);
417
- }
418
456
  }
419
457
  };
420
458
 
@@ -624,7 +662,7 @@ var Authon = class {
624
662
  async startOAuthFlow(provider) {
625
663
  try {
626
664
  const redirectUri = `${this.config.apiUrl}/v1/auth/oauth/redirect`;
627
- const { url } = await this.apiGet(
665
+ const { url, state } = await this.apiGet(
628
666
  `/v1/auth/oauth/${provider}/url?redirectUri=${encodeURIComponent(redirectUri)}`
629
667
  );
630
668
  this.modal?.showLoading();
@@ -646,59 +684,80 @@ var Authon = class {
646
684
  this.emit("error", new Error("Popup was blocked by the browser"));
647
685
  return;
648
686
  }
649
- let callbackReceived = false;
687
+ let resolved = false;
650
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
+ };
651
708
  const cleanup = () => {
652
709
  if (cleaned) return;
653
710
  cleaned = true;
654
- window.removeEventListener("message", handler);
655
- if (pollTimer) clearInterval(pollTimer);
711
+ window.removeEventListener("message", messageHandler);
712
+ if (apiPollTimer) clearInterval(apiPollTimer);
713
+ if (closePollTimer) clearInterval(closePollTimer);
656
714
  if (maxTimer) clearTimeout(maxTimer);
657
715
  };
658
- const pollTimer = setInterval(() => {
659
- if (callbackReceived || cleaned) return;
716
+ const messageHandler = (e) => {
717
+ if (e.data?.type !== "authon-oauth-callback") return;
718
+ if (e.data.tokens) {
719
+ resolve(e.data.tokens);
720
+ }
721
+ };
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;
660
744
  try {
661
745
  if (popup.closed) {
746
+ clearInterval(closePollTimer);
662
747
  setTimeout(() => {
663
- if (callbackReceived || cleaned) return;
748
+ if (resolved || cleaned) return;
664
749
  cleanup();
665
750
  this.modal?.hideLoading();
666
- }, 500);
667
- clearInterval(pollTimer);
751
+ }, 3e3);
668
752
  }
669
753
  } catch {
670
754
  }
671
755
  }, 500);
672
756
  const maxTimer = setTimeout(() => {
673
- if (callbackReceived || cleaned) return;
757
+ if (resolved || cleaned) return;
674
758
  cleanup();
675
759
  this.modal?.hideLoading();
676
760
  }, 18e4);
677
- const handler = async (e) => {
678
- if (e.data?.type !== "authon-oauth-callback") return;
679
- callbackReceived = true;
680
- cleanup();
681
- try {
682
- if (!popup.closed) popup.close();
683
- } catch {
684
- }
685
- try {
686
- const tokens = await this.apiPost("/v1/auth/oauth/callback", {
687
- code: e.data.code,
688
- state: e.data.state,
689
- provider
690
- });
691
- this.session.setSession(tokens);
692
- this.modal?.close();
693
- this.emit("signedIn", tokens.user);
694
- } catch (err) {
695
- this.modal?.hideLoading();
696
- const msg = err instanceof Error ? err.message : String(err);
697
- this.modal?.showError(msg.includes("500") ? "Authentication failed. Please try again." : msg);
698
- this.emit("error", err instanceof Error ? err : new Error(msg));
699
- }
700
- };
701
- window.addEventListener("message", handler);
702
761
  } catch (err) {
703
762
  this.modal?.hideLoading();
704
763
  this.emit("error", err instanceof Error ? err : new Error(String(err)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authon/js",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Authon core SDK — ShadowDOM login modal for any app",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",