@flow-industries/id 0.21.0 → 0.21.1

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
@@ -47,6 +47,38 @@ Full documentation at **[docs.flow.industries/en/auth](https://docs.flow.industr
47
47
  - [JWT verify](https://docs.flow.industries/en/auth/jwt-verify) - verify Flow sessions on your backend
48
48
  - [API reference](https://docs.flow.industries/en/auth/api) and [self-hosting](https://docs.flow.industries/en/auth/self-hosting)
49
49
 
50
+ ## Public profile overlays
51
+
52
+ ```ts
53
+ import { closeProfile, openProfile } from "@flow-industries/id";
54
+ import { isValidUsername } from "@flow-industries/id/usernames";
55
+
56
+ if (isValidUsername(username)) {
57
+ openProfile(username, { onClose: () => resumeInput() });
58
+ }
59
+
60
+ closeProfile();
61
+ ```
62
+
63
+ `@flow-industries/id/usernames` also exports `USERNAME_REGEX` and
64
+ `MAX_USERNAME_LENGTH`. It has no browser or server dependencies.
65
+
66
+ `OpenProfileOptions.onClose?: () => void` fires once after the overlay stops
67
+ accepting input, before its exit animation finishes. It covers the close button,
68
+ backdrop, Escape, `closeProfile()`, and sign-in/sign-out handoffs. The latest
69
+ successful `openProfile` call owns the callback, even when switching profiles
70
+ while open; an omitted callback clears the previous one. Invalid usernames and
71
+ SSR return `false` without replacing it. Closing an already closed overlay is a
72
+ no-op. A callback may open another profile.
73
+
74
+ React callers pass the same option to
75
+ `useOpenProfile({ onClose: () => resumeInput() })` from
76
+ `@flow-industries/id/react`; the callback is captured when its returned opener
77
+ is called. The overlay belongs to the page and survives component unmounts.
78
+
79
+ Open from the document that owns the full viewport. Canvas hosts must release
80
+ pointer lock before opening, then use `onClose` to restore their input state.
81
+
50
82
  ## License
51
83
 
52
84
  MIT
@@ -21,6 +21,7 @@ function createViewer(options) {
21
21
  // meant for the page underneath with nothing on screen to explain it.
22
22
  let ready = false;
23
23
  let visible = false;
24
+ let onClose;
24
25
  function applyHitTesting() {
25
26
  if (frame)
26
27
  frame.style.pointerEvents = ready && visible ? "auto" : "none";
@@ -78,7 +79,7 @@ function createViewer(options) {
78
79
  });
79
80
  return opened;
80
81
  }
81
- function open(username) {
82
+ function open(username, nextOnClose) {
82
83
  const mounted = bridge;
83
84
  const live = mounted ?? mount(username);
84
85
  // A frame created just now is already on this profile; an existing one is
@@ -93,6 +94,8 @@ function createViewer(options) {
93
94
  type: "profile-identity",
94
95
  identity: hostIdentity(flow),
95
96
  });
97
+ window.addEventListener("keydown", onKeyDown);
98
+ onClose = nextOnClose;
96
99
  visible = true;
97
100
  applyHitTesting();
98
101
  void live.send("__internal", { type: "dialog-shown" });
@@ -104,11 +107,21 @@ function createViewer(options) {
104
107
  // always-rendered, transparent and click-through while hidden keeps every
105
108
  // open animation reliable, on the second open and the tenth.
106
109
  function close() {
107
- if (!frame || !bridge)
110
+ if (!visible || !frame || !bridge)
108
111
  return;
109
112
  visible = false;
113
+ window.removeEventListener("keydown", onKeyDown);
110
114
  applyHitTesting();
111
115
  void bridge.send("__internal", { type: "dialog-hidden" });
116
+ const notifyClose = onClose;
117
+ onClose = undefined;
118
+ notifyClose?.();
119
+ }
120
+ function onKeyDown(event) {
121
+ if (event.key !== "Escape" || event.defaultPrevented)
122
+ return;
123
+ event.preventDefault();
124
+ close();
112
125
  }
113
126
  function setTheme(next) {
114
127
  if (next === theme)
@@ -163,7 +176,7 @@ export function openProfile(username, options = {}) {
163
176
  viewer = live;
164
177
  if (options.theme)
165
178
  live.setTheme(options.theme);
166
- live.open(username);
179
+ live.open(username, options.onClose);
167
180
  return true;
168
181
  }
169
182
  /**
@@ -19,6 +19,6 @@ import { useFlow } from "./hooks";
19
19
  */
20
20
  export function useOpenProfile(options = {}) {
21
21
  const flow = useFlow();
22
- const { host, theme } = options;
23
- return useCallback((username) => openProfile(username, { flow, host, theme }), [flow, host, theme]);
22
+ const { host, theme, onClose } = options;
23
+ return useCallback((username) => openProfile(username, { flow, host, theme, onClose }), [flow, host, theme, onClose]);
24
24
  }
@@ -230,6 +230,14 @@ export type ProfileButtonHandle = {
230
230
  destroy: () => void;
231
231
  };
232
232
  export type OpenProfileOptions = {
233
+ /**
234
+ * Called once when the overlay closes, including its close button, backdrop,
235
+ * Escape, closeProfile(), or a sign-in/sign-out handoff. Runs after the
236
+ * overlay stops accepting input, without waiting for the exit animation.
237
+ * The latest successful open replaces this callback; omitting it clears the
238
+ * previous callback. Rejected opens and repeated closes do not notify.
239
+ */
240
+ onClose?: () => void;
233
241
  /**
234
242
  * The Flow ID origin serving the overlay (e.g. https://id.flow.industries).
235
243
  * Defaults to the resolved Flow instance's `host`, then the SDK default.
@@ -249,12 +257,7 @@ export type OpenProfileOptions = {
249
257
  */
250
258
  flow?: Flow;
251
259
  };
252
- export type UseOpenProfileOptions = {
253
- /** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
254
- host?: string;
255
- /** Color scheme for the dialog chrome; defaults to "light dark" (the OS). */
256
- theme?: ColorScheme;
257
- };
260
+ export type UseOpenProfileOptions = Pick<OpenProfileOptions, "host" | "theme" | "onClose">;
258
261
  /** The inline Flow ID widgets an embedder can mount alongside its own UI. */
259
262
  export type FlowWidgetName = "xp" | "action-timer";
260
263
  export type MountFlowWidgetOptions = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/sdk/client/index.d.ts",
20
20
  "import": "./dist/sdk/client/index.js"
21
21
  },
22
+ "./usernames": {
23
+ "types": "./dist/sdk/usernames.d.ts",
24
+ "import": "./dist/sdk/usernames.js"
25
+ },
22
26
  "./react": {
23
27
  "types": "./dist/sdk/react/index.d.ts",
24
28
  "import": "./dist/sdk/react/index.js"
@@ -69,6 +73,7 @@
69
73
  "usernames:refresh": "bun run scripts/generate-reserved-usernames.ts",
70
74
  "lint": "biome check",
71
75
  "test": "bun test src",
76
+ "test:sdk-package": "bash scripts/test-sdk-package.sh",
72
77
  "format": "biome format --write",
73
78
  "check": "biome check --write",
74
79
  "typecheck": "tsr generate && tsc --noEmit"