@zoreal/oauth2-react 0.2.20 → 0.2.22

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/README.md CHANGED
@@ -89,22 +89,15 @@ import { ZorealOAuthProvider } from '@zoreal/oauth2-react';
89
89
  ```
90
90
 
91
91
  ```tsx
92
- // ZorealSignIn.tsx — your own button, the way a production sign-in page uses it.
93
- import { useState } from 'react';
94
- import { useZorealLogin, ZorealBusyRing, ZorealMark } from '@zoreal/oauth2-react';
92
+ // ZorealSignIn.tsx — your own button, styled your way.
93
+ import { useZorealLogin, ZorealMark } from '@zoreal/oauth2-react';
95
94
 
96
95
  function ZorealSignIn({ onSignedIn }: { onSignedIn: () => void }) {
97
- // Busy from the tap until the flow ends. On a computer that is while the
98
- // pairing modal is open; on a phone it is the moment between the tap and
99
- // the hand-over to the ZOREAL ID app. Cleared by every outcome below.
100
- const [busy, setBusy] = useState(false);
101
-
102
96
  // `email` (and profile.name, etc.) are returned from /userinfo on your backend.
103
97
  const login = useZorealLogin({
104
98
  flow: 'auth-code',
105
99
  scope: 'openid email profile.name',
106
100
  onSuccess: async ({ code, code_verifier, nonce }) => {
107
- setBusy(false);
108
101
  // Send ALL THREE to your backend over TLS. It calls POST /token with the
109
102
  // code and verifier plus its client authentication, verifies the ID
110
103
  // token's nonce is this one, then reads the email and name from
@@ -116,12 +109,8 @@ function ZorealSignIn({ onSignedIn }: { onSignedIn: () => void }) {
116
109
  });
117
110
  if (res.ok) onSignedIn();
118
111
  },
119
- onError: (e) => {
120
- setBusy(false);
121
- console.error(e.description ?? e.error);
122
- },
112
+ onError: (e) => console.error(e.description ?? e.error),
123
113
  onNonOAuthError: (e) => {
124
- setBusy(false);
125
114
  // The holder declining or ignoring the request, or closing the dialog,
126
115
  // is not an error to surface: the button is simply ready again.
127
116
  if (e.type === 'request_denied' || e.type === 'request_expired' || e.type === 'popup_closed') return;
@@ -129,36 +118,26 @@ function ZorealSignIn({ onSignedIn }: { onSignedIn: () => void }) {
129
118
  },
130
119
  });
131
120
 
121
+ // Pass the click through as it is. The SDK takes the button from it, holds it
122
+ // busy with the pairing modal's light round it until the login ends, and lets
123
+ // it go on every outcome. No wrapper, no busy state of your own.
132
124
  return (
133
- // The wrapper runs the pairing modal's light around the button while busy.
134
- // `block` because this button fills its row; `radius` is the button's own.
135
- <ZorealBusyRing busy={busy} radius={12} block>
136
- <button
137
- type="button"
138
- disabled={busy}
139
- aria-busy={busy}
140
- onClick={() => {
141
- if (busy) return;
142
- setBusy(true);
143
- login(); // from the click handler itself: on a phone this tap is the navigation
144
- }}
145
- >
146
- <ZorealMark size={22} brand />
147
- Continue with ZOREAL
148
- </button>
149
- </ZorealBusyRing>
125
+ <button type="button" onClick={login}>
126
+ <ZorealMark size={22} brand />
127
+ Continue with ZOREAL
128
+ </button>
150
129
  );
151
130
  }
152
131
  ```
153
132
 
154
133
  That is the whole integration. When `login()` runs on a computer, the provider
155
- puts the pairing modal on screen. On a phone the tap itself navigates to the
156
- provider, which opens the ZOREAL ID app; once the person has approved, the app
157
- brings them back to this page, and the same hook finishes the sign-in and calls
158
- your `onSuccess` there. So mount this component on the page the sign-in starts
159
- from, and expect `onSuccess` on a fresh page load. See
160
- [The pairing modal](#the-pairing-modal) for what it does and how to theme,
161
- translate, time out or replace it.
134
+ puts the pairing modal on screen and the button you tapped waits, disabled, with
135
+ the light round it. On a phone the tap itself navigates to the provider, which
136
+ opens the ZOREAL ID app; once the person has approved, the app brings them back
137
+ to this page, and the same hook finishes the sign-in and calls your `onSuccess`
138
+ there. So mount this component on the page the sign-in starts from, and expect
139
+ `onSuccess` on a fresh page load. See [The pairing modal](#the-pairing-modal)
140
+ for what it does and how to theme, translate, time out or replace it.
162
141
 
163
142
  ## Quick start: the button (no backend, pseudonymous)
164
143
 
@@ -286,7 +265,7 @@ What the modal does:
286
265
 
287
266
  | | |
288
267
  | --- | --- |
289
- | **Mobile** | No QR and no modal. The tap itself is a navigation: the SDK sends the tab to the provider's `/pair/start` with the pairing's parameters, synchronously from the click, and the provider answers with a redirect to the pairing's universal link, which the ZOREAL ID app claims while the page stays put and polls. A browser hands a link to an app only inside a navigation the person began, which is why nothing is fetched first. With no app installed the same redirect lands on the page that installs it. Call `login()` from the click handler itself: `ZorealLogin` does, disables itself and runs a light round its edge until the flow ends. Once the holder has approved, the app reopens your page with the pairing named in the fragment, and the first `useZorealLogin` or `ZorealLogin` on that page finishes the sign-in there, through the same `onSuccess` and `onError`; the tab that was left behind stands down when the returned page finishes first; a site with its own button keeps that button and draws its own busy state from the tap until `onSuccess` or `onError` fires, or wraps it in `ZorealBusyRing` to get the same light (a wrapper: pass `block` for a full-width button, keep `overflow: hidden` off its ancestors, and expect `.parent > button` selectors to stop matching). Force one or the other with `display: 'qr'` / `'link'`. |
268
+ | **Mobile** | No QR and no modal. The tap itself is a navigation: the SDK sends the tab to the provider's `/pair/start` with the pairing's parameters, synchronously from the click, and the provider answers with a redirect to the pairing's universal link, which the ZOREAL ID app claims while the page stays put and polls. A browser hands a link to an app only inside a navigation the person began, which is why nothing is fetched first. With no app installed the same redirect lands on the page that installs it. Pass the click event through to `login` (`onClick={login}`): the SDK takes the button from it, disables it and runs the light round it until the flow ends, then lets it go, on every outcome. `ZorealLogin` does the same for itself. Once the holder has approved, the app reopens your page with the pairing named in the fragment, and the first `useZorealLogin` or `ZorealLogin` on that page finishes the sign-in there, through the same `onSuccess` and `onError`; the tab that was left behind stands down when the returned page finishes first; a site with its own button gets the same by passing the click to `login`; `ZorealBusyRing` remains for a site that wants to drive the light itself (a wrapper: pass `block` for a full-width button, keep `overflow: hidden` off its ancestors, and expect `.parent > button` selectors to stop matching). Force one or the other with `display: 'qr'` / `'link'`. |
290
269
  | **Live status** | The copy and the title follow the pairing: waiting for a scan, then waiting for approval once the holder has claimed the code (the spent QR blurs out behind a phone glyph). |
291
270
  | **Title** | Says what the scan is for, inferred from the request: "Scan to sign in" for `openid`, `email` and `profile.name`; "Scan to verify your identity" once a document attribute such as `zoreal.age` or `profile.birthdate` is requested; "Scan to prove you are a real human" for `openid` alone with `acr_values: 'zoreal.live'`. Override with `intent`, one of `'sign-in'`, `'identify'`, `'presence'`, when the scope does not say. |
292
271
  | **Countdown** | Counts down to expiry, turning amber under 20s. Reads the clock each tick rather than decrementing, so a backgrounded tab comes back honest. |
@@ -613,18 +592,17 @@ error path.
613
592
  ## A complete example
614
593
 
615
594
  A full sign-in component, end to end, the shape a production auth-code
616
- integration takes: your own button, busy from the tap until the flow ends,
617
- the SDK's pairing modal on a computer and the app hand-over on a phone,
618
- `{ code, code_verifier, nonce }` to your backend on success, the human outcomes
619
- treated as the non-events they are, and the return from the app on a phone
620
- handled by the same hook.
595
+ integration takes: your own button, held busy by the SDK from the tap until
596
+ the flow ends, the SDK's pairing modal on a computer and the app hand-over on
597
+ a phone, `{ code, code_verifier, nonce }` to your backend on success, the human
598
+ outcomes treated as the non-events they are, and the return from the app on a
599
+ phone handled by the same hook.
621
600
 
622
601
  ```tsx
623
602
  import { useState } from 'react';
624
- import { ZorealOAuthProvider, useZorealLogin, ZorealBusyRing, ZorealMark } from '@zoreal/oauth2-react';
603
+ import { ZorealOAuthProvider, useZorealLogin, ZorealMark } from '@zoreal/oauth2-react';
625
604
 
626
605
  function ZorealSignIn() {
627
- const [busy, setBusy] = useState(false);
628
606
  const [note, setNote] = useState<string | null>(null);
629
607
 
630
608
  const login = useZorealLogin({
@@ -633,7 +611,6 @@ function ZorealSignIn() {
633
611
  // acr_values: 'zoreal.live', // request a fresh liveness for a step-up / high-value login
634
612
 
635
613
  onSuccess: async ({ code, code_verifier, nonce }) => {
636
- setBusy(false);
637
614
  // Post ALL THREE to YOUR backend over TLS. Your backend does the /token
638
615
  // exchange with its client authentication, verifies the ID token
639
616
  // (ES256 against the JWKS, iss/aud/exp, and this nonce), checks the acr
@@ -654,15 +631,11 @@ function ZorealSignIn() {
654
631
  },
655
632
 
656
633
  // An OAuth error from the provider (e.g. a scope not on your allow list).
657
- onError: (e) => {
658
- setBusy(false);
659
- setNote(e.description ?? e.error); // the provider's words, verbatim
660
- },
634
+ onError: (e) => setNote(e.description ?? e.error), // the provider's words, verbatim
661
635
 
662
636
  // The human outcomes: declined, expired, the dialog closed. Not faults:
663
637
  // the button is ready again. Do not alarm on these.
664
638
  onNonOAuthError: (e) => {
665
- setBusy(false);
666
639
  if (e.type === 'request_denied') setNote('Login was declined. Try again when ready.');
667
640
  else if (e.type === 'request_expired') setNote('That took too long. Try again.');
668
641
  else if (e.type === 'popup_closed') setNote(null);
@@ -672,21 +645,12 @@ function ZorealSignIn() {
672
645
 
673
646
  return (
674
647
  <div>
675
- <ZorealBusyRing busy={busy} radius={12} block>
676
- <button
677
- type="button"
678
- disabled={busy}
679
- aria-busy={busy}
680
- onClick={() => {
681
- if (busy) return;
682
- setBusy(true);
683
- login();
684
- }}
685
- >
686
- <ZorealMark size={22} brand />
687
- Continue with ZOREAL
688
- </button>
689
- </ZorealBusyRing>
648
+ {/* The click goes through as it is: the SDK holds this button busy, with
649
+ the light round it, until the login ends. */}
650
+ <button type="button" onClick={login}>
651
+ <ZorealMark size={22} brand />
652
+ Continue with ZOREAL
653
+ </button>
690
654
 
691
655
  {note && <p role="status">{note}</p>}
692
656
  </div>
package/dist/index.cjs CHANGED
@@ -42,7 +42,7 @@ var import_react2 = require("react");
42
42
 
43
43
  // src/wire.ts
44
44
  var WIRE_VERSION = 1;
45
- var SDK_VERSION = "0.2.20";
45
+ var SDK_VERSION = "0.2.22";
46
46
  var DEFAULT_ISSUER = "https://id.zoreal.com";
47
47
  var POLL_INTERVAL_MS = 2e3;
48
48
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1397,6 +1397,16 @@ var CSS = `
1397
1397
  transition: opacity 200ms ease-out;
1398
1398
  }
1399
1399
  .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1400
+ /* The same light as an overlay in the document body, placed over a site's
1401
+ own control by the package itself (busy.ts): nothing of the site's
1402
+ markup or CSS is touched, and neither an ancestor's overflow nor a
1403
+ selector on the control's parent is affected. */
1404
+ .${PREFIX}-ring-overlay {
1405
+ position: fixed;
1406
+ display: block;
1407
+ z-index: 2147483000;
1408
+ pointer-events: none;
1409
+ }
1400
1410
  .${PREFIX}-ring-svg rect {
1401
1411
  --zrl-l: var(--zrl-ring-len, 600px);
1402
1412
  x: 2px;
@@ -1632,11 +1642,101 @@ function useZorealOAuth() {
1632
1642
  }
1633
1643
 
1634
1644
  // src/ZorealLogin.tsx
1635
- var import_react5 = require("react");
1645
+ var import_react4 = require("react");
1636
1646
 
1637
1647
  // src/useZorealLogin.ts
1638
1648
  var import_react3 = require("react");
1639
1649
 
1650
+ // src/busy.ts
1651
+ var SVG_NS = "http://www.w3.org/2000/svg";
1652
+ var TAIL = 0.3;
1653
+ var STACK = Array.from({ length: 12 }, (_, i) => 12 - i);
1654
+ var HALO = [3, 2, 1];
1655
+ function radiusOf(control) {
1656
+ const value = parseFloat(getComputedStyle(control).borderTopLeftRadius);
1657
+ return Number.isFinite(value) ? value : 8;
1658
+ }
1659
+ function holdBusy(control) {
1660
+ if (typeof document === "undefined") return () => {
1661
+ };
1662
+ ensureStyles();
1663
+ const hadDisabled = control.hasAttribute("disabled");
1664
+ const hadBusy = control.getAttribute("aria-busy");
1665
+ if (control instanceof HTMLButtonElement || control instanceof HTMLInputElement) {
1666
+ control.disabled = true;
1667
+ } else {
1668
+ control.setAttribute("aria-disabled", "true");
1669
+ }
1670
+ control.setAttribute("aria-busy", "true");
1671
+ const overlay = document.createElement("div");
1672
+ overlay.className = `${cx("root")} ${cx("ring")} ${cx("ring-overlay")}`;
1673
+ overlay.dataset.theme = "auto";
1674
+ overlay.dataset.busy = "true";
1675
+ overlay.setAttribute("aria-hidden", "true");
1676
+ const svg = document.createElementNS(SVG_NS, "svg");
1677
+ svg.setAttribute("class", cx("ring-svg"));
1678
+ const rx = radiusOf(control) + 2;
1679
+ const layer = (name, len, alpha) => {
1680
+ const rect = document.createElementNS(SVG_NS, "rect");
1681
+ rect.setAttribute("class", cx(name));
1682
+ rect.setAttribute("rx", String(rx));
1683
+ rect.setAttribute("ry", String(rx));
1684
+ rect.style.strokeDasharray = `calc(var(--zrl-l) * ${len}) calc(var(--zrl-l) * ${1 - len})`;
1685
+ rect.style.setProperty("--zrl-s", `calc(var(--zrl-l) * ${-(TAIL - len)})`);
1686
+ rect.style.opacity = alpha;
1687
+ svg.appendChild(rect);
1688
+ return rect;
1689
+ };
1690
+ let measured = null;
1691
+ for (const n of HALO) {
1692
+ const rect = layer("ring-halo", TAIL * n / HALO.length, `calc(var(--zrl-glow-opacity) * 0.6 / ${n})`);
1693
+ measured ?? (measured = rect);
1694
+ }
1695
+ for (const n of STACK) layer(n <= 2 ? "ring-head" : "ring-tail", TAIL * n / STACK.length, String(1 / n));
1696
+ overlay.appendChild(svg);
1697
+ document.body.appendChild(overlay);
1698
+ let frame = 0;
1699
+ const place = () => {
1700
+ frame = 0;
1701
+ const box = control.getBoundingClientRect();
1702
+ overlay.style.left = `${box.left}px`;
1703
+ overlay.style.top = `${box.top}px`;
1704
+ overlay.style.width = `${box.width}px`;
1705
+ overlay.style.height = `${box.height}px`;
1706
+ const length = typeof measured?.getTotalLength === "function" ? measured.getTotalLength() : 0;
1707
+ if (length > 0) overlay.style.setProperty("--zrl-ring-len", `${length}px`);
1708
+ };
1709
+ const schedule = () => {
1710
+ if (!frame) frame = requestAnimationFrame(place);
1711
+ };
1712
+ place();
1713
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
1714
+ observer?.observe(control);
1715
+ window.addEventListener("scroll", schedule, true);
1716
+ window.addEventListener("resize", schedule);
1717
+ let released = false;
1718
+ return () => {
1719
+ if (released) return;
1720
+ released = true;
1721
+ if (frame) cancelAnimationFrame(frame);
1722
+ observer?.disconnect();
1723
+ window.removeEventListener("scroll", schedule, true);
1724
+ window.removeEventListener("resize", schedule);
1725
+ overlay.remove();
1726
+ if (control instanceof HTMLButtonElement || control instanceof HTMLInputElement) {
1727
+ control.disabled = hadDisabled;
1728
+ } else {
1729
+ control.removeAttribute("aria-disabled");
1730
+ }
1731
+ if (hadBusy === null) control.removeAttribute("aria-busy");
1732
+ else control.setAttribute("aria-busy", hadBusy);
1733
+ };
1734
+ }
1735
+ function controlFrom(event) {
1736
+ const target = event?.currentTarget;
1737
+ return typeof HTMLElement !== "undefined" && target instanceof HTMLElement ? target : null;
1738
+ }
1739
+
1640
1740
  // src/return.ts
1641
1741
  var PREFIX2 = "zoreal:oauth2:return:";
1642
1742
  var DONE = "zoreal:oauth2:done:";
@@ -2101,6 +2201,7 @@ function useZorealFlow(options) {
2101
2201
  () => () => {
2102
2202
  abortRef.current?.abort();
2103
2203
  publishRef.current?.(null);
2204
+ releaseRef.current();
2104
2205
  },
2105
2206
  []
2106
2207
  );
@@ -2159,10 +2260,21 @@ function useZorealFlow(options) {
2159
2260
  }
2160
2261
  })();
2161
2262
  }, [clientId, issuer]);
2162
- const login = (0, import_react3.useCallback)(() => {
2263
+ const releaseRef = (0, import_react3.useRef)(() => {
2264
+ });
2265
+ const login = (0, import_react3.useCallback)((event) => {
2163
2266
  const opts = optionsRef.current;
2267
+ const control = controlFrom(event);
2164
2268
  const run = async () => {
2165
2269
  abortRef.current?.abort();
2270
+ releaseRef.current();
2271
+ releaseRef.current = control ? holdBusy(control) : () => {
2272
+ };
2273
+ const release = () => {
2274
+ releaseRef.current();
2275
+ releaseRef.current = () => {
2276
+ };
2277
+ };
2166
2278
  const controller = new AbortController();
2167
2279
  abortRef.current = controller;
2168
2280
  const flow = opts.flow;
@@ -2311,6 +2423,7 @@ function useZorealFlow(options) {
2311
2423
  setPairing(null);
2312
2424
  publishRef.current?.(null);
2313
2425
  if (returnId) markReturnDone(returnId);
2426
+ release();
2314
2427
  if (flow === "auth-code") {
2315
2428
  opts.onCode?.({
2316
2429
  code,
@@ -2335,6 +2448,7 @@ function useZorealFlow(options) {
2335
2448
  };
2336
2449
  opts.onCredential?.(response);
2337
2450
  } catch (e) {
2451
+ release();
2338
2452
  setPairing(null);
2339
2453
  publishRef.current?.(null);
2340
2454
  if (e instanceof DOMException && e.name === "AbortError") {
@@ -2380,79 +2494,8 @@ function useZorealLogin(options) {
2380
2494
  }).login;
2381
2495
  }
2382
2496
 
2383
- // src/ring.tsx
2384
- var import_react4 = require("react");
2385
- var import_jsx_runtime5 = require("react/jsx-runtime");
2386
- var TAIL = 0.3;
2387
- var STACK = Array.from({ length: 12 }, (_, i) => 12 - i);
2388
- var HALO = [3, 2, 1];
2389
- function ZorealBusyRing({
2390
- busy,
2391
- radius = 8,
2392
- theme = "auto",
2393
- block = false,
2394
- className,
2395
- style,
2396
- children
2397
- }) {
2398
- (0, import_react4.useEffect)(() => {
2399
- ensureStyles();
2400
- }, []);
2401
- const wrapRef = (0, import_react4.useRef)(null);
2402
- const measureRef = (0, import_react4.useRef)(null);
2403
- (0, import_react4.useLayoutEffect)(() => {
2404
- const wrap = wrapRef.current;
2405
- const rect = measureRef.current;
2406
- if (!wrap || !rect) return;
2407
- const measure = () => {
2408
- const length = rect.getTotalLength();
2409
- if (length > 0) wrap.style.setProperty("--zrl-ring-len", `${length}px`);
2410
- };
2411
- measure();
2412
- if (typeof ResizeObserver === "undefined") return;
2413
- const observer = new ResizeObserver(measure);
2414
- observer.observe(wrap);
2415
- return () => observer.disconnect();
2416
- }, [radius]);
2417
- const rx = radius + 2;
2418
- const layer = (key, name, len, alpha, ref) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2419
- "rect",
2420
- {
2421
- ref,
2422
- className: cx(name),
2423
- rx,
2424
- ry: rx,
2425
- style: {
2426
- strokeDasharray: `calc(var(--zrl-l) * ${len}) calc(var(--zrl-l) * ${1 - len})`,
2427
- "--zrl-s": `calc(var(--zrl-l) * ${-(TAIL - len)})`,
2428
- opacity: alpha
2429
- }
2430
- },
2431
- key
2432
- );
2433
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2434
- "span",
2435
- {
2436
- ref: wrapRef,
2437
- className: className ? `${cx("root")} ${cx("ring")} ${className}` : `${cx("root")} ${cx("ring")}`,
2438
- "data-theme": theme,
2439
- "data-busy": busy,
2440
- style: block ? { display: "flex", ...style } : style,
2441
- children: [
2442
- children,
2443
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { className: cx("ring-svg"), "aria-hidden": "true", children: [
2444
- HALO.map(
2445
- (n, i) => layer(`h${n}`, "ring-halo", TAIL * n / HALO.length, `calc(var(--zrl-glow-opacity) * 0.6 / ${n})`, i === 0 ? measureRef : void 0)
2446
- ),
2447
- STACK.map((n) => layer(`t${n}`, n <= 2 ? "ring-head" : "ring-tail", TAIL * n / STACK.length, String(1 / n)))
2448
- ] })
2449
- ]
2450
- }
2451
- );
2452
- }
2453
-
2454
2497
  // src/ZorealLogin.tsx
2455
- var import_jsx_runtime6 = require("react/jsx-runtime");
2498
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2456
2499
  var TEXTS = {
2457
2500
  continue_with: null,
2458
2501
  signin_with: "Sign in with ZOREAL",
@@ -2483,31 +2526,18 @@ function ZorealLogin(props) {
2483
2526
  } = props;
2484
2527
  const { locale } = useZorealOAuth();
2485
2528
  const label = TEXTS[text] ?? strings(locale).buttonContinue;
2486
- const [busy, setBusy] = (0, import_react5.useState)(false);
2487
2529
  const { login } = useZorealFlow({
2488
2530
  ...request,
2489
2531
  flow,
2490
- onCredential: flow === "browser-direct" ? (r) => {
2491
- setBusy(false);
2492
- onSuccess(r);
2493
- } : void 0,
2494
- onCode: flow === "auth-code" ? (r) => {
2495
- setBusy(false);
2496
- onSuccess(r);
2497
- } : void 0,
2498
- onError: (e) => {
2499
- setBusy(false);
2500
- onError?.({ type: "unknown", description: e.description ?? e.error });
2501
- },
2502
- onNonOAuthError: (e) => {
2503
- setBusy(false);
2504
- onError?.(e);
2505
- }
2532
+ onCredential: flow === "browser-direct" ? onSuccess : void 0,
2533
+ onCode: flow === "auth-code" ? onSuccess : void 0,
2534
+ onError: (e) => onError?.({ type: "unknown", description: e.description ?? e.error }),
2535
+ onNonOAuthError: (e) => onError?.(e)
2506
2536
  });
2507
2537
  const s = SIZES[size];
2508
2538
  const radius = shape === "pill" ? s.height / 2 : shape === "square" ? 4 : s.radius;
2509
2539
  const brandMark = theme !== "filled";
2510
- const style = (0, import_react5.useMemo)(
2540
+ const style = (0, import_react4.useMemo)(
2511
2541
  () => ({
2512
2542
  display: "inline-flex",
2513
2543
  alignItems: "center",
@@ -2525,25 +2555,92 @@ function ZorealLogin(props) {
2525
2555
  }),
2526
2556
  [logo_alignment, s, radius, theme, width]
2527
2557
  );
2528
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ...containerProps, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ZorealBusyRing, { busy, radius, theme: theme === "outline" ? "auto" : "light", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2558
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ...containerProps, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2529
2559
  "button",
2530
2560
  {
2531
2561
  type: "button",
2532
- style: busy ? { ...style, cursor: "progress" } : style,
2533
- disabled: busy,
2534
- "aria-busy": busy,
2535
- onClick: () => {
2536
- if (busy) return;
2562
+ style,
2563
+ onClick: (event) => {
2537
2564
  click_listener?.();
2538
- setBusy(true);
2539
- login();
2565
+ login(event);
2540
2566
  },
2541
2567
  children: [
2542
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ZorealMark, { size: s.mark, brand: brandMark }),
2568
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ZorealMark, { size: s.mark, brand: brandMark }),
2543
2569
  type === "standard" && label
2544
2570
  ]
2545
2571
  }
2546
- ) }) });
2572
+ ) });
2573
+ }
2574
+
2575
+ // src/ring.tsx
2576
+ var import_react5 = require("react");
2577
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2578
+ var TAIL2 = 0.3;
2579
+ var STACK2 = Array.from({ length: 12 }, (_, i) => 12 - i);
2580
+ var HALO2 = [3, 2, 1];
2581
+ function ZorealBusyRing({
2582
+ busy,
2583
+ radius = 8,
2584
+ theme = "auto",
2585
+ block = false,
2586
+ className,
2587
+ style,
2588
+ children
2589
+ }) {
2590
+ (0, import_react5.useEffect)(() => {
2591
+ ensureStyles();
2592
+ }, []);
2593
+ const wrapRef = (0, import_react5.useRef)(null);
2594
+ const measureRef = (0, import_react5.useRef)(null);
2595
+ (0, import_react5.useLayoutEffect)(() => {
2596
+ const wrap = wrapRef.current;
2597
+ const rect = measureRef.current;
2598
+ if (!wrap || !rect) return;
2599
+ const measure = () => {
2600
+ const length = rect.getTotalLength();
2601
+ if (length > 0) wrap.style.setProperty("--zrl-ring-len", `${length}px`);
2602
+ };
2603
+ measure();
2604
+ if (typeof ResizeObserver === "undefined") return;
2605
+ const observer = new ResizeObserver(measure);
2606
+ observer.observe(wrap);
2607
+ return () => observer.disconnect();
2608
+ }, [radius]);
2609
+ const rx = radius + 2;
2610
+ const layer = (key, name, len, alpha, ref) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2611
+ "rect",
2612
+ {
2613
+ ref,
2614
+ className: cx(name),
2615
+ rx,
2616
+ ry: rx,
2617
+ style: {
2618
+ strokeDasharray: `calc(var(--zrl-l) * ${len}) calc(var(--zrl-l) * ${1 - len})`,
2619
+ "--zrl-s": `calc(var(--zrl-l) * ${-(TAIL2 - len)})`,
2620
+ opacity: alpha
2621
+ }
2622
+ },
2623
+ key
2624
+ );
2625
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2626
+ "span",
2627
+ {
2628
+ ref: wrapRef,
2629
+ className: className ? `${cx("root")} ${cx("ring")} ${className}` : `${cx("root")} ${cx("ring")}`,
2630
+ "data-theme": theme,
2631
+ "data-busy": busy,
2632
+ style: block ? { display: "flex", ...style } : style,
2633
+ children: [
2634
+ children,
2635
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { className: cx("ring-svg"), "aria-hidden": "true", children: [
2636
+ HALO2.map(
2637
+ (n, i) => layer(`h${n}`, "ring-halo", TAIL2 * n / HALO2.length, `calc(var(--zrl-glow-opacity) * 0.6 / ${n})`, i === 0 ? measureRef : void 0)
2638
+ ),
2639
+ STACK2.map((n) => layer(`t${n}`, n <= 2 ? "ring-head" : "ring-tail", TAIL2 * n / STACK2.length, String(1 / n)))
2640
+ ] })
2641
+ ]
2642
+ }
2643
+ );
2547
2644
  }
2548
2645
 
2549
2646
  // src/useZorealAutoLogin.ts