@flow-industries/id 0.17.0 → 0.18.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.
@@ -34,12 +34,13 @@ export function createFlowWidget(options) {
34
34
  return {
35
35
  frame: null,
36
36
  post() { },
37
+ setTheme() { },
37
38
  destroy() { },
38
39
  };
39
40
  const flow = options.flow ?? getFlow() ?? requireFlow();
40
41
  const host = resolveIdHost(options.host ?? flow.host);
41
42
  const hostOrigin = new URL(host).origin;
42
- const theme = options.theme ?? "light dark";
43
+ let theme = options.theme ?? "light dark";
43
44
  const route = WIDGET_ROUTE[options.widget];
44
45
  const frame = makeIframe(`${host}${route}`);
45
46
  frame.style.background = "transparent";
@@ -70,6 +71,15 @@ export function createFlowWidget(options) {
70
71
  post(message) {
71
72
  frame.contentWindow?.postMessage(message, hostOrigin);
72
73
  },
74
+ setTheme(next) {
75
+ if (next === theme)
76
+ return;
77
+ theme = next;
78
+ void bridge.send("__internal", {
79
+ type: "set-theme",
80
+ theme: { colorScheme: next },
81
+ });
82
+ },
73
83
  destroy() {
74
84
  bridge.destroy();
75
85
  frame.remove();
@@ -29,11 +29,13 @@ const OVERLAY_STYLE = {
29
29
  */
30
30
  export function createProfileButton(options) {
31
31
  if (typeof document === "undefined")
32
- return { destroy() { } };
32
+ return { setTheme() { }, destroy() { } };
33
33
  const flow = options.flow ?? getFlow() ?? requireFlow();
34
34
  const host = resolveIdHost(options.host ?? flow.host);
35
35
  const hostOrigin = new URL(host).origin;
36
- const theme = options.theme ?? "light dark";
36
+ // Mutable so `setTheme` recolors over the open bridge, and so a dialog opened
37
+ // after a toggle is initialized with the theme the host is actually showing.
38
+ let theme = options.theme ?? "light dark";
37
39
  let createdContainer = null;
38
40
  let container = options.container ?? null;
39
41
  if (!container) {
@@ -183,6 +185,19 @@ export function createProfileButton(options) {
183
185
  pushIdentity();
184
186
  const unsubscribe = flow.subscribe(pushIdentity);
185
187
  return {
188
+ setTheme(next) {
189
+ if (next === theme)
190
+ return;
191
+ theme = next;
192
+ void pillBridge.send("__internal", {
193
+ type: "set-theme",
194
+ theme: { colorScheme: next },
195
+ });
196
+ void dialogBridge?.send("__internal", {
197
+ type: "set-theme",
198
+ theme: { colorScheme: next },
199
+ });
200
+ },
186
201
  destroy() {
187
202
  unsubscribe();
188
203
  pillBridge.destroy();
@@ -8,5 +8,9 @@ import type { FlowWidgetProps } from "../types";
8
8
  * Prefer this over embedding the widget's URL in your own `<iframe>`: only a
9
9
  * frame mounted this way completes the handshake that lets it authenticate as
10
10
  * the signed-in user from a cross-site page (AUTH-171).
11
+ *
12
+ * A changing `theme` recolors the mounted widget over its open bridge rather
13
+ * than remounting it — see `<ProfileButton>` for why the frame must survive a
14
+ * light/dark toggle.
11
15
  */
12
16
  export declare function FlowWidget({ widget, className, host, theme, }: FlowWidgetProps): import("react/jsx-runtime").JSX.Element;
@@ -11,10 +11,16 @@ import { useFlow } from "./hooks";
11
11
  * Prefer this over embedding the widget's URL in your own `<iframe>`: only a
12
12
  * frame mounted this way completes the handshake that lets it authenticate as
13
13
  * the signed-in user from a cross-site page (AUTH-171).
14
+ *
15
+ * A changing `theme` recolors the mounted widget over its open bridge rather
16
+ * than remounting it — see `<ProfileButton>` for why the frame must survive a
17
+ * light/dark toggle.
14
18
  */
15
19
  export function FlowWidget({ widget, className, host, theme, }) {
16
20
  const flow = useFlow();
17
21
  const ref = useRef(null);
22
+ const handleRef = useRef(null);
23
+ const themeRef = useRef(theme);
18
24
  useEffect(() => {
19
25
  const container = ref.current;
20
26
  if (!container)
@@ -24,14 +30,23 @@ export function FlowWidget({ widget, className, host, theme, }) {
24
30
  container,
25
31
  flow,
26
32
  ...(host ? { host } : {}),
27
- ...(theme ? { theme } : {}),
33
+ ...(themeRef.current ? { theme: themeRef.current } : {}),
28
34
  });
35
+ handleRef.current = handle;
29
36
  if (handle.frame) {
30
37
  handle.frame.style.width = "100%";
31
38
  handle.frame.style.height = "100%";
32
39
  handle.frame.style.display = "block";
33
40
  }
34
- return () => handle.destroy();
35
- }, [widget, flow, host, theme]);
41
+ return () => {
42
+ handleRef.current = null;
43
+ handle.destroy();
44
+ };
45
+ }, [widget, flow, host]);
46
+ useEffect(() => {
47
+ themeRef.current = theme;
48
+ if (theme)
49
+ handleRef.current?.setTheme(theme);
50
+ }, [theme]);
36
51
  return _jsx("div", { ref: ref, className: className });
37
52
  }
@@ -5,5 +5,10 @@ import type { ProfileButtonProps } from "../types";
5
5
  * the pill iframe mounts into; the iframe itself is sized to its content and
6
6
  * expands to the profile dialog on click. Resolves the Flow instance from a
7
7
  * `<FlowIdProvider>` or the createFlow() singleton.
8
+ *
9
+ * A changing `theme` recolors the mounted pill over its open bridge rather than
10
+ * remounting it: `theme` is deliberately absent from the mount effect's deps,
11
+ * because tearing the iframe down on a light/dark toggle reloads the widget and
12
+ * drops the pill out of the host's layout mid-toggle.
8
13
  */
9
14
  export declare function ProfileButton({ className, host, theme }: ProfileButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -8,10 +8,17 @@ import { useFlow } from "./hooks";
8
8
  * the pill iframe mounts into; the iframe itself is sized to its content and
9
9
  * expands to the profile dialog on click. Resolves the Flow instance from a
10
10
  * `<FlowIdProvider>` or the createFlow() singleton.
11
+ *
12
+ * A changing `theme` recolors the mounted pill over its open bridge rather than
13
+ * remounting it: `theme` is deliberately absent from the mount effect's deps,
14
+ * because tearing the iframe down on a light/dark toggle reloads the widget and
15
+ * drops the pill out of the host's layout mid-toggle.
11
16
  */
12
17
  export function ProfileButton({ className, host, theme }) {
13
18
  const flow = useFlow();
14
19
  const ref = useRef(null);
20
+ const handleRef = useRef(null);
21
+ const themeRef = useRef(theme);
15
22
  useEffect(() => {
16
23
  const container = ref.current;
17
24
  if (!container)
@@ -20,9 +27,18 @@ export function ProfileButton({ className, host, theme }) {
20
27
  container,
21
28
  flow,
22
29
  ...(host ? { host } : {}),
23
- ...(theme ? { theme } : {}),
30
+ ...(themeRef.current ? { theme: themeRef.current } : {}),
24
31
  });
25
- return () => handle.destroy();
26
- }, [flow, host, theme]);
32
+ handleRef.current = handle;
33
+ return () => {
34
+ handleRef.current = null;
35
+ handle.destroy();
36
+ };
37
+ }, [flow, host]);
38
+ useEffect(() => {
39
+ themeRef.current = theme;
40
+ if (theme)
41
+ handleRef.current?.setTheme(theme);
42
+ }, [theme]);
27
43
  return _jsx("div", { ref: ref, className: className });
28
44
  }
@@ -6,7 +6,7 @@ export type { Call, ConnectCapabilities, ConnectRequest, ConnectResponse, GuestR
6
6
  export { getConnectCapabilities, isPersonalSignParams, isSendCallsParams, isSendTransactionParams, } from "./protocol";
7
7
  export type { CreateRoomEventInput, RoomEventAttendance, RoomEventFrequency, RoomEventInstance, RoomEventInterest, RoomEventInterestState, RoomEventList, RoomEventOccurrence, RoomEventRecurrence, RoomEventRoomRef, RoomEventSeries, RoomEventStatus, SetInterestInput, UpdateRoomEventInput, } from "./room-events";
8
8
  export type { CreateRoomInput, GlobalRole, ModerationSubject, MyRooms, RoleChangeVerdict, RoomChannel, RoomDetail, RoomList, RoomMemberEntry, RoomMembers, RoomOccupancy, RoomOwner, RoomPresenceEntry, RoomPresenceSnapshot, RoomRestrictionKind, RoomRole, RoomSummary, RoomSurfaceSettings, RoomsApi, RoomVisibility, StaffEntry, UpdateRoomInput, VerifyRoomContext, Viewer, } from "./rooms";
9
- export type { AccessKeyOptions, AccessKeyPreparation, Address, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, FlowWidgetHandle, FlowWidgetName, FlowWidgetProps, Listener, LoginOptions, MountFlowWidgetOptions, MountProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
9
+ export type { AccessKeyOptions, AccessKeyPreparation, Address, ColorScheme, CreateDialogHostOptions, CreateFlowOptions, DialogHost, DialogOpenOptions, FinalizeAccessKeyParams, Flow, FlowConnectorParameters, FlowCookieNames, FlowIdProviderProps, FlowSessionState, FlowState, FlowWidgetHandle, FlowWidgetName, FlowWidgetProps, Listener, LoginOptions, MountFlowWidgetOptions, MountProfileOptions, PrepareArgsWithAuth, PrepareTransactionRequestPhase, ProfileButtonHandle, ProfileButtonProps, ProfilePosition, ResolveAccountParams, ResolvedAccessKeyOptions, RootCredential, RunLoginParams, RunLoginResult, SendCallsArgs, SendTransactionArgs, Session, SigningContext, SignMessageArgs, SignTypedDataArgs, Store, StoredAccessKey, WagmiConnectCapabilities, WagmiConnectParams, } from "./sdk";
10
10
  export type { ResolvedFlowSession, ResolveSessionOptions, SessionRouteOptions, SessionRouteResponse, SessionRouteSession, } from "./server";
11
11
  export type { CoinAsset, IdentifiedTx, TxApprove, TxConvert, TxSend, TxSwap, } from "./tx";
12
12
  export type { ActionDayContext, ActionEventKind, ActionSessionState, ActiveActionResponse, LevelProgress, PublicProfile, XpGrantResult, XpRecentGrant, XpSummary, } from "./xp";
@@ -187,6 +187,11 @@ export type DialogOpenOptions = {
187
187
  icon?: string;
188
188
  };
189
189
  };
190
+ /**
191
+ * Color scheme for Flow ID iframe chrome. "light dark" follows the OS, which
192
+ * is the default; a host with its own theme toggle passes the resolved value.
193
+ */
194
+ export type ColorScheme = "light" | "dark" | "light dark";
190
195
  /** Anchor for an auto-created (host-less) profile widget container. */
191
196
  export type ProfilePosition = "top-right" | "top-left" | "bottom-right" | "bottom-left";
192
197
  export type MountProfileOptions = {
@@ -209,11 +214,17 @@ export type MountProfileOptions = {
209
214
  * Color scheme for the widget chrome. Set this to match the host (the dark
210
215
  * game UI passes "dark"). Defaults to "light dark" (follows the OS).
211
216
  */
212
- theme?: "light" | "dark" | "light dark";
217
+ theme?: ColorScheme;
213
218
  /** Explicit Flow instance. Defaults to the createFlow() singleton. */
214
219
  flow?: Flow;
215
220
  };
216
221
  export type ProfileButtonHandle = {
222
+ /**
223
+ * Recolors the mounted pill (and its profile dialog) in place. A host with a
224
+ * theme toggle calls this instead of remounting: rebuilding the iframe would
225
+ * reload the widget and flash the pill out of the layout on every toggle.
226
+ */
227
+ setTheme: (theme: ColorScheme) => void;
217
228
  /** Tears down the iframe, listeners, and any auto-created container. */
218
229
  destroy: () => void;
219
230
  };
@@ -237,7 +248,7 @@ export type MountFlowWidgetOptions = {
237
248
  * Color scheme for the widget chrome. Set this to match the host (the dark
238
249
  * game UI passes "dark"). Defaults to "light dark" (follows the OS).
239
250
  */
240
- theme?: "light" | "dark" | "light dark";
251
+ theme?: ColorScheme;
241
252
  /** Explicit Flow instance. Defaults to the createFlow() singleton. */
242
253
  flow?: Flow;
243
254
  };
@@ -252,6 +263,11 @@ export type FlowWidgetHandle = {
252
263
  * the Flow ID origin.
253
264
  */
254
265
  post: (message: unknown) => void;
266
+ /**
267
+ * Recolors the mounted widget in place, for the same reason the pill exposes
268
+ * it: a remount reloads the frame and flashes it out of the host's layout.
269
+ */
270
+ setTheme: (theme: ColorScheme) => void;
255
271
  /** Tears down the iframe and its bridge. */
256
272
  destroy: () => void;
257
273
  };
@@ -263,7 +279,7 @@ export type FlowWidgetProps = {
263
279
  /** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
264
280
  host?: string;
265
281
  /** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
266
- theme?: "light" | "dark" | "light dark";
282
+ theme?: ColorScheme;
267
283
  };
268
284
  export type DialogHost = {
269
285
  open: (options?: DialogOpenOptions) => void;
@@ -364,7 +380,7 @@ export type ProfileButtonProps = {
364
380
  /** Override the Flow ID origin; defaults to the resolved Flow's `host`. */
365
381
  host?: string;
366
382
  /** Color scheme for the widget chrome; defaults to "light dark" (the OS). */
367
- theme?: "light" | "dark" | "light dark";
383
+ theme?: ColorScheme;
368
384
  };
369
385
  export type RunLoginParams = {
370
386
  dialog: DialogHost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "main": "./dist/sdk/client/index.js",
5
5
  "module": "./dist/sdk/client/index.js",
6
6
  "types": "./dist/sdk/client/index.d.ts",