@nexbasira/react 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NexBasira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @nexbasira/react
2
+
3
+ React component + hook wrapping the [`@nexbasira/embed`](../embed) browser widget.
4
+
5
+ Thin by design — the iframe and postMessage plumbing live in `@nexbasira/embed`; this package just makes the widget feel native to a React app (mount, unmount, ref-based imperative access, fresh-closure callbacks).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @nexbasira/react @nexbasira/embed react
11
+ ```
12
+
13
+ `react` and `@nexbasira/embed` are peer dependencies — they aren't bundled.
14
+
15
+ ## Quick start (component)
16
+
17
+ ```tsx
18
+ import { NexBasiraSession } from "@nexbasira/react";
19
+
20
+ function InspectionPage({ joinUrl }: { joinUrl: string }) {
21
+ return (
22
+ <NexBasiraSession
23
+ sessionUrl={joinUrl}
24
+ height="720px"
25
+ onSessionComplete={(id) => {
26
+ console.log("inspection done:", id);
27
+ }}
28
+ />
29
+ );
30
+ }
31
+ ```
32
+
33
+ `sessionUrl` is the field-join URL your backend mints via `POST /v1/public/sessions/{id}/invites` (or the SPA equivalent). Never embed an API secret in the URL.
34
+
35
+ ## Imperative control (ref)
36
+
37
+ ```tsx
38
+ import { useRef } from "react";
39
+ import { NexBasiraSession, type NexBasiraSessionHandle } from "@nexbasira/react";
40
+
41
+ function ToolbarHost({ joinUrl }: { joinUrl: string }) {
42
+ const session = useRef<NexBasiraSessionHandle>(null);
43
+ return (
44
+ <>
45
+ <button onClick={() => session.current?.requestSnapshot()}>
46
+ Capture
47
+ </button>
48
+ <NexBasiraSession ref={session} sessionUrl={joinUrl} />
49
+ </>
50
+ );
51
+ }
52
+ ```
53
+
54
+ Methods on the handle:
55
+
56
+ - `requestSnapshot()`
57
+ - `openWhiteboard()` / `closeWhiteboard()`
58
+ - `switchCamera()`
59
+ - `mute()` / `unmute()`
60
+ - `endSession()`
61
+
62
+ ## Hook variant
63
+
64
+ Use this when `<NexBasiraSession>`'s wrapper `<div>` doesn't fit your layout, or when you want to share the widget handle across multiple components without prop-drilling refs.
65
+
66
+ ```tsx
67
+ import { useNexBasiraSession } from "@nexbasira/react";
68
+
69
+ function CustomLayout({ joinUrl }: { joinUrl: string }) {
70
+ const [containerRef, widget] = useNexBasiraSession({
71
+ sessionUrl: joinUrl,
72
+ onSessionComplete: (id) => navigate(`/inspections/${id}`),
73
+ });
74
+ return (
75
+ <div className="my-layout">
76
+ <header>
77
+ <button onClick={() => widget.current?.requestSnapshot()}>
78
+ Snap
79
+ </button>
80
+ </header>
81
+ <div ref={containerRef} style={{ flex: 1 }} />
82
+ </div>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ## Lifecycle callbacks
88
+
89
+ Every callback supported by `@nexbasira/embed` is passed through unchanged:
90
+
91
+ | Prop | Fires when |
92
+ |---|---|
93
+ | `onReady` | The iframe finished loading + handshake completed. |
94
+ | `onSessionJoined(id)` | The field user joined the session. |
95
+ | `onSessionComplete(id)` | The session was closed (final state, recording paths queued). |
96
+ | `onEvidenceAdded(ev)` | A new evidence row was created (snapshot, whiteboard, etc.). |
97
+ | `onWhiteboardOpened` / `onWhiteboardSaved(ev)` | Whiteboard lifecycle. |
98
+ | `onParticipantJoined(p)` / `onParticipantLeft(p)` | Participant changes. |
99
+ | `onError({message, code})` | Anything the iframe surfaces as an error. |
100
+
101
+ Callbacks are always re-read from the latest props on every re-render — closing over fresh state in your handlers works without remounting the iframe.
102
+
103
+ ## When the iframe rebuilds
104
+
105
+ The iframe is re-created **only** when `sessionUrl` changes. Every other prop (callbacks, `width`, `height`, `expectedOrigin`) is applied without remounting, so toolbars and overlays stay continuous through a session.
106
+
107
+ ## TypeScript
108
+
109
+ `NexBasiraSessionProps`, `NexBasiraSessionHandle`, `EmbedOptions`, and `EmbedWidget` are all exported. The component is a `forwardRef`, so `useRef<NexBasiraSessionHandle>()` gives you typed handle access.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,42 @@
1
+ import type { EmbedOptions, EmbedWidget } from "@nexbasira/embed";
2
+ import { type CSSProperties, type Ref } from "react";
3
+ /** Props for `<NexBasiraSession>` — superset of `EmbedOptions`
4
+ * minus `container` (the component manages its own div). Adds a
5
+ * `className` + `style` so callers can position the wrapper. */
6
+ export type NexBasiraSessionProps = Omit<EmbedOptions, "container"> & {
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ };
10
+ /** Imperative handle exposed via `ref` — mirrors the methods on
11
+ * `EmbedWidget`, so callers get the same shape they'd get from
12
+ * `@nexbasira/embed` directly. */
13
+ export interface NexBasiraSessionHandle {
14
+ requestSnapshot(): void;
15
+ openWhiteboard(): void;
16
+ closeWhiteboard(): void;
17
+ switchCamera(): void;
18
+ mute(): void;
19
+ unmute(): void;
20
+ endSession(): void;
21
+ /** Phase 1B-I — recording lifecycle from the host page. Routes
22
+ * through to the iframe's RecordingControl. */
23
+ startRecording(): void;
24
+ stopRecording(): void;
25
+ pauseRecording(): void;
26
+ resumeRecording(): void;
27
+ }
28
+ export declare const NexBasiraSession: import("react").ForwardRefExoticComponent<Omit<EmbedOptions, "container"> & {
29
+ className?: string;
30
+ style?: CSSProperties;
31
+ } & import("react").RefAttributes<NexBasiraSessionHandle>>;
32
+ /** Hook variant. Returns `[containerRef, widget]`. Attach
33
+ * `containerRef` to the host element (typically a `<div>`), and
34
+ * call methods on `widget.current` once the component is mounted.
35
+ *
36
+ * Use this when `<NexBasiraSession>`'s built-in wrapper div
37
+ * doesn't fit your layout, or when you want fine-grained control
38
+ * over when the embed widget initialises. */
39
+ export declare function useNexBasiraSession(options: Omit<EmbedOptions, "container">): [Ref<HTMLDivElement>, {
40
+ current: EmbedWidget | null;
41
+ }];
42
+ export type { EmbedOptions, EmbedWidget } from "@nexbasira/embed";
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @nexbasira/react — React wrapper around `@nexbasira/embed`.
4
+ *
5
+ * Two surfaces:
6
+ *
7
+ * 1. `<NexBasiraSession>` — drop-in component for the common
8
+ * case. Render it with `sessionUrl` and the lifecycle props
9
+ * you care about; the iframe lives inside, the widget gets
10
+ * created on mount, the listener is torn down on unmount.
11
+ *
12
+ * 2. `useNexBasiraSession()` — hook for callers that need
13
+ * imperative access (forwarding camera commands from a
14
+ * separate toolbar, hooking into an existing layout). Returns
15
+ * both the container ref to attach + the widget handle so
16
+ * you can call `.requestSnapshot()` etc. from anywhere.
17
+ *
18
+ * The wrapper is intentionally a leaf — it doesn't restyle the
19
+ * iframe or impose layout decisions, and lifecycle callbacks pass
20
+ * through unchanged so existing `@nexbasira/embed` consumers can
21
+ * migrate without rewiring handler signatures.
22
+ *
23
+ * Usage:
24
+ * import { NexBasiraSession } from "@nexbasira/react";
25
+ *
26
+ * <NexBasiraSession
27
+ * sessionUrl={mintedFieldJoinUrl}
28
+ * onSessionComplete={(id) => router.push(`/inspections/${id}`)}
29
+ * height="720px"
30
+ * />
31
+ */
32
+ import { embed } from "@nexbasira/embed";
33
+ import { forwardRef, useEffect, useImperativeHandle, useRef, } from "react";
34
+ export const NexBasiraSession = forwardRef(function NexBasiraSession(props, ref) {
35
+ const { className, style, ...embedProps } = props;
36
+ const containerRef = useRef(null);
37
+ const widgetRef = useRef(null);
38
+ // Stash the latest props in a ref so the embed widget's
39
+ // lifecycle callbacks always read fresh closures — without
40
+ // this, a parent that re-renders with a new onSessionComplete
41
+ // (e.g. closing over fresh state) would see the stale one
42
+ // because the embed widget was created with the first mount's
43
+ // props.
44
+ const propsRef = useRef(embedProps);
45
+ propsRef.current = embedProps;
46
+ useEffect(() => {
47
+ if (!containerRef.current)
48
+ return;
49
+ const widget = embed({
50
+ ...propsRef.current,
51
+ container: containerRef.current,
52
+ // Wrap each callback so the embed widget always invokes the
53
+ // current prop value, not the snapshot from the mount.
54
+ onReady: () => propsRef.current.onReady?.(),
55
+ onSessionJoined: (id) => propsRef.current.onSessionJoined?.(id),
56
+ onSessionComplete: (id) => propsRef.current.onSessionComplete?.(id),
57
+ onEvidenceAdded: (ev) => propsRef.current.onEvidenceAdded?.(ev),
58
+ onWhiteboardOpened: () => propsRef.current.onWhiteboardOpened?.(),
59
+ onWhiteboardSaved: (ev) => propsRef.current.onWhiteboardSaved?.(ev),
60
+ onParticipantJoined: (p) => propsRef.current.onParticipantJoined?.(p),
61
+ onParticipantLeft: (p) => propsRef.current.onParticipantLeft?.(p),
62
+ onError: (err) => propsRef.current.onError?.(err),
63
+ });
64
+ widgetRef.current = widget;
65
+ return () => {
66
+ widget.destroy();
67
+ widgetRef.current = null;
68
+ };
69
+ // Intentionally re-create the iframe only when sessionUrl
70
+ // changes — every other prop is read through propsRef. A
71
+ // sessionUrl change means a different session; the iframe
72
+ // must reload.
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [embedProps.sessionUrl]);
75
+ useImperativeHandle(ref, () => ({
76
+ requestSnapshot: () => widgetRef.current?.requestSnapshot(),
77
+ openWhiteboard: () => widgetRef.current?.openWhiteboard(),
78
+ closeWhiteboard: () => widgetRef.current?.closeWhiteboard(),
79
+ switchCamera: () => widgetRef.current?.switchCamera(),
80
+ mute: () => widgetRef.current?.mute(),
81
+ unmute: () => widgetRef.current?.unmute(),
82
+ endSession: () => widgetRef.current?.endSession(),
83
+ startRecording: () => widgetRef.current?.startRecording(),
84
+ stopRecording: () => widgetRef.current?.stopRecording(),
85
+ pauseRecording: () => widgetRef.current?.pauseRecording(),
86
+ resumeRecording: () => widgetRef.current?.resumeRecording(),
87
+ }), []);
88
+ return _jsx("div", { ref: containerRef, className: className, style: style });
89
+ });
90
+ /** Hook variant. Returns `[containerRef, widget]`. Attach
91
+ * `containerRef` to the host element (typically a `<div>`), and
92
+ * call methods on `widget.current` once the component is mounted.
93
+ *
94
+ * Use this when `<NexBasiraSession>`'s built-in wrapper div
95
+ * doesn't fit your layout, or when you want fine-grained control
96
+ * over when the embed widget initialises. */
97
+ export function useNexBasiraSession(options) {
98
+ const containerRef = useRef(null);
99
+ const widgetRef = useRef(null);
100
+ const propsRef = useRef(options);
101
+ propsRef.current = options;
102
+ useEffect(() => {
103
+ if (!containerRef.current)
104
+ return;
105
+ const widget = embed({
106
+ ...propsRef.current,
107
+ container: containerRef.current,
108
+ onReady: () => propsRef.current.onReady?.(),
109
+ onSessionJoined: (id) => propsRef.current.onSessionJoined?.(id),
110
+ onSessionComplete: (id) => propsRef.current.onSessionComplete?.(id),
111
+ onEvidenceAdded: (ev) => propsRef.current.onEvidenceAdded?.(ev),
112
+ onWhiteboardOpened: () => propsRef.current.onWhiteboardOpened?.(),
113
+ onWhiteboardSaved: (ev) => propsRef.current.onWhiteboardSaved?.(ev),
114
+ onParticipantJoined: (p) => propsRef.current.onParticipantJoined?.(p),
115
+ onParticipantLeft: (p) => propsRef.current.onParticipantLeft?.(p),
116
+ onError: (err) => propsRef.current.onError?.(err),
117
+ });
118
+ widgetRef.current = widget;
119
+ return () => {
120
+ widget.destroy();
121
+ widgetRef.current = null;
122
+ };
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, [options.sessionUrl]);
125
+ return [containerRef, widgetRef];
126
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nexbasira/react",
3
+ "version": "0.1.0",
4
+ "description": "React component + hook wrapping the NexBasira embed widget.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md", "LICENSE"],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/codelounge-io/certivisiopro_backend.git",
27
+ "directory": "sdks/react"
28
+ },
29
+ "homepage": "https://github.com/codelounge-io/certivisiopro_backend/tree/main/sdks/react",
30
+ "bugs": {
31
+ "url": "https://github.com/codelounge-io/certivisiopro_backend/issues"
32
+ },
33
+ "keywords": [
34
+ "nexbasira",
35
+ "remote-inspection",
36
+ "react",
37
+ "component",
38
+ "embed",
39
+ "iframe"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=17.0.0",
46
+ "@nexbasira/embed": "^0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@nexbasira/embed": "file:../embed",
50
+ "@testing-library/react": "^15.0.0",
51
+ "@types/react": "^18.2.0",
52
+ "jsdom": "^24.0.0",
53
+ "react": "^18.2.0",
54
+ "react-dom": "^18.2.0",
55
+ "typescript": "^5.4.0",
56
+ "vitest": "^1.5.0"
57
+ }
58
+ }