@zoreal/oauth2-react 0.2.19 → 0.2.20

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,41 +89,76 @@ import { ZorealOAuthProvider } from '@zoreal/oauth2-react';
89
89
  ```
90
90
 
91
91
  ```tsx
92
- // ZorealSignIn.tsx
93
- import { useZorealLogin } from '@zoreal/oauth2-react';
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';
95
+
96
+ 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);
94
101
 
95
- function ZorealSignIn() {
96
102
  // `email` (and profile.name, etc.) are returned from /userinfo on your backend.
97
103
  const login = useZorealLogin({
98
104
  flow: 'auth-code',
99
105
  scope: 'openid email profile.name',
100
106
  onSuccess: async ({ code, code_verifier, nonce }) => {
107
+ setBusy(false);
101
108
  // Send ALL THREE to your backend over TLS. It calls POST /token with the
102
109
  // code and verifier plus its client authentication, verifies the ID
103
110
  // token's nonce is this one, then reads the email and name from
104
111
  // /userinfo. That is where personal data is delivered.
105
- await fetch('/api/auth/zoreal', {
112
+ const res = await fetch('/api/auth/zoreal', {
106
113
  method: 'POST',
107
114
  headers: { 'Content-Type': 'application/json' },
108
115
  body: JSON.stringify({ code, code_verifier, nonce }),
109
116
  });
117
+ if (res.ok) onSignedIn();
118
+ },
119
+ onError: (e) => {
120
+ setBusy(false);
121
+ console.error(e.description ?? e.error);
110
122
  },
111
- onError: (e) => console.error(e.description ?? e.error),
112
123
  onNonOAuthError: (e) => {
113
- // The holder closing or ignoring the request is not an error to surface.
114
- if (e.type === 'request_denied' || e.type === 'request_expired') return;
124
+ setBusy(false);
125
+ // The holder declining or ignoring the request, or closing the dialog,
126
+ // is not an error to surface: the button is simply ready again.
127
+ if (e.type === 'request_denied' || e.type === 'request_expired' || e.type === 'popup_closed') return;
115
128
  console.error(e.description ?? e.type);
116
129
  },
117
130
  });
118
131
 
119
- return <button onClick={login}>Continue with ZOREAL</button>;
132
+ 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>
150
+ );
120
151
  }
121
152
  ```
122
153
 
123
- That is the whole integration. When `login()` runs, the provider puts the
124
- pairing modal on screen; on a phone it skips the QR and opens the ZOREAL ID
125
- app instead. See [The pairing modal](#the-pairing-modal) for what it does and
126
- how to theme, translate, time out or replace it.
154
+ 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.
127
162
 
128
163
  ## Quick start: the button (no backend, pseudonymous)
129
164
 
@@ -577,28 +612,28 @@ error path.
577
612
 
578
613
  ## A complete example
579
614
 
580
- A full sign-in component, end to end the shape a real auth-code integration
581
- takes. It renders the button, renders its own pairing UI (the hook renders
582
- none), hands `{ code, code_verifier, nonce }` to your backend on success, and
583
- treats the decline/expiry path as the non-events they are.
615
+ 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.
584
621
 
585
622
  ```tsx
586
623
  import { useState } from 'react';
587
- import { ZorealOAuthProvider, useZorealLogin } from '@zoreal/oauth2-react';
588
- import type { PairingState } from '@zoreal/oauth2-react';
624
+ import { ZorealOAuthProvider, useZorealLogin, ZorealBusyRing, ZorealMark } from '@zoreal/oauth2-react';
589
625
 
590
626
  function ZorealSignIn() {
591
- const [pairing, setPairing] = useState<PairingState | null>(null);
627
+ const [busy, setBusy] = useState(false);
592
628
  const [note, setNote] = useState<string | null>(null);
593
629
 
594
630
  const login = useZorealLogin({
595
631
  flow: 'auth-code',
596
632
  scope: 'openid email profile.name',
597
633
  // acr_values: 'zoreal.live', // request a fresh liveness for a step-up / high-value login
598
- onPairingStateChange: setPairing,
599
634
 
600
635
  onSuccess: async ({ code, code_verifier, nonce }) => {
601
- setPairing(null);
636
+ setBusy(false);
602
637
  // Post ALL THREE to YOUR backend over TLS. Your backend does the /token
603
638
  // exchange with its client authentication, verifies the ID token
604
639
  // (ES256 against the JWKS, iss/aud/exp, and this nonce), checks the acr
@@ -620,37 +655,38 @@ function ZorealSignIn() {
620
655
 
621
656
  // An OAuth error from the provider (e.g. a scope not on your allow list).
622
657
  onError: (e) => {
623
- setPairing(null);
658
+ setBusy(false);
624
659
  setNote(e.description ?? e.error); // the provider's words, verbatim
625
660
  },
626
661
 
627
- // The human outcomes: declined, expired, cancelled. Not faults — clear the
628
- // pairing UI and let them try again. Do not alarm on these.
662
+ // The human outcomes: declined, expired, the dialog closed. Not faults:
663
+ // the button is ready again. Do not alarm on these.
629
664
  onNonOAuthError: (e) => {
630
- setPairing(null);
665
+ setBusy(false);
631
666
  if (e.type === 'request_denied') setNote('Login was declined. Try again when ready.');
632
667
  else if (e.type === 'request_expired') setNote('That took too long. Try again.');
668
+ else if (e.type === 'popup_closed') setNote(null);
633
669
  else setNote('Something went wrong. Try again.');
634
670
  },
635
671
  });
636
672
 
637
673
  return (
638
674
  <div>
639
- <button onClick={login}>Continue with ZOREAL</button>
640
-
641
- {/* The hook renders nothing, so the pairing UI is yours. On desktop the
642
- login cannot complete until something shows pairing.qrUrl to scan. */}
643
- {pairing && !pairing.appLink && ['pending', 'claimed'].includes(pairing.status) && (
644
- <div role="dialog" aria-label="Log in with ZOREAL">
645
- <img src={pairing.qrUrl} alt="Log in with ZOREAL" width={200} height={200} />
646
- <p>
647
- {pairing.status === 'claimed'
648
- ? 'Approve the login in your ZOREAL ID app.'
649
- : 'Scan with your phone camera or the ZOREAL ID app.'}
650
- </p>
651
- <button onClick={pairing.cancel}>Cancel</button>
652
- </div>
653
- )}
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>
654
690
 
655
691
  {note && <p role="status">{note}</p>}
656
692
  </div>
@@ -659,13 +695,20 @@ function ZorealSignIn() {
659
695
 
660
696
  export default function App() {
661
697
  return (
662
- <ZorealOAuthProvider clientId="ast_your_asset_id">
698
+ <ZorealOAuthProvider clientId="ast_your_asset_id" locale="en" theme="auto">
663
699
  <ZorealSignIn />
664
700
  </ZorealOAuthProvider>
665
701
  );
666
702
  }
667
703
  ```
668
704
 
705
+ On a computer the provider renders the pairing modal for this button; nothing
706
+ here draws a QR. To draw your own instead, see
707
+ [Rendering it yourself](#rendering-it-yourself). On a phone there is no dialog:
708
+ the tap navigates to the provider, the ZOREAL ID app opens, and after the
709
+ approval the app reopens this page, where `useZorealLogin` finishes the sign-in
710
+ and `onSuccess` runs on that fresh page load.
711
+
669
712
  **The backend must verify.** This component only starts the flow and forwards a
670
713
  code; on its own it proves nothing. The security is your backend exchanging the
671
714
  code with its client authentication and verifying the signed ID token — use a
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  PairingModal: () => PairingModal,
26
26
  ZorealBusyRing: () => ZorealBusyRing,
27
27
  ZorealLogin: () => ZorealLogin,
28
+ ZorealMark: () => ZorealMark,
28
29
  ZorealOAuthProvider: () => ZorealOAuthProvider,
29
30
  hasGrantedAllScopesZoreal: () => hasGrantedAllScopesZoreal,
30
31
  hasGrantedAnyScopeZoreal: () => hasGrantedAnyScopeZoreal,
@@ -41,7 +42,7 @@ var import_react2 = require("react");
41
42
 
42
43
  // src/wire.ts
43
44
  var WIRE_VERSION = 1;
44
- var SDK_VERSION = "0.2.19";
45
+ var SDK_VERSION = "0.2.20";
45
46
  var DEFAULT_ISSUER = "https://id.zoreal.com";
46
47
  var POLL_INTERVAL_MS = 2e3;
47
48
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -2590,6 +2591,7 @@ function hasGrantedAnyScopeZoreal(response, firstScope, ...restScopes) {
2590
2591
  PairingModal,
2591
2592
  ZorealBusyRing,
2592
2593
  ZorealLogin,
2594
+ ZorealMark,
2593
2595
  ZorealOAuthProvider,
2594
2596
  hasGrantedAllScopesZoreal,
2595
2597
  hasGrantedAnyScopeZoreal,