@zoreal/oauth2-react 0.1.11 → 0.2.6

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
@@ -1,6 +1,6 @@
1
1
  # @zoreal/oauth2-react
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![types](https://img.shields.io/npm/types/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![CI](https://img.shields.io/github/actions/workflow/status/Bynn-Intelligence/zoreal-oauth2-react/ci.yml?branch=main&label=CI)](https://github.com/Bynn-Intelligence/zoreal-oauth2-react/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
3
+ [![npm](https://img.shields.io/npm/v/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![types](https://img.shields.io/npm/types/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![CI](https://img.shields.io/github/actions/workflow/status/Bynn-Intelligence/zoreal-oauth2-react/ci.yml?branch=main&label=CI)](https://github.com/Bynn-Intelligence/zoreal-oauth2-react/actions/workflows/ci.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Bynn-Intelligence/zoreal-oauth2-react/badge)](https://scorecard.dev/viewer/?uri=github.com/Bynn-Intelligence/zoreal-oauth2-react) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
4
 
5
5
  Login with ZOREAL for React: a ZOREAL Verified Proof-of-Human behind every sign-in.
6
6
 
@@ -76,20 +76,28 @@ origin.
76
76
 
77
77
  ## Quick start: auth-code (email and name, needs your backend)
78
78
 
79
+ Two pieces: the provider once, and a button wherever you sign people in. The
80
+ QR, its live status, the countdown and the cancel wiring are the SDK's job.
81
+
79
82
  ```tsx
80
- import { useState } from 'react';
83
+ // app.tsx once, around the part of your app that signs in.
84
+ import { ZorealOAuthProvider } from '@zoreal/oauth2-react';
85
+
86
+ <ZorealOAuthProvider clientId={ZOREAL_CLIENT_ID} locale="en" theme="auto">
87
+ <App />
88
+ </ZorealOAuthProvider>;
89
+ ```
90
+
91
+ ```tsx
92
+ // ZorealSignIn.tsx
81
93
  import { useZorealLogin } from '@zoreal/oauth2-react';
82
94
 
83
95
  function ZorealSignIn() {
84
- const [pairing, setPairing] = useState(null);
85
-
86
96
  // `email` (and profile.name, etc.) are returned from /userinfo on your backend.
87
97
  const login = useZorealLogin({
88
98
  flow: 'auth-code',
89
99
  scope: 'openid email profile.name',
90
- onPairingStateChange: setPairing,
91
100
  onSuccess: async ({ code, code_verifier, nonce }) => {
92
- setPairing(null);
93
101
  // Send ALL THREE to your backend over TLS. It calls POST /token with the
94
102
  // code and verifier plus its client authentication, verifies the ID
95
103
  // token's nonce is this one, then reads the email and name from
@@ -100,35 +108,22 @@ function ZorealSignIn() {
100
108
  body: JSON.stringify({ code, code_verifier, nonce }),
101
109
  });
102
110
  },
111
+ onError: (e) => console.error(e.description ?? e.error),
112
+ 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;
115
+ console.error(e.description ?? e.type);
116
+ },
103
117
  });
104
118
 
105
- return (
106
- <div>
107
- <button onClick={login}>Continue with ZOREAL</button>
108
- {pairing && !pairing.appLink && ['pending', 'claimed'].includes(pairing.status) && (
109
- <div>
110
- <img src={pairing.qrUrl} alt="Log in with ZOREAL" width={200} height={200} />
111
- <p>
112
- {pairing.status === 'claimed'
113
- ? 'Approve the login in your ZOREAL ID app.'
114
- : 'Scan with your phone camera or the ZOREAL ID app.'}
115
- </p>
116
- <button onClick={pairing.cancel}>Cancel</button>
117
- </div>
118
- )}
119
- </div>
120
- );
119
+ return <button onClick={login}>Continue with ZOREAL</button>;
121
120
  }
122
121
  ```
123
122
 
124
- **The hook renders nothing, so in this flow the pairing UI is yours.** On
125
- desktop the login cannot complete unless something shows `pairing.qrUrl` (the
126
- provider-served QR image) for the holder to scan that is what
127
- `onPairingStateChange` is for, and since 0.1.4 it carries `pairUrl`, `qrUrl`,
128
- `appLink` and `cancel` on every callback, including one fired immediately when
129
- the pairing starts. On a phone (`appLink: true`) the SDK opens the pairing
130
- link itself and no panel is needed. Only the drop-in `<ZorealLogin>` button
131
- below renders its own QR panel, and that component is browser-direct only.
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.
132
127
 
133
128
  ## Quick start: the button (no backend, pseudonymous)
134
129
 
@@ -148,9 +143,10 @@ import { ZorealOAuthProvider, ZorealLogin } from '@zoreal/oauth2-react';
148
143
  </ZorealOAuthProvider>
149
144
  ```
150
145
 
151
- On desktop the button shows a QR; the user scans it with their phone and
152
- approves in the ZOREAL ID app. On a phone it opens the app directly. Either
153
- way your page just receives `onSuccess`.
146
+ On desktop the provider opens the [pairing modal](#the-pairing-modal); the user
147
+ scans the QR with their phone and approves in the ZOREAL ID app. On a phone it
148
+ skips the QR and opens the app directly. Either way your page just receives
149
+ `onSuccess`.
154
150
 
155
151
  ### On your backend
156
152
 
@@ -209,6 +205,57 @@ Things a backend implementer needs to know, learned the concrete way:
209
205
  `ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
210
206
  in a URL, which is a credential in every access log on the path.
211
207
 
208
+ ## The pairing modal
209
+
210
+ On desktop, a QR sign-in cannot complete unless something puts the pairing code
211
+ on screen. Since 0.2.0 that something is this package: `ZorealOAuthProvider`
212
+ renders the modal for **both** flows, so `useZorealLogin` integrators get the
213
+ whole pairing UI without writing (or styling, or translating) a line of it.
214
+
215
+ ```jsx
216
+ <ZorealOAuthProvider clientId={CLIENT_ID} locale={i18n.language} theme="auto">
217
+ <App />
218
+ </ZorealOAuthProvider>
219
+ ```
220
+
221
+ That is the entire integration. Your button stays a button:
222
+
223
+ ```jsx
224
+ const login = useZorealLogin({ flow: 'auth-code', scope: 'openid email', onSuccess });
225
+ return <button onClick={login}>Continue with ZOREAL</button>;
226
+ ```
227
+
228
+ What the modal does:
229
+
230
+ | | |
231
+ | --- | --- |
232
+ | **Mobile** | No QR. The SDK opens the pairing link, which the ZOREAL ID app claims; the modal never appears. Force one or the other with `display: 'qr'` / `'link'`. |
233
+ | **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). |
234
+ | **Countdown** | Counts down to expiry, turning amber under 20s. Reads the clock each tick rather than decrementing, so a backgrounded tab comes back honest. |
235
+ | **Timeout** | Closes and cancels at zero. Defaults to 120s; override with `pairingTimeoutMs`. The provider's own expiry wins when it is shorter. |
236
+ | **Cancel** | The X, the Cancel button, `Escape`, clicking outside and the timeout are one behaviour: abort the poll, close the modal. An orphaned poll is exactly how a request gets cancelled for over-polling. |
237
+ | **No ZOREAL ID yet** | A footer says the same code also installs the app. Without it the panel reads as "scan this with something I do not have", and the flow dead-ends at the one moment it can still be recovered. |
238
+ | **Themes** | `theme="auto"` (default) follows `prefers-color-scheme`; `"light"` and `"dark"` force it. The QR well stays white in dark mode on purpose — an inverted QR fails on a good number of phone cameras. |
239
+ | **Language** | Ships its own copy in 13 locales. Pass `locale` and the modal matches the page it opened on; omit it and it follows the browser's preference list, English as the floor. Arabic flips the dialog to RTL. |
240
+ | **Accessibility** | `role="dialog"`, `aria-modal`, labelled by its title, focus moved in on open, scroll locked, visible focus rings, and a `prefers-reduced-motion` fallback. |
241
+
242
+ Styling is a single `<style>` tag injected once, every selector prefixed `zrl-`
243
+ and scoped to the dialog. There is no CSS file to import and nothing to add to
244
+ your build, because a required import step is a required support ticket.
245
+
246
+ ### Rendering it yourself
247
+
248
+ If you already have a design system you want the QR to live inside, opt out:
249
+
250
+ ```jsx
251
+ <ZorealOAuthProvider clientId={CLIENT_ID} pairingUI="none">
252
+ ```
253
+
254
+ `onPairingStateChange` then carries everything you need on every state:
255
+ `qrUrl`, `pairUrl`, `status`, `expiresIn` and `cancel`. Render `qrUrl` in an
256
+ `<img>`; do not draw your own. `PairingModal` is also exported if you want the
257
+ real dialog but on your own terms.
258
+
212
259
  ## Scopes and claims
213
260
 
214
261
  Request scopes in the `scope` string, space-separated, always starting with