@lessonkit/react 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@lessonkit/react`
2
2
 
3
- [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/checks.yml)
3
+ [![CI](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@lessonkit/react.svg)](https://www.npmjs.com/package/@lessonkit/react)
5
5
  [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](../../LICENSE)
6
6
 
@@ -56,7 +56,7 @@ export default function App() {
56
56
  }
57
57
  ```
58
58
 
59
- ## API (0.5.0)
59
+ ## API (0.6.0)
60
60
 
61
61
  ### Components
62
62
 
@@ -87,6 +87,8 @@ export default function App() {
87
87
  - `Course` accepts a `config` prop that is passed through to `LessonkitProvider` (tracking sink,
88
88
  optional `xapi.transport` or custom `xapi.client`, session metadata). Hoist `config` with `useMemo`
89
89
  so tracking/xAPI clients are not recreated every render.
90
+ - xAPI is enabled by default unless `xapi.enabled: false`. Provide `xapi.transport` or `xapi.client`
91
+ or statements are queued in memory and never sent (dev warns once).
90
92
  - A lesson is marked complete when its `<Lesson>` unmounts (for example, wizard navigation) or when
91
93
  another lesson becomes active via `setActiveLesson`. Use stable `lessonId` values so completion and
92
94
  time-on-task telemetry stay consistent.
package/dist/index.cjs CHANGED
@@ -56,16 +56,61 @@ var import_accessibility = require("@lessonkit/accessibility");
56
56
  var import_react = require("react");
57
57
  var import_core3 = require("@lessonkit/core");
58
58
  var import_xapi3 = require("@lessonkit/xapi");
59
+ var import_xapi4 = require("@lessonkit/xapi");
59
60
 
60
61
  // src/runtime/emitTelemetry.ts
61
62
  var import_core = require("@lessonkit/core");
62
63
  var import_xapi = require("@lessonkit/xapi");
64
+
65
+ // src/runtime/lxpackBridge.ts
66
+ var import_bridge = require("@lessonkit/lxpack/bridge");
67
+ function getBridge() {
68
+ if (typeof window === "undefined") return null;
69
+ const parent = window.parent;
70
+ if (!parent || parent === window) return null;
71
+ return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
72
+ }
73
+ function forwardTelemetryToLxpack(event, mode = "auto") {
74
+ if (mode === "off") return;
75
+ const bridge = getBridge();
76
+ if (!bridge) return;
77
+ switch (event.name) {
78
+ case "lesson_completed": {
79
+ const lessonId = event.lessonId;
80
+ if (lessonId) bridge.completeLesson?.(lessonId);
81
+ return;
82
+ }
83
+ case "course_completed":
84
+ bridge.completeCourse?.();
85
+ return;
86
+ case "quiz_completed": {
87
+ const data = event.data;
88
+ if (!data?.checkId) return;
89
+ const scaled = (0, import_bridge.normalizeAssessmentScore)({
90
+ score: data.score,
91
+ maxScore: data.maxScore
92
+ });
93
+ if (scaled === null) return;
94
+ bridge.submitAssessment?.({
95
+ id: data.checkId,
96
+ score: scaled,
97
+ passingScore: (0, import_bridge.normalizeAssessmentPassingScore)(data.passingScore)
98
+ });
99
+ return;
100
+ }
101
+ default:
102
+ return;
103
+ }
104
+ }
105
+
106
+ // src/runtime/emitTelemetry.ts
63
107
  var warnedMissingCourseId = false;
108
+ var warnedMissingQuizLesson = false;
64
109
  function isDevEnvironment() {
65
110
  const g = globalThis;
66
111
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
67
112
  }
68
- function emitTelemetry(tracking, xapi, event) {
113
+ function emitTelemetry(tracking, xapi, event, opts) {
69
114
  if (!event.courseId) {
70
115
  if (isDevEnvironment() && !warnedMissingCourseId) {
71
116
  warnedMissingCourseId = true;
@@ -82,6 +127,7 @@ function emitTelemetry(tracking, xapi, event) {
82
127
  console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
83
128
  }
84
129
  }
130
+ forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
85
131
  }
86
132
  function buildTrackEvent(opts) {
87
133
  const base = {
@@ -104,7 +150,7 @@ function buildTrackEvent(opts) {
104
150
  name: "lesson_started",
105
151
  ...base,
106
152
  lessonId,
107
- data: { lessonId, ...data }
153
+ data: { ...data, lessonId }
108
154
  };
109
155
  }
110
156
  case "lesson_completed":
@@ -116,7 +162,7 @@ function buildTrackEvent(opts) {
116
162
  name: opts.name,
117
163
  ...base,
118
164
  lessonId,
119
- data: { lessonId, ...data }
165
+ data: { ...data, lessonId }
120
166
  };
121
167
  }
122
168
  case "quiz_answered": {
@@ -142,6 +188,19 @@ function buildTrackEvent(opts) {
142
188
  return { name: opts.name, ...base };
143
189
  }
144
190
  }
191
+ function tryBuildTrackEvent(opts) {
192
+ const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
193
+ if (isQuiz && !opts.lessonId) {
194
+ if (isDevEnvironment() && !warnedMissingQuizLesson) {
195
+ warnedMissingQuizLesson = true;
196
+ console.warn(
197
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
198
+ );
199
+ }
200
+ return null;
201
+ }
202
+ return buildTrackEvent(opts);
203
+ }
145
204
 
146
205
  // src/runtime/ports.ts
147
206
  function createNoopStorage() {
@@ -270,6 +329,8 @@ function LessonkitProvider(props) {
270
329
  userRef.current = config.session?.user;
271
330
  const courseIdRef = (0, import_react.useRef)(config.courseId);
272
331
  courseIdRef.current = config.courseId;
332
+ const lxpackBridgeModeRef = (0, import_react.useRef)(config.lxpack?.bridge ?? "auto");
333
+ lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
273
334
  const progressRef = (0, import_react.useRef)(createProgressController());
274
335
  const [progress, setProgress] = (0, import_react.useState)(() => progressRef.current.getState());
275
336
  const syncProgress = (0, import_react.useCallback)(() => {
@@ -277,6 +338,53 @@ function LessonkitProvider(props) {
277
338
  }, []);
278
339
  const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
279
340
  activeLessonIdRef.current = progress.activeLessonId;
341
+ const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
342
+ const xapiRef = (0, import_react.useRef)(null);
343
+ const [xapi, setXapi] = (0, import_react.useState)(null);
344
+ const xapiEnabled = config.xapi?.enabled;
345
+ const xapiClient = config.xapi?.client;
346
+ const xapiTransport = config.xapi?.transport;
347
+ const courseId = config.courseId;
348
+ useIsoLayoutEffect(() => {
349
+ const prev = xapiRef.current;
350
+ const next = createXapiClientFromConfig(config, xapiQueueRef.current);
351
+ xapiRef.current = next;
352
+ setXapi(next);
353
+ if (next && !prev) {
354
+ const sessionId = sessionIdRef.current;
355
+ const cid = courseIdRef.current;
356
+ if (hasCourseStarted(defaultStorage, sessionId, cid)) {
357
+ try {
358
+ const statement = (0, import_xapi4.telemetryEventToXAPIStatement)(
359
+ buildTrackEvent({
360
+ name: "course_started",
361
+ courseId: cid,
362
+ sessionId,
363
+ attemptId: attemptIdRef.current,
364
+ user: userRef.current
365
+ })
366
+ );
367
+ if (statement) next.send(statement);
368
+ } catch {
369
+ }
370
+ }
371
+ }
372
+ void (async () => {
373
+ if (prev) {
374
+ try {
375
+ await prev.flush();
376
+ } catch {
377
+ }
378
+ }
379
+ try {
380
+ await next?.flush();
381
+ } catch {
382
+ }
383
+ })();
384
+ return () => {
385
+ void prev?.flush();
386
+ };
387
+ }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
280
388
  const trackingRef = (0, import_react.useRef)((0, import_core3.createTrackingClient)());
281
389
  const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
282
390
  const trackingEnabled = config.tracking?.enabled;
@@ -303,7 +411,8 @@ function LessonkitProvider(props) {
303
411
  sessionId,
304
412
  attemptId: attemptIdRef.current,
305
413
  user: userRef.current
306
- })
414
+ }),
415
+ { lxpackBridge: lxpackBridgeModeRef.current }
307
416
  );
308
417
  }
309
418
  return () => {
@@ -317,37 +426,17 @@ function LessonkitProvider(props) {
317
426
  batchFlushIntervalMs,
318
427
  batchMaxBatchSize
319
428
  ]);
320
- const xapiQueueRef = (0, import_react.useRef)((0, import_xapi3.createInMemoryXAPIQueue)());
321
- const xapiRef = (0, import_react.useRef)(null);
322
- const [xapi, setXapi] = (0, import_react.useState)(null);
323
- const xapiEnabled = config.xapi?.enabled;
324
- const xapiClient = config.xapi?.client;
325
- const xapiTransport = config.xapi?.transport;
326
- const courseId = config.courseId;
327
- useIsoLayoutEffect(() => {
328
- const prev = xapiRef.current;
329
- const next = createXapiClientFromConfig(config, xapiQueueRef.current);
330
- xapiRef.current = next;
331
- setXapi(next);
332
- void (async () => {
333
- if (prev) {
334
- try {
335
- await prev.flush();
336
- } catch {
337
- }
338
- }
339
- try {
340
- await next?.flush();
341
- } catch {
342
- }
343
- })();
344
- return () => {
345
- void prev?.flush();
346
- };
347
- }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
429
+ const emitWithBridge = (0, import_react.useCallback)(
430
+ (trackingClient, event) => {
431
+ emitTelemetry(trackingClient, xapiRef.current, event, {
432
+ lxpackBridge: lxpackBridgeModeRef.current
433
+ });
434
+ },
435
+ []
436
+ );
348
437
  const track = (0, import_react.useCallback)(
349
438
  (name, data, opts) => {
350
- const event = buildTrackEvent({
439
+ const event = tryBuildTrackEvent({
351
440
  name,
352
441
  courseId: courseIdRef.current,
353
442
  lessonId: opts?.lessonId ?? activeLessonIdRef.current,
@@ -356,10 +445,35 @@ function LessonkitProvider(props) {
356
445
  user: userRef.current,
357
446
  data
358
447
  });
359
- emitTelemetry(trackingRef.current, xapiRef.current, event);
448
+ if (!event) return;
449
+ emitWithBridge(trackingRef.current, event);
360
450
  },
361
- []
451
+ [emitWithBridge]
362
452
  );
453
+ const prevCourseIdRef = (0, import_react.useRef)(config.courseId);
454
+ (0, import_react.useEffect)(() => {
455
+ if (prevCourseIdRef.current === config.courseId) return;
456
+ prevCourseIdRef.current = config.courseId;
457
+ progressRef.current = createProgressController();
458
+ syncProgress();
459
+ const sessionId = sessionIdRef.current;
460
+ const cid = config.courseId;
461
+ if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
462
+ markCourseStarted(defaultStorage, sessionId, cid);
463
+ emitTelemetry(
464
+ trackingRef.current,
465
+ xapiRef.current,
466
+ buildTrackEvent({
467
+ name: "course_started",
468
+ courseId: cid,
469
+ sessionId,
470
+ attemptId: attemptIdRef.current,
471
+ user: userRef.current
472
+ }),
473
+ { lxpackBridge: lxpackBridgeModeRef.current }
474
+ );
475
+ }
476
+ }, [config.courseId, syncProgress]);
363
477
  (0, import_react.useEffect)(() => {
364
478
  return () => {
365
479
  trackingRef.current?.flush?.();
@@ -407,6 +521,9 @@ function LessonkitProvider(props) {
407
521
  syncProgress();
408
522
  track("course_completed");
409
523
  }, [track, syncProgress]);
524
+ const sessionUser = config.session?.user;
525
+ const sessionAttemptId = config.session?.attemptId;
526
+ const sessionConfiguredId = config.session?.sessionId;
410
527
  const runtime = (0, import_react.useMemo)(
411
528
  () => ({
412
529
  config,
@@ -419,7 +536,19 @@ function LessonkitProvider(props) {
419
536
  completeCourse,
420
537
  track
421
538
  }),
422
- [config, tracking, xapi, progress, setActiveLesson, completeLesson, completeCourse, track]
539
+ [
540
+ config,
541
+ tracking,
542
+ xapi,
543
+ progress,
544
+ setActiveLesson,
545
+ completeLesson,
546
+ completeCourse,
547
+ track,
548
+ sessionUser,
549
+ sessionAttemptId,
550
+ sessionConfiguredId
551
+ ]
423
552
  );
424
553
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LessonkitContext.Provider, { value: runtime, children: props.children });
425
554
  }
@@ -573,7 +702,7 @@ function Quiz(props) {
573
702
  });
574
703
  if (correct && !completedRef.current) {
575
704
  completedRef.current = true;
576
- quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1 });
705
+ quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
577
706
  }
578
707
  }
579
708
  }
package/dist/index.d.cts CHANGED
@@ -34,6 +34,10 @@ type LessonkitConfig = {
34
34
  transport?: XAPITransport;
35
35
  client?: XAPIClient;
36
36
  };
37
+ lxpack?: {
38
+ /** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
39
+ bridge?: "auto" | "off";
40
+ };
37
41
  };
38
42
 
39
43
  type LessonkitRuntime = {
@@ -114,6 +118,7 @@ declare function useQuizState(): {
114
118
  checkId: CheckId;
115
119
  score?: number;
116
120
  maxScore?: number;
121
+ passingScore?: number;
117
122
  }) => void;
118
123
  };
119
124
 
package/dist/index.d.ts CHANGED
@@ -34,6 +34,10 @@ type LessonkitConfig = {
34
34
  transport?: XAPITransport;
35
35
  client?: XAPIClient;
36
36
  };
37
+ lxpack?: {
38
+ /** Forward completion events to `window.parent.lxpackBridge.v1` when embedded (default `auto`). */
39
+ bridge?: "auto" | "off";
40
+ };
37
41
  };
38
42
 
39
43
  type LessonkitRuntime = {
@@ -114,6 +118,7 @@ declare function useQuizState(): {
114
118
  checkId: CheckId;
115
119
  score?: number;
116
120
  maxScore?: number;
121
+ passingScore?: number;
117
122
  }) => void;
118
123
  };
119
124
 
package/dist/index.js CHANGED
@@ -14,16 +14,64 @@ import {
14
14
  } from "react";
15
15
  import { createTrackingClient } from "@lessonkit/core";
16
16
  import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
17
+ import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
17
18
 
18
19
  // src/runtime/emitTelemetry.ts
19
20
  import { nowIso } from "@lessonkit/core";
20
21
  import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
22
+
23
+ // src/runtime/lxpackBridge.ts
24
+ import {
25
+ normalizeAssessmentPassingScore,
26
+ normalizeAssessmentScore
27
+ } from "@lessonkit/lxpack/bridge";
28
+ function getBridge() {
29
+ if (typeof window === "undefined") return null;
30
+ const parent = window.parent;
31
+ if (!parent || parent === window) return null;
32
+ return parent.lxpackBridge?.v1 ?? parent.lxpack ?? null;
33
+ }
34
+ function forwardTelemetryToLxpack(event, mode = "auto") {
35
+ if (mode === "off") return;
36
+ const bridge = getBridge();
37
+ if (!bridge) return;
38
+ switch (event.name) {
39
+ case "lesson_completed": {
40
+ const lessonId = event.lessonId;
41
+ if (lessonId) bridge.completeLesson?.(lessonId);
42
+ return;
43
+ }
44
+ case "course_completed":
45
+ bridge.completeCourse?.();
46
+ return;
47
+ case "quiz_completed": {
48
+ const data = event.data;
49
+ if (!data?.checkId) return;
50
+ const scaled = normalizeAssessmentScore({
51
+ score: data.score,
52
+ maxScore: data.maxScore
53
+ });
54
+ if (scaled === null) return;
55
+ bridge.submitAssessment?.({
56
+ id: data.checkId,
57
+ score: scaled,
58
+ passingScore: normalizeAssessmentPassingScore(data.passingScore)
59
+ });
60
+ return;
61
+ }
62
+ default:
63
+ return;
64
+ }
65
+ }
66
+
67
+ // src/runtime/emitTelemetry.ts
21
68
  var warnedMissingCourseId = false;
69
+ var warnedMissingQuizLesson = false;
22
70
  function isDevEnvironment() {
23
71
  const g = globalThis;
24
72
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
25
73
  }
26
- function emitTelemetry(tracking, xapi, event) {
74
+ function emitTelemetry(tracking, xapi, event, opts) {
27
75
  if (!event.courseId) {
28
76
  if (isDevEnvironment() && !warnedMissingCourseId) {
29
77
  warnedMissingCourseId = true;
@@ -40,6 +88,7 @@ function emitTelemetry(tracking, xapi, event) {
40
88
  console.warn("[lessonkit] xAPI mapping skipped:", err instanceof Error ? err.message : err);
41
89
  }
42
90
  }
91
+ forwardTelemetryToLxpack(event, opts?.lxpackBridge ?? "auto");
43
92
  }
44
93
  function buildTrackEvent(opts) {
45
94
  const base = {
@@ -62,7 +111,7 @@ function buildTrackEvent(opts) {
62
111
  name: "lesson_started",
63
112
  ...base,
64
113
  lessonId,
65
- data: { lessonId, ...data }
114
+ data: { ...data, lessonId }
66
115
  };
67
116
  }
68
117
  case "lesson_completed":
@@ -74,7 +123,7 @@ function buildTrackEvent(opts) {
74
123
  name: opts.name,
75
124
  ...base,
76
125
  lessonId,
77
- data: { lessonId, ...data }
126
+ data: { ...data, lessonId }
78
127
  };
79
128
  }
80
129
  case "quiz_answered": {
@@ -100,6 +149,19 @@ function buildTrackEvent(opts) {
100
149
  return { name: opts.name, ...base };
101
150
  }
102
151
  }
152
+ function tryBuildTrackEvent(opts) {
153
+ const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
154
+ if (isQuiz && !opts.lessonId) {
155
+ if (isDevEnvironment() && !warnedMissingQuizLesson) {
156
+ warnedMissingQuizLesson = true;
157
+ console.warn(
158
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
159
+ );
160
+ }
161
+ return null;
162
+ }
163
+ return buildTrackEvent(opts);
164
+ }
103
165
 
104
166
  // src/runtime/ports.ts
105
167
  function createNoopStorage() {
@@ -228,6 +290,8 @@ function LessonkitProvider(props) {
228
290
  userRef.current = config.session?.user;
229
291
  const courseIdRef = useRef(config.courseId);
230
292
  courseIdRef.current = config.courseId;
293
+ const lxpackBridgeModeRef = useRef(config.lxpack?.bridge ?? "auto");
294
+ lxpackBridgeModeRef.current = config.lxpack?.bridge ?? "auto";
231
295
  const progressRef = useRef(createProgressController());
232
296
  const [progress, setProgress] = useState(() => progressRef.current.getState());
233
297
  const syncProgress = useCallback(() => {
@@ -235,6 +299,53 @@ function LessonkitProvider(props) {
235
299
  }, []);
236
300
  const activeLessonIdRef = useRef(progress.activeLessonId);
237
301
  activeLessonIdRef.current = progress.activeLessonId;
302
+ const xapiQueueRef = useRef(createInMemoryXAPIQueue());
303
+ const xapiRef = useRef(null);
304
+ const [xapi, setXapi] = useState(null);
305
+ const xapiEnabled = config.xapi?.enabled;
306
+ const xapiClient = config.xapi?.client;
307
+ const xapiTransport = config.xapi?.transport;
308
+ const courseId = config.courseId;
309
+ useIsoLayoutEffect(() => {
310
+ const prev = xapiRef.current;
311
+ const next = createXapiClientFromConfig(config, xapiQueueRef.current);
312
+ xapiRef.current = next;
313
+ setXapi(next);
314
+ if (next && !prev) {
315
+ const sessionId = sessionIdRef.current;
316
+ const cid = courseIdRef.current;
317
+ if (hasCourseStarted(defaultStorage, sessionId, cid)) {
318
+ try {
319
+ const statement = telemetryEventToXAPIStatement2(
320
+ buildTrackEvent({
321
+ name: "course_started",
322
+ courseId: cid,
323
+ sessionId,
324
+ attemptId: attemptIdRef.current,
325
+ user: userRef.current
326
+ })
327
+ );
328
+ if (statement) next.send(statement);
329
+ } catch {
330
+ }
331
+ }
332
+ }
333
+ void (async () => {
334
+ if (prev) {
335
+ try {
336
+ await prev.flush();
337
+ } catch {
338
+ }
339
+ }
340
+ try {
341
+ await next?.flush();
342
+ } catch {
343
+ }
344
+ })();
345
+ return () => {
346
+ void prev?.flush();
347
+ };
348
+ }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
238
349
  const trackingRef = useRef(createTrackingClient());
239
350
  const [tracking, setTracking] = useState(() => trackingRef.current);
240
351
  const trackingEnabled = config.tracking?.enabled;
@@ -261,7 +372,8 @@ function LessonkitProvider(props) {
261
372
  sessionId,
262
373
  attemptId: attemptIdRef.current,
263
374
  user: userRef.current
264
- })
375
+ }),
376
+ { lxpackBridge: lxpackBridgeModeRef.current }
265
377
  );
266
378
  }
267
379
  return () => {
@@ -275,37 +387,17 @@ function LessonkitProvider(props) {
275
387
  batchFlushIntervalMs,
276
388
  batchMaxBatchSize
277
389
  ]);
278
- const xapiQueueRef = useRef(createInMemoryXAPIQueue());
279
- const xapiRef = useRef(null);
280
- const [xapi, setXapi] = useState(null);
281
- const xapiEnabled = config.xapi?.enabled;
282
- const xapiClient = config.xapi?.client;
283
- const xapiTransport = config.xapi?.transport;
284
- const courseId = config.courseId;
285
- useIsoLayoutEffect(() => {
286
- const prev = xapiRef.current;
287
- const next = createXapiClientFromConfig(config, xapiQueueRef.current);
288
- xapiRef.current = next;
289
- setXapi(next);
290
- void (async () => {
291
- if (prev) {
292
- try {
293
- await prev.flush();
294
- } catch {
295
- }
296
- }
297
- try {
298
- await next?.flush();
299
- } catch {
300
- }
301
- })();
302
- return () => {
303
- void prev?.flush();
304
- };
305
- }, [xapiEnabled, xapiClient, xapiTransport, courseId]);
390
+ const emitWithBridge = useCallback(
391
+ (trackingClient, event) => {
392
+ emitTelemetry(trackingClient, xapiRef.current, event, {
393
+ lxpackBridge: lxpackBridgeModeRef.current
394
+ });
395
+ },
396
+ []
397
+ );
306
398
  const track = useCallback(
307
399
  (name, data, opts) => {
308
- const event = buildTrackEvent({
400
+ const event = tryBuildTrackEvent({
309
401
  name,
310
402
  courseId: courseIdRef.current,
311
403
  lessonId: opts?.lessonId ?? activeLessonIdRef.current,
@@ -314,10 +406,35 @@ function LessonkitProvider(props) {
314
406
  user: userRef.current,
315
407
  data
316
408
  });
317
- emitTelemetry(trackingRef.current, xapiRef.current, event);
409
+ if (!event) return;
410
+ emitWithBridge(trackingRef.current, event);
318
411
  },
319
- []
412
+ [emitWithBridge]
320
413
  );
414
+ const prevCourseIdRef = useRef(config.courseId);
415
+ useEffect(() => {
416
+ if (prevCourseIdRef.current === config.courseId) return;
417
+ prevCourseIdRef.current = config.courseId;
418
+ progressRef.current = createProgressController();
419
+ syncProgress();
420
+ const sessionId = sessionIdRef.current;
421
+ const cid = config.courseId;
422
+ if (!hasCourseStarted(defaultStorage, sessionId, cid)) {
423
+ markCourseStarted(defaultStorage, sessionId, cid);
424
+ emitTelemetry(
425
+ trackingRef.current,
426
+ xapiRef.current,
427
+ buildTrackEvent({
428
+ name: "course_started",
429
+ courseId: cid,
430
+ sessionId,
431
+ attemptId: attemptIdRef.current,
432
+ user: userRef.current
433
+ }),
434
+ { lxpackBridge: lxpackBridgeModeRef.current }
435
+ );
436
+ }
437
+ }, [config.courseId, syncProgress]);
321
438
  useEffect(() => {
322
439
  return () => {
323
440
  trackingRef.current?.flush?.();
@@ -365,6 +482,9 @@ function LessonkitProvider(props) {
365
482
  syncProgress();
366
483
  track("course_completed");
367
484
  }, [track, syncProgress]);
485
+ const sessionUser = config.session?.user;
486
+ const sessionAttemptId = config.session?.attemptId;
487
+ const sessionConfiguredId = config.session?.sessionId;
368
488
  const runtime = useMemo(
369
489
  () => ({
370
490
  config,
@@ -377,7 +497,19 @@ function LessonkitProvider(props) {
377
497
  completeCourse,
378
498
  track
379
499
  }),
380
- [config, tracking, xapi, progress, setActiveLesson, completeLesson, completeCourse, track]
500
+ [
501
+ config,
502
+ tracking,
503
+ xapi,
504
+ progress,
505
+ setActiveLesson,
506
+ completeLesson,
507
+ completeCourse,
508
+ track,
509
+ sessionUser,
510
+ sessionAttemptId,
511
+ sessionConfiguredId
512
+ ]
381
513
  );
382
514
  return /* @__PURE__ */ jsx(LessonkitContext.Provider, { value: runtime, children: props.children });
383
515
  }
@@ -531,7 +663,7 @@ function Quiz(props) {
531
663
  });
532
664
  if (correct && !completedRef.current) {
533
665
  completedRef.current = true;
534
- quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1 });
666
+ quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
535
667
  }
536
668
  }
537
669
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -37,8 +37,8 @@
37
37
  "dist"
38
38
  ],
39
39
  "scripts": {
40
- "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
41
- "dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/themes",
40
+ "build": "tsup src/index.tsx --format esm,cjs --dts --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
41
+ "dev": "tsup src/index.tsx --format esm,cjs --dts --watch --external react --external react-dom --external @lessonkit/accessibility --external @lessonkit/lxpack --external @lessonkit/themes",
42
42
  "prepublishOnly": "npm run build",
43
43
  "typecheck": "tsc -p tsconfig.json",
44
44
  "test": "vitest run --passWithNoTests",
@@ -50,10 +50,11 @@
50
50
  "react-dom": ">=18"
51
51
  },
52
52
  "dependencies": {
53
- "@lessonkit/accessibility": "0.5.0",
54
- "@lessonkit/core": "0.5.0",
55
- "@lessonkit/themes": "0.5.0",
56
- "@lessonkit/xapi": "0.5.0"
53
+ "@lessonkit/accessibility": "0.6.0",
54
+ "@lessonkit/core": "0.6.0",
55
+ "@lessonkit/lxpack": "0.6.0",
56
+ "@lessonkit/themes": "0.6.0",
57
+ "@lessonkit/xapi": "0.6.0"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@testing-library/react": "^16.3.0",