@qrauth/web-components 0.1.0 → 0.3.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # @qrauth/web-components
2
+
3
+ ## 0.3.0
4
+
5
+ - `<qrauth-2fa>`: mobile-aware pending state. Primary CTA opens the hosted approval page (`/a/:token`) in a new tab; QR demoted to a "Use another device" expander. Polling in the original tab continues to fire `qrauth:verified` on approval. Adds `force-mode` and `mobile-fallback-only` attributes.
6
+ - `<qrauth-ephemeral>`: adds `force-mode` and `mobile-fallback-only` attributes for forward compatibility. No default mobile UI change — the ephemeral flow is a QR meant to be scanned by another device, so QR-first remains correct on all viewports.
7
+
8
+ ## 0.2.0 — 2026-04-18
9
+
10
+ ### Minor
11
+
12
+ - **`<qrauth-login>` — mobile-aware pending state.** Mobile-aware pending state: primary CTA opens the hosted approval page (`/a/:token`) in a new tab. QR demoted to a "Use another device" expander. Polling in the original tab continues to fire `qrauth:authenticated` on approval.
13
+ - **New attribute `force-mode="mobile" | "desktop" | "auto"`** — overrides the automatic detection. Useful for demos, screenshots, and host sites that know their audience's form factor.
14
+ - **New attribute `mobile-fallback-only`** — when present, disables the mobile-aware UI entirely and keeps the QR-first body on every device. Intended for host sites that already provide a same-device login path around the component; the QRAuth modal is then purely the cross-device scan option.
15
+ - **Shared `isMobileLike()` detector on `QRAuthElement`** — base-class helper so `<qrauth-2fa>` and `<qrauth-ephemeral>` can adopt the same branch without refactor when they grow analogous mobile-aware modes.
16
+ - **`MOBILE_MEDIA_QUERY` export** — `'(pointer: coarse) and (hover: none)'`, for documentation and test mocks.
17
+
18
+ ### Internal
19
+
20
+ - Added `vitest` + `happy-dom` test harness (`npm test` runs the new `tests/login.mobile.test.ts`).
21
+ - Added Playwright spec `packages/e2e/tests/qrauth-login-mobile.spec.ts` under an iPhone 13 viewport (chromium-emulated).
22
+ - Bundle size: 10.4 KB gzipped IIFE / 10.2 KB gzipped ESM (target was <12 KB).
23
+
24
+ ### Non-breaking
25
+
26
+ - Event surface (`qrauth:authenticated`, `qrauth:scanned`, `qrauth:expired`, `qrauth:denied`, `qrauth:error`) unchanged.
27
+ - Desktop pending body markup unchanged.
28
+ - Polling / SSE path unchanged — APPROVED transitions still fire `qrauth:authenticated` regardless of whether approval completed on the hosted page or via a scanned QR.
29
+
30
+ ## 0.1.0
31
+
32
+ - Initial release: `<qrauth-login>`, `<qrauth-2fa>`, `<qrauth-ephemeral>` Shadow DOM custom elements.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @qrauth/web-components
2
+
3
+ Drop-in Web Components for [QRAuth](https://qrauth.io) — passwordless QR authentication, 2FA, and ephemeral delegated access. Shadow DOM, framework-free, ~10 KB gzipped.
4
+
5
+ - `<qrauth-login>` — passwordless sign-in, button or inline. Mobile-aware pending state (tap-to-continue CTA with QR demoted under a "Use another device" expander).
6
+ - `<qrauth-2fa>` — drop-in second-factor step with session-token binding.
7
+ - `<qrauth-ephemeral>` — time-limited, device-bound access QRs.
8
+
9
+ All three components bubble `qrauth:*` events across the Shadow DOM boundary (`composed: true`) so any framework can listen with plain `addEventListener`.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @qrauth/web-components
15
+ ```
16
+
17
+ Or load the CDN IIFE directly in any HTML page:
18
+
19
+ ```html
20
+ <script
21
+ src="https://cdn.qrauth.io/v1/components.js"
22
+ integrity="sha384-..."
23
+ crossorigin="anonymous"
24
+ ></script>
25
+
26
+ <qrauth-login tenant="your-app-id"></qrauth-login>
27
+ ```
28
+
29
+ ## Quickstart
30
+
31
+ ```html
32
+ <qrauth-login
33
+ tenant="qrauth_app_xxx"
34
+ theme="light"
35
+ base-url="https://qrauth.io"
36
+ ></qrauth-login>
37
+
38
+ <script>
39
+ document.querySelector('qrauth-login').addEventListener('qrauth:authenticated', (e) => {
40
+ const { sessionId, user, signature } = e.detail;
41
+ // Exchange sessionId + signature server-side for your own session.
42
+ });
43
+ </script>
44
+ ```
45
+
46
+ ## Attributes (login)
47
+
48
+ | Attribute | Values | Default | Purpose |
49
+ |---|---|---|---|
50
+ | `tenant` | string | — | Your QRAuth App client ID. Required. |
51
+ | `theme` | `light` \| `dark` | `light` | Colour scheme. |
52
+ | `base-url` | URL | `https://qrauth.io` | API host. Override for staging / local dev. |
53
+ | `scopes` | space-separated | `identity` | OAuth-style scopes requested. |
54
+ | `redirect-url` | URL | — | Auto-redirect after success. |
55
+ | `on-auth` | string | — | Name of a global function to invoke on success. |
56
+ | `display` | `button` \| `inline` | `button` | Modal trigger vs. embedded widget. |
57
+ | `animated` | boolean | off | Enables the pulse animation on the QR. |
58
+ | `force-mode` | `mobile` \| `desktop` \| `auto` | `auto` | Override automatic mobile detection. |
59
+ | `mobile-fallback-only` | boolean | off | Keep the QR-first body on every device; disables the mobile CTA path. |
60
+
61
+ ## Events
62
+
63
+ All events bubble across the Shadow DOM (`composed: true`):
64
+
65
+ - `qrauth:authenticated` — `detail: { sessionId, user, signature }`
66
+ - `qrauth:scanned` — fired on the PENDING → SCANNED transition (pre-approval)
67
+ - `qrauth:expired` — session timed out
68
+ - `qrauth:denied` — user denied approval on their device
69
+ - `qrauth:error` — `detail: { message }`
70
+
71
+ ## Theming
72
+
73
+ Override any of the following CSS custom properties on the host page or on the element itself:
74
+
75
+ ```css
76
+ qrauth-login {
77
+ --qrauth-primary: #00a76f;
78
+ --qrauth-text: #1a1a2e;
79
+ --qrauth-bg: #ffffff;
80
+ --qrauth-surface: #f9fafb;
81
+ --qrauth-border: #e0e0e0;
82
+ --qrauth-radius: 12px;
83
+ --qrauth-btn-bg: #1b2a4a;
84
+ --qrauth-btn-hover: #263b66;
85
+ --qrauth-shadow: 0 24px 48px rgba(0, 0, 0, 0.15);
86
+ --qrauth-font: 'Inter', sans-serif;
87
+ }
88
+ ```
89
+
90
+ Or flip the bundled dark palette via `theme="dark"`.
91
+
92
+ ## Framework integration
93
+
94
+ The components are framework-free native Custom Elements. They work in React, Vue, Svelte, Angular, plain HTML — wherever the browser exposes `customElements`. For React, wrap event listeners in `useEffect` because React's synthetic event system doesn't observe custom events directly.
95
+
96
+ ## Links
97
+
98
+ - [Homepage](https://qrauth.io)
99
+ - [Docs](https://docs.qrauth.io)
100
+ - [Signing Architecture](https://docs.qrauth.io/guide/signing-architecture) — cryptographic model behind every approved session
101
+ - [Issues](https://github.com/qrauth-io/qrauth/issues)
102
+
103
+ ## License
104
+
105
+ MIT.
package/dist/base.d.ts CHANGED
@@ -3,6 +3,11 @@
3
3
  * Provides: Shadow DOM setup, CSS custom property theming, SSE connection management,
4
4
  * lifecycle hooks, and event dispatching.
5
5
  */
6
+ /**
7
+ * Media query that identifies mobile-like devices (coarse pointer + no hover).
8
+ * Exported for documentation and test mocks.
9
+ */
10
+ export declare const MOBILE_MEDIA_QUERY = "(pointer: coarse) and (hover: none)";
6
11
  export declare abstract class QRAuthElement extends HTMLElement {
7
12
  protected shadow: ShadowRoot;
8
13
  private _sseConnection;
@@ -16,6 +21,22 @@ export declare abstract class QRAuthElement extends HTMLElement {
16
21
  disconnectedCallback(): void;
17
22
  attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
18
23
  protected abstract render(): void;
24
+ /**
25
+ * Detect whether the component is running on a mobile-like device. Subclasses
26
+ * may use this to branch UX — e.g. `<qrauth-login>` swaps the big QR for a
27
+ * "Continue on this device" CTA when isMobileLike() returns true, since the
28
+ * scan-with-another-device flow is a dead end when the user already IS the
29
+ * phone. Resolution order:
30
+ *
31
+ * 1. `force-mode="mobile"` or `force-mode="desktop"` attribute — wins.
32
+ * 2. `mobile-fallback-only` attribute — always returns false (keeps the
33
+ * component's QR-only behaviour even on phones; consumers opt in here
34
+ * when they provide their own alternative flow around the component).
35
+ * 3. matchMedia on `(pointer: coarse)` + `(hover: none)` (both must match).
36
+ * 4. UA sniff fallback for environments without matchMedia (headless tests,
37
+ * old browsers).
38
+ */
39
+ protected isMobileLike(): boolean;
19
40
  protected connectSSE(url: string): void;
20
41
  protected onSSEEvent(event: string, callback: (data: unknown) => void): void;
21
42
  protected disconnectSSE(): void;
@@ -4,14 +4,25 @@ import { QRAuthElement } from './base.js';
4
4
  * and claim status updates.
5
5
  *
6
6
  * Attributes:
7
- * tenant — Your QRAuth App client ID (required)
8
- * theme — "light" (default) | "dark"
9
- * base-url — API base URL (default: https://qrauth.io)
10
- * scopes — Space-separated scope strings (default: "access")
11
- * ttl — TTL string like "30m", "6h", "1d" (default: "30m")
12
- * max-uses — Maximum number of claims (default: "1")
13
- * device-binding — Boolean attribute; present = device binding enabled
14
- * display — "button" | "inline" (default: "inline")
7
+ * tenant — Your QRAuth App client ID (required)
8
+ * theme — "light" (default) | "dark"
9
+ * base-url — API base URL (default: https://qrauth.io)
10
+ * scopes — Space-separated scope strings (default: "access")
11
+ * ttl — TTL string like "30m", "6h", "1d" (default: "30m")
12
+ * max-uses — Maximum number of claims (default: "1")
13
+ * device-binding — Boolean attribute; present = device binding enabled
14
+ * display — "button" | "inline" (default: "inline")
15
+ * force-mode — Reserved for future mobile-variant UI; no default
16
+ * behavior change on mobile viewports as of 0.3.0.
17
+ * The ephemeral flow is inherently "generate QR for
18
+ * another device to scan", so QR-first is correct
19
+ * regardless of form factor. Accepted values:
20
+ * "mobile" | "desktop" | "auto" (default "auto").
21
+ * mobile-fallback-only — Reserved for forward compatibility with the
22
+ * pattern used by <qrauth-login> and <qrauth-2fa>.
23
+ * No effect in 0.3.0 — included so consumers can
24
+ * start passing it without breakage when a future
25
+ * mobile-variant UI is added.
15
26
  *
16
27
  * Events (bubbles + composed, cross shadow-DOM):
17
28
  * qrauth:claimed — detail: { sessionId, status, scopes, metadata, useCount, maxUses }
@@ -36,6 +47,12 @@ export declare class QRAuthEphemeral extends QRAuthElement {
36
47
  get maxUses(): number;
37
48
  get deviceBinding(): boolean;
38
49
  get display(): string;
50
+ /**
51
+ * Reserved attribute — no effect in 0.3.0. See JSDoc header above for why
52
+ * the ephemeral flow intentionally keeps the QR-first body on mobile.
53
+ */
54
+ get forceMode(): 'mobile' | 'desktop' | 'auto';
55
+ get mobileFallbackOnly(): boolean;
39
56
  disconnectedCallback(): void;
40
57
  protected render(): void;
41
58
  private _renderButton;
package/dist/login.d.ts CHANGED
@@ -3,14 +3,24 @@ import { QRAuthElement } from './base.js';
3
3
  * <qrauth-login> — Drop-in QR authentication button / inline widget.
4
4
  *
5
5
  * Attributes:
6
- * tenant — Your QRAuth App client ID (required)
7
- * theme — "light" (default) | "dark"
8
- * base-url — API base URL (default: https://qrauth.io)
9
- * scopes — Space-separated scopes (default: "identity")
10
- * redirect-url — Optional redirect after successful auth
11
- * on-auth — Name of a global callback function to call on success
12
- * display — "button" (default) | "inline"
13
- * animated — If present, enables subtle QR pulse animation
6
+ * tenant — Your QRAuth App client ID (required)
7
+ * theme — "light" (default) | "dark"
8
+ * base-url — API base URL (default: https://qrauth.io)
9
+ * scopes — Space-separated scopes (default: "identity")
10
+ * redirect-url — Optional redirect after successful auth
11
+ * on-auth — Name of a global callback function to call on success
12
+ * display — "button" (default) | "inline"
13
+ * animated — If present, enables subtle QR pulse animation
14
+ * force-mode — "mobile" | "desktop" | "auto" (default "auto").
15
+ * Overrides the automatic mobile-like detection that
16
+ * swaps the QR-first body for a "Continue with
17
+ * QRAuth" CTA + QR expander on phones and tablets.
18
+ * mobile-fallback-only — If present, disables the mobile-aware UI entirely
19
+ * and keeps the QR-first body on every device. Use
20
+ * this when the surrounding page already provides an
21
+ * alternative same-device login path and the
22
+ * component is only there as the cross-device scan
23
+ * option.
14
24
  *
15
25
  * Events (bubbles + composed, cross shadow-DOM):
16
26
  * qrauth:authenticated — detail: { sessionId, user, signature }
@@ -25,6 +35,7 @@ import { QRAuthElement } from './base.js';
25
35
  export declare class QRAuthLogin extends QRAuthElement {
26
36
  static get observedAttributes(): string[];
27
37
  private _sessionId;
38
+ private _sessionToken;
28
39
  private _codeVerifier;
29
40
  private _status;
30
41
  private _expiresAt;
@@ -35,6 +46,8 @@ export declare class QRAuthLogin extends QRAuthElement {
35
46
  get onAuth(): string | null;
36
47
  get display(): string;
37
48
  get animated(): boolean;
49
+ get forceMode(): 'mobile' | 'desktop' | 'auto';
50
+ get mobileFallbackOnly(): boolean;
38
51
  disconnectedCallback(): void;
39
52
  protected render(): void;
40
53
  private _renderButton;
@@ -42,10 +55,30 @@ export declare class QRAuthLogin extends QRAuthElement {
42
55
  private _inlineIdleContent;
43
56
  private _modalContent;
44
57
  private _bodyForStatus;
58
+ /**
59
+ * Shared QR frame markup used by both the desktop pending body and the
60
+ * mobile "Use another device" expander. Centralised so the two paths can't
61
+ * drift.
62
+ */
63
+ private _qrFrameHtml;
64
+ /**
65
+ * Mobile-aware pending body. Primary CTA opens the hosted approval page
66
+ * (/a/:token) in a new tab — this same-device path short-circuits the
67
+ * cross-device QR scan that is a dead end on phones. The QR itself is
68
+ * demoted to a collapsed expander for the "I have a second device" case.
69
+ */
70
+ private _mobilePendingBody;
45
71
  private _sharedModalStyles;
46
72
  private _bindButtonEvents;
47
73
  private _bindInlineEvents;
48
74
  private _bindModalButtons;
75
+ /**
76
+ * Mobile same-device path: open the hosted approval page in a new tab.
77
+ * The existing polling loop in _beginPolling picks up SCANNED / APPROVED
78
+ * transitions from the server once the user completes the flow there,
79
+ * so qrauth:scanned and qrauth:authenticated fire unchanged.
80
+ */
81
+ private _openApprovalPage;
49
82
  private _openModal;
50
83
  private _closeModal;
51
84
  private _startAuth;