@zoreal/oauth2-react 0.2.13 → 0.2.15

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
@@ -148,6 +148,28 @@ scans the QR with their phone and approves in the ZOREAL ID app. On a phone it
148
148
  skips the QR and opens the app directly. Either way your page just receives
149
149
  `onSuccess`.
150
150
 
151
+ ### What it looks like
152
+
153
+ The default: the mark in the brand blue, the label in the page's font, a 12px
154
+ corner. `shape="pill"`, `theme="filled_black"` and `text="verify_with"` are the
155
+ other shapes and words it takes.
156
+
157
+ <p>
158
+ <img src="docs/buttons/continue-with-zoreal.svg" alt="Continue with ZOREAL" height="62">
159
+ <img src="docs/buttons/verify-with-zoreal-id-pill.svg" alt="Verify with ZOREAL ID, pill" height="62">
160
+ <img src="docs/buttons/sign-in-with-zoreal-black.svg" alt="Sign in with ZOREAL, black" height="62">
161
+ </p>
162
+
163
+ While the tap is being answered, on a phone between the tap and the hand-over
164
+ to the app, the button is disabled and the pairing modal's light runs round it:
165
+
166
+ <p>
167
+ <img src="docs/buttons/continue-with-zoreal-busy.svg" alt="Continue with ZOREAL, busy" height="82">
168
+ </p>
169
+
170
+ A site that renders its own button can wrap it in `ZorealBusyRing` for the
171
+ same light; see [the pairing modal](#the-pairing-modal), **Mobile**.
172
+
151
173
  ### On your backend
152
174
 
153
175
  There is a sibling library for every major backend — use one instead of
@@ -229,7 +251,7 @@ What the modal does:
229
251
 
230
252
  | | |
231
253
  | --- | --- |
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'`. |
254
+ | **Mobile** | No QR and no modal. The SDK creates the pairing and sends the tab to the pairing link, which the ZOREAL ID app claims; with no app installed the same link is the page that installs it. That is one round trip after the tap: `ZorealLogin` disables itself and runs a light round its edge for that gap; 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'`. |
233
255
  | **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
256
  | **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. |
235
257
  | **Countdown** | Counts down to expiry, turning amber under 20s. Reads the clock each tick rather than decrementing, so a backgrounded tab comes back honest. |
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  DEFAULT_PAIRING_TIMEOUT_MS: () => DEFAULT_PAIRING_TIMEOUT_MS,
25
25
  PairingModal: () => PairingModal,
26
+ ZorealBusyRing: () => ZorealBusyRing,
26
27
  ZorealLogin: () => ZorealLogin,
27
28
  ZorealOAuthProvider: () => ZorealOAuthProvider,
28
29
  hasGrantedAllScopesZoreal: () => hasGrantedAllScopesZoreal,
@@ -40,7 +41,7 @@ var import_react2 = require("react");
40
41
 
41
42
  // src/wire.ts
42
43
  var WIRE_VERSION = 1;
43
- var SDK_VERSION = "0.2.13";
44
+ var SDK_VERSION = "0.2.15";
44
45
  var DEFAULT_ISSUER = "https://id.zoreal.com";
45
46
  var POLL_INTERVAL_MS = 2e3;
46
47
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1362,6 +1363,65 @@ var CSS = `
1362
1363
  }
1363
1364
 
1364
1365
  @keyframes ${PREFIX}-orbit { to { transform: rotate(360deg) } }
1366
+ /* THE BUSY RING. The light of the QR well, around any control that is
1367
+ waiting on the provider: the button on a phone between the tap and the
1368
+ hand-over to the app. The well's sweep is a cone from the centre, which is
1369
+ even on a square and useless on a wide button: it crawls along the long
1370
+ sides and lights two edges at once near the ends. So here the light is a
1371
+ dash on an SVG outline, which moves at one speed the whole way round
1372
+ whatever the shape, drawn with the well's tokens: its colour and head
1373
+ tint, its line width, its halo, its four second lap. pathLength makes the
1374
+ outline 100 units long, so every dash and offset is a percentage of the
1375
+ way round; the three layers share one head at 30 and differ in length
1376
+ and brightness, which is the comet's tail. Shown only while busy. */
1377
+ .${PREFIX}-ring {
1378
+ position: relative;
1379
+ display: inline-flex;
1380
+ vertical-align: middle;
1381
+ --zrl-beam-time: 4s;
1382
+ }
1383
+ .${PREFIX}-ring-svg {
1384
+ position: absolute;
1385
+ inset: -4px;
1386
+ width: calc(100% + 8px);
1387
+ height: calc(100% + 8px);
1388
+ overflow: visible;
1389
+ pointer-events: none;
1390
+ opacity: 0;
1391
+ transition: opacity 200ms ease-out;
1392
+ }
1393
+ .${PREFIX}-ring[data-busy="true"] > .${PREFIX}-ring-svg { opacity: 1; }
1394
+ .${PREFIX}-ring-svg rect {
1395
+ x: 2px;
1396
+ y: 2px;
1397
+ width: calc(100% - 4px);
1398
+ height: calc(100% - 4px);
1399
+ fill: none;
1400
+ stroke: var(--zrl-beam);
1401
+ stroke-width: var(--zrl-beam-line);
1402
+ stroke-linecap: round;
1403
+ animation: ${PREFIX}-dash var(--zrl-beam-time) linear infinite;
1404
+ }
1405
+ .${PREFIX}-ring-tail { stroke-dasharray: 30 70; stroke-dashoffset: 0; opacity: 0.35; }
1406
+ .${PREFIX}-ring-body { stroke-dasharray: 16 84; stroke-dashoffset: -14; opacity: 0.8; }
1407
+ .${PREFIX}-ring-head { stroke-dasharray: 5 95; stroke-dashoffset: -25; stroke: var(--zrl-beam-head); }
1408
+ .${PREFIX}-ring-halo {
1409
+ stroke-dasharray: 30 70;
1410
+ stroke-dashoffset: 0;
1411
+ stroke-width: calc(var(--zrl-glow-core) * 2 + var(--zrl-beam-line));
1412
+ opacity: calc(var(--zrl-glow-opacity) * 0.5);
1413
+ filter: blur(var(--zrl-glow-blur));
1414
+ }
1415
+ @keyframes ${PREFIX}-dash {
1416
+ to { stroke-dashoffset: calc(var(--zrl-dash-start, 0) - 100); }
1417
+ }
1418
+ .${PREFIX}-ring-body { --zrl-dash-start: -14; }
1419
+ .${PREFIX}-ring-head { --zrl-dash-start: -25; }
1420
+ @media (prefers-reduced-motion: reduce) {
1421
+ .${PREFIX}-ring-svg rect { animation: none; stroke-dasharray: none; opacity: 0.45; }
1422
+ .${PREFIX}-ring-halo, .${PREFIX}-ring-head { display: none; }
1423
+ }
1424
+
1365
1425
 
1366
1426
  @media (prefers-reduced-motion: reduce) {
1367
1427
  .${PREFIX}-scrim,
@@ -1570,7 +1630,7 @@ function useZorealOAuth() {
1570
1630
  }
1571
1631
 
1572
1632
  // src/ZorealLogin.tsx
1573
- var import_react4 = require("react");
1633
+ var import_react5 = require("react");
1574
1634
 
1575
1635
  // src/useZorealLogin.ts
1576
1636
  var import_react3 = require("react");
@@ -1937,8 +1997,45 @@ function useZorealLogin(options) {
1937
1997
  }).login;
1938
1998
  }
1939
1999
 
1940
- // src/ZorealLogin.tsx
2000
+ // src/ring.tsx
2001
+ var import_react4 = require("react");
1941
2002
  var import_jsx_runtime5 = require("react/jsx-runtime");
2003
+ function ZorealBusyRing({
2004
+ busy,
2005
+ radius = 8,
2006
+ theme = "auto",
2007
+ block = false,
2008
+ className,
2009
+ style,
2010
+ children
2011
+ }) {
2012
+ (0, import_react4.useEffect)(() => {
2013
+ ensureStyles();
2014
+ }, []);
2015
+ const rx = radius + 2;
2016
+ const layer = (name) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { className: cx(name), rx, ry: rx, pathLength: 100 });
2017
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2018
+ "span",
2019
+ {
2020
+ className: className ? `${cx("root")} ${cx("ring")} ${className}` : `${cx("root")} ${cx("ring")}`,
2021
+ "data-theme": theme,
2022
+ "data-busy": busy,
2023
+ style: block ? { display: "flex", ...style } : style,
2024
+ children: [
2025
+ children,
2026
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { className: cx("ring-svg"), "aria-hidden": "true", children: [
2027
+ layer("ring-halo"),
2028
+ layer("ring-tail"),
2029
+ layer("ring-body"),
2030
+ layer("ring-head")
2031
+ ] })
2032
+ ]
2033
+ }
2034
+ );
2035
+ }
2036
+
2037
+ // src/ZorealLogin.tsx
2038
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1942
2039
  var TEXTS = {
1943
2040
  continue_with: null,
1944
2041
  signin_with: "Sign in with ZOREAL",
@@ -1947,9 +2044,9 @@ var TEXTS = {
1947
2044
  verify_with: "Verify with ZOREAL ID"
1948
2045
  };
1949
2046
  var SIZES = {
1950
- large: { height: 44, font: 15, pad: 20 },
1951
- medium: { height: 38, font: 14, pad: 16 },
1952
- small: { height: 32, font: 12, pad: 12 }
2047
+ large: { height: 50, font: 14, pad: 20, mark: 22, gap: 12, radius: 12 },
2048
+ medium: { height: 42, font: 14, pad: 16, mark: 20, gap: 10, radius: 10 },
2049
+ small: { height: 34, font: 12, pad: 12, mark: 16, gap: 8, radius: 8 }
1953
2050
  };
1954
2051
  function ZorealLogin(props) {
1955
2052
  const {
@@ -1957,11 +2054,11 @@ function ZorealLogin(props) {
1957
2054
  onError,
1958
2055
  containerProps,
1959
2056
  type = "standard",
1960
- theme = "filled",
2057
+ theme = "outline",
1961
2058
  size = "large",
1962
2059
  text = "continue_with",
1963
2060
  shape = "rectangular",
1964
- logo_alignment = "left",
2061
+ logo_alignment = "center",
1965
2062
  width,
1966
2063
  click_listener,
1967
2064
  flow = "browser-direct",
@@ -1969,21 +2066,36 @@ function ZorealLogin(props) {
1969
2066
  } = props;
1970
2067
  const { locale } = useZorealOAuth();
1971
2068
  const label = TEXTS[text] ?? strings(locale).buttonContinue;
2069
+ const [busy, setBusy] = (0, import_react5.useState)(false);
1972
2070
  const { login } = useZorealFlow({
1973
2071
  ...request,
1974
2072
  flow,
1975
- onCredential: flow === "browser-direct" ? onSuccess : void 0,
1976
- onCode: flow === "auth-code" ? onSuccess : void 0,
1977
- onError: (e) => onError?.({ type: "unknown", description: e.description ?? e.error }),
1978
- onNonOAuthError: (e) => onError?.(e)
2073
+ onCredential: flow === "browser-direct" ? (r) => {
2074
+ setBusy(false);
2075
+ onSuccess(r);
2076
+ } : void 0,
2077
+ onCode: flow === "auth-code" ? (r) => {
2078
+ setBusy(false);
2079
+ onSuccess(r);
2080
+ } : void 0,
2081
+ onError: (e) => {
2082
+ setBusy(false);
2083
+ onError?.({ type: "unknown", description: e.description ?? e.error });
2084
+ },
2085
+ onNonOAuthError: (e) => {
2086
+ setBusy(false);
2087
+ onError?.(e);
2088
+ }
1979
2089
  });
1980
2090
  const s = SIZES[size];
1981
- const style = (0, import_react4.useMemo)(
2091
+ const radius = shape === "pill" ? s.height / 2 : shape === "square" ? 4 : s.radius;
2092
+ const brandMark = theme !== "filled";
2093
+ const style = (0, import_react5.useMemo)(
1982
2094
  () => ({
1983
2095
  display: "inline-flex",
1984
2096
  alignItems: "center",
1985
2097
  justifyContent: logo_alignment === "center" ? "center" : "flex-start",
1986
- gap: 10,
2098
+ gap: s.gap,
1987
2099
  height: s.height,
1988
2100
  padding: `0 ${s.pad}px`,
1989
2101
  width,
@@ -1991,30 +2103,34 @@ function ZorealLogin(props) {
1991
2103
  fontFamily: "inherit",
1992
2104
  fontWeight: 500,
1993
2105
  cursor: "pointer",
1994
- borderRadius: shape === "pill" ? s.height / 2 : shape === "square" ? 4 : 8,
1995
- ...theme === "outline" ? { background: "transparent", color: "inherit", border: "1px solid rgba(128,128,128,0.5)" } : theme === "filled_black" ? { background: "#111", color: "#fff", border: "1px solid #111" } : { background: "#00b4d9", color: "#fff", border: "1px solid #00b4d9" }
2106
+ borderRadius: radius,
2107
+ ...theme === "outline" ? { background: "#ffffff", color: "#16181c", border: "1px solid #e2e4de" } : theme === "filled_black" ? { background: "#111", color: "#fff", border: "1px solid #111" } : { background: "#00b4d9", color: "#fff", border: "1px solid #00b4d9" }
1996
2108
  }),
1997
- [logo_alignment, s, shape, theme, width]
2109
+ [logo_alignment, s, radius, theme, width]
1998
2110
  );
1999
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ...containerProps, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2111
+ 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)(
2000
2112
  "button",
2001
2113
  {
2002
2114
  type: "button",
2003
- style,
2115
+ style: busy ? { ...style, cursor: "progress" } : style,
2116
+ disabled: busy,
2117
+ "aria-busy": busy,
2004
2118
  onClick: () => {
2119
+ if (busy) return;
2005
2120
  click_listener?.();
2121
+ setBusy(true);
2006
2122
  login();
2007
2123
  },
2008
2124
  children: [
2009
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ZorealMark, { size: Math.round(s.font * 1.25) }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ZorealMark, { size: s.mark, brand: brandMark }),
2010
2126
  type === "standard" && label
2011
2127
  ]
2012
2128
  }
2013
- ) });
2129
+ ) }) });
2014
2130
  }
2015
2131
 
2016
2132
  // src/useZorealAutoLogin.ts
2017
- var import_react5 = require("react");
2133
+ var import_react6 = require("react");
2018
2134
  function useZorealAutoLogin(options) {
2019
2135
  const { login } = useZorealFlow({
2020
2136
  flow: "browser-direct",
@@ -2031,8 +2147,8 @@ function useZorealAutoLogin(options) {
2031
2147
  },
2032
2148
  onNonOAuthError: (e) => options.onError?.(e)
2033
2149
  });
2034
- const fired = (0, import_react5.useRef)(false);
2035
- (0, import_react5.useEffect)(() => {
2150
+ const fired = (0, import_react6.useRef)(false);
2151
+ (0, import_react6.useEffect)(() => {
2036
2152
  if (options.disabled || fired.current) return;
2037
2153
  fired.current = true;
2038
2154
  login();
@@ -2056,6 +2172,7 @@ function hasGrantedAnyScopeZoreal(response, firstScope, ...restScopes) {
2056
2172
  0 && (module.exports = {
2057
2173
  DEFAULT_PAIRING_TIMEOUT_MS,
2058
2174
  PairingModal,
2175
+ ZorealBusyRing,
2059
2176
  ZorealLogin,
2060
2177
  ZorealOAuthProvider,
2061
2178
  hasGrantedAllScopesZoreal,