@lessonkit/react 0.9.0 → 0.9.2

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
@@ -59,7 +59,7 @@ export default function App() {
59
59
  }
60
60
  ```
61
61
 
62
- ## API (0.9.0)
62
+ ## API (0.9.2)
63
63
 
64
64
  ### Block catalog
65
65
 
package/dist/index.cjs CHANGED
@@ -369,9 +369,15 @@ function createTrackingClientFromConfig(config) {
369
369
  batch: config.tracking?.batch
370
370
  });
371
371
  }
372
- function disposeTrackingClient(client) {
373
- client?.flush?.();
374
- client?.dispose?.();
372
+ async function disposeTrackingClient(client) {
373
+ try {
374
+ await client?.flush?.();
375
+ } catch {
376
+ }
377
+ try {
378
+ await client?.dispose?.();
379
+ } catch {
380
+ }
375
381
  }
376
382
 
377
383
  // src/context.tsx
@@ -382,6 +388,33 @@ var defaultStorage = createSessionStoragePort();
382
388
  function isTrackingActive(tracking) {
383
389
  return tracking?.enabled !== false;
384
390
  }
391
+ function emitCourseStarted(opts) {
392
+ const pluginCtx = buildPluginContext({
393
+ courseId: opts.courseId,
394
+ sessionId: opts.sessionId,
395
+ attemptId: opts.attemptId
396
+ });
397
+ try {
398
+ emitTelemetryWithPlugins({
399
+ pluginHost: opts.pluginHost,
400
+ tracking: opts.tracking,
401
+ xapi: opts.xapi,
402
+ event: buildTrackEvent({
403
+ name: "course_started",
404
+ courseId: opts.courseId,
405
+ sessionId: opts.sessionId,
406
+ attemptId: opts.attemptId,
407
+ user: opts.user
408
+ }),
409
+ pluginCtx,
410
+ lxpackBridge: opts.lxpackBridge
411
+ });
412
+ markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
413
+ return true;
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
385
418
  function LessonkitProvider(props) {
386
419
  const config = props.config;
387
420
  const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
@@ -428,7 +461,17 @@ function LessonkitProvider(props) {
428
461
  const courseId = config.courseId;
429
462
  const trackingEnabled = config.tracking?.enabled;
430
463
  useIsoLayoutEffect(() => {
431
- if (prevXapiCourseIdRef.current !== courseId) {
464
+ const courseChanged = prevXapiCourseIdRef.current !== courseId;
465
+ if (courseChanged) {
466
+ if (config.xapi?.client) {
467
+ const g = globalThis;
468
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
469
+ console.warn(
470
+ "[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
471
+ );
472
+ }
473
+ void xapiRef.current?.flush();
474
+ }
432
475
  xapiQueueRef.current = (0, import_xapi3.createInMemoryXAPIQueue)();
433
476
  prevXapiCourseIdRef.current = courseId;
434
477
  }
@@ -452,7 +495,10 @@ function LessonkitProvider(props) {
452
495
  user: userRef.current
453
496
  })
454
497
  );
455
- if (statement) next.send(statement);
498
+ if (statement) {
499
+ next.send(statement);
500
+ markCourseStarted(defaultStorage, sessionId, cid);
501
+ }
456
502
  } catch {
457
503
  }
458
504
  }
@@ -484,17 +530,27 @@ function LessonkitProvider(props) {
484
530
  const batchEnabled = config.tracking?.batch?.enabled;
485
531
  const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
486
532
  const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
487
- useIsoLayoutEffect(() => {
488
- const prev = trackingRef.current;
489
- const pluginCtx = buildPluginContext({
533
+ const buildCurrentPluginCtx = (0, import_react.useCallback)(
534
+ () => buildPluginContext({
490
535
  courseId: courseIdRef.current,
491
536
  sessionId: sessionIdRef.current,
492
537
  attemptId: attemptIdRef.current
493
- });
494
- const sink = pluginHostRef.current?.composeTrackingSink(config.tracking?.sink, pluginCtx) ?? config.tracking?.sink;
538
+ }),
539
+ []
540
+ );
541
+ useIsoLayoutEffect(() => {
542
+ const prev = trackingRef.current;
543
+ const baseSink = config.tracking?.sink;
544
+ const sink = pluginHostRef.current && baseSink ? (event) => {
545
+ const composed = pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx()) ?? baseSink;
546
+ return composed(event);
547
+ } : baseSink;
495
548
  const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
496
- const filtered = pluginHostRef.current.runTelemetryBatch(events, pluginCtx);
497
- return config.tracking.batchSink(filtered);
549
+ const delivered = pluginHostRef.current.deliverTelemetryBatch(
550
+ events,
551
+ buildCurrentPluginCtx()
552
+ );
553
+ return config.tracking.batchSink(delivered);
498
554
  } : config.tracking?.batchSink;
499
555
  const next = createTrackingClientFromConfig({
500
556
  tracking: { ...config.tracking, sink, batchSink }
@@ -508,32 +564,24 @@ function LessonkitProvider(props) {
508
564
  if (!trackingActive) {
509
565
  courseStartedEmittedToSinkRef.current = false;
510
566
  } else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
511
- markCourseStarted(defaultStorage, sessionId, cid);
512
- emitTelemetryWithPlugins({
567
+ const emitted = emitCourseStarted({
513
568
  pluginHost: pluginHostRef.current,
514
569
  tracking: next,
515
570
  xapi: xapiRef.current,
516
- event: buildTrackEvent({
517
- name: "course_started",
518
- courseId: cid,
519
- sessionId,
520
- attemptId: attemptIdRef.current,
521
- user: userRef.current
522
- }),
523
- pluginCtx: buildPluginContext({
524
- courseId: cid,
525
- sessionId,
526
- attemptId: attemptIdRef.current
527
- }),
571
+ storage: defaultStorage,
572
+ sessionId,
573
+ courseId: cid,
574
+ attemptId: attemptIdRef.current,
575
+ user: userRef.current,
528
576
  lxpackBridge: lxpackBridgeModeRef.current
529
577
  });
530
- courseStartedEmittedToSinkRef.current = true;
578
+ courseStartedEmittedToSinkRef.current = emitted;
531
579
  } else if (trackingActive) {
532
580
  courseStartedEmittedToSinkRef.current = true;
533
581
  }
534
582
  return () => {
535
583
  if (prev !== trackingRef.current) {
536
- disposeTrackingClient(prev);
584
+ void disposeTrackingClient(prev);
537
585
  }
538
586
  };
539
587
  }, [
@@ -543,7 +591,9 @@ function LessonkitProvider(props) {
543
591
  batchEnabled,
544
592
  batchFlushIntervalMs,
545
593
  batchMaxBatchSize,
546
- config.plugins
594
+ config.plugins,
595
+ config.courseId,
596
+ buildCurrentPluginCtx
547
597
  ]);
548
598
  const emitWithBridge = (0, import_react.useCallback)((trackingClient, event) => {
549
599
  emitTelemetryWithPlugins({
@@ -582,28 +632,26 @@ function LessonkitProvider(props) {
582
632
  if (!isTrackingActive(config.tracking)) return;
583
633
  const sessionId = sessionIdRef.current;
584
634
  const cid = courseIdRef.current;
585
- if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
586
- markCourseStarted(defaultStorage, sessionId, cid);
587
- emitTelemetryWithPlugins({
588
- pluginHost: pluginHostRef.current,
589
- tracking: trackingRef.current,
590
- xapi: xapiRef.current,
591
- event: buildTrackEvent({
592
- name: "course_started",
593
- courseId: cid,
635
+ void (async () => {
636
+ try {
637
+ await trackingRef.current?.flush?.();
638
+ } catch {
639
+ }
640
+ if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
641
+ const emitted = emitCourseStarted({
642
+ pluginHost: pluginHostRef.current,
643
+ tracking: trackingRef.current,
644
+ xapi: xapiRef.current,
645
+ storage: defaultStorage,
594
646
  sessionId,
595
- attemptId: attemptIdRef.current,
596
- user: userRef.current
597
- }),
598
- pluginCtx: buildPluginContext({
599
647
  courseId: cid,
600
- sessionId,
601
- attemptId: attemptIdRef.current
602
- }),
603
- lxpackBridge: lxpackBridgeModeRef.current
604
- });
605
- courseStartedEmittedToSinkRef.current = true;
606
- }
648
+ attemptId: attemptIdRef.current,
649
+ user: userRef.current,
650
+ lxpackBridge: lxpackBridgeModeRef.current
651
+ });
652
+ courseStartedEmittedToSinkRef.current = emitted;
653
+ }
654
+ })();
607
655
  }, [config.courseId, config.tracking?.enabled, syncProgress]);
608
656
  const emitLessonCompleted = (0, import_react.useCallback)(
609
657
  (lessonId, durationMs) => {
@@ -620,25 +668,27 @@ function LessonkitProvider(props) {
620
668
  if (!result.didComplete) return;
621
669
  syncProgress();
622
670
  emitLessonCompleted(lessonId, result.durationMs);
623
- void trackingRef.current?.flush?.();
671
+ void Promise.resolve(trackingRef.current?.flush?.());
624
672
  },
625
673
  [syncProgress, emitLessonCompleted]
626
674
  );
627
- const unmountTimerIdsRef = (0, import_react.useRef)([]);
628
675
  (0, import_react.useEffect)(() => {
629
676
  return () => {
630
- for (const id of unmountTimerIdsRef.current) clearTimeout(id);
631
- unmountTimerIdsRef.current = [];
632
677
  const client = trackingClientForUnmountRef.current;
633
- void xapiRef.current?.flush();
634
- const flushTimer = setTimeout(() => {
635
- client?.flush?.();
636
- const disposeTimer = setTimeout(() => {
637
- client?.dispose?.();
638
- }, 0);
639
- unmountTimerIdsRef.current.push(disposeTimer);
640
- }, 0);
641
- unmountTimerIdsRef.current.push(flushTimer);
678
+ void (async () => {
679
+ try {
680
+ await xapiRef.current?.flush();
681
+ } catch {
682
+ }
683
+ try {
684
+ await client?.flush?.();
685
+ } catch {
686
+ }
687
+ try {
688
+ await client?.dispose?.();
689
+ } catch {
690
+ }
691
+ })();
642
692
  };
643
693
  }, []);
644
694
  const setActiveLesson = (0, import_react.useCallback)(
@@ -650,6 +700,7 @@ function LessonkitProvider(props) {
650
700
  const completed = progressRef.current.completeLesson(previous, Date.now());
651
701
  if (completed.didComplete) {
652
702
  emitLessonCompleted(previous, completed.durationMs);
703
+ void Promise.resolve(trackingRef.current?.flush?.());
653
704
  }
654
705
  }
655
706
  progressRef.current.setActiveLesson(lessonId, Date.now());
@@ -854,13 +905,24 @@ function KnowledgeCheck(props) {
854
905
  function Quiz(props) {
855
906
  warnInvalidComponentId(props.checkId, "checkId");
856
907
  const quiz = useQuizState();
908
+ const { plugins, config, progress, session } = useLessonkit();
857
909
  const [selected, setSelected] = (0, import_react3.useState)(null);
910
+ const [selectionCorrect, setSelectionCorrect] = (0, import_react3.useState)(null);
858
911
  const completedRef = (0, import_react3.useRef)(false);
859
912
  const questionId = (0, import_react3.useId)();
860
913
  (0, import_react3.useEffect)(() => {
861
914
  completedRef.current = false;
862
915
  setSelected(null);
916
+ setSelectionCorrect(null);
863
917
  }, [props.checkId, props.answer, props.question]);
918
+ const isChoiceCorrect = (choice, custom) => {
919
+ if (!custom) return choice === props.answer;
920
+ if (custom.passed !== void 0) return custom.passed;
921
+ if (custom.maxScore != null && custom.maxScore > 0) {
922
+ return custom.score / custom.maxScore >= 1;
923
+ }
924
+ return choice === props.answer;
925
+ };
864
926
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
865
927
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
866
928
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { "aria-labelledby": questionId, children: [
@@ -875,7 +937,21 @@ function Quiz(props) {
875
937
  checked: selected === c,
876
938
  onChange: () => {
877
939
  setSelected(c);
878
- const correct = c === props.answer;
940
+ const pluginCtx = buildPluginContext({
941
+ courseId: config.courseId,
942
+ sessionId: session.sessionId,
943
+ attemptId: session.attemptId
944
+ });
945
+ const custom = plugins?.scoreAssessment(
946
+ {
947
+ checkId: props.checkId,
948
+ lessonId: progress.activeLessonId,
949
+ response: c
950
+ },
951
+ pluginCtx
952
+ ) ?? null;
953
+ const correct = isChoiceCorrect(c, custom);
954
+ setSelectionCorrect(correct);
879
955
  quiz.answer({
880
956
  checkId: props.checkId,
881
957
  question: props.question,
@@ -884,7 +960,12 @@ function Quiz(props) {
884
960
  });
885
961
  if (correct && !completedRef.current) {
886
962
  completedRef.current = true;
887
- quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
963
+ quiz.complete({
964
+ checkId: props.checkId,
965
+ score: custom?.score ?? 1,
966
+ maxScore: custom?.maxScore ?? 1,
967
+ passingScore: 1
968
+ });
888
969
  }
889
970
  }
890
971
  }
@@ -892,7 +973,7 @@ function Quiz(props) {
892
973
  c
893
974
  ] }, `${questionId}-${i}`))
894
975
  ] }),
895
- selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selected === props.answer ? "Correct" : "Try again" }) : null
976
+ selected && selectionCorrect !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
896
977
  ] });
897
978
  }
898
979
  function ProgressTracker() {
package/dist/index.js CHANGED
@@ -327,9 +327,15 @@ function createTrackingClientFromConfig(config) {
327
327
  batch: config.tracking?.batch
328
328
  });
329
329
  }
330
- function disposeTrackingClient(client) {
331
- client?.flush?.();
332
- client?.dispose?.();
330
+ async function disposeTrackingClient(client) {
331
+ try {
332
+ await client?.flush?.();
333
+ } catch {
334
+ }
335
+ try {
336
+ await client?.dispose?.();
337
+ } catch {
338
+ }
333
339
  }
334
340
 
335
341
  // src/context.tsx
@@ -340,6 +346,33 @@ var defaultStorage = createSessionStoragePort();
340
346
  function isTrackingActive(tracking) {
341
347
  return tracking?.enabled !== false;
342
348
  }
349
+ function emitCourseStarted(opts) {
350
+ const pluginCtx = buildPluginContext({
351
+ courseId: opts.courseId,
352
+ sessionId: opts.sessionId,
353
+ attemptId: opts.attemptId
354
+ });
355
+ try {
356
+ emitTelemetryWithPlugins({
357
+ pluginHost: opts.pluginHost,
358
+ tracking: opts.tracking,
359
+ xapi: opts.xapi,
360
+ event: buildTrackEvent({
361
+ name: "course_started",
362
+ courseId: opts.courseId,
363
+ sessionId: opts.sessionId,
364
+ attemptId: opts.attemptId,
365
+ user: opts.user
366
+ }),
367
+ pluginCtx,
368
+ lxpackBridge: opts.lxpackBridge
369
+ });
370
+ markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
371
+ return true;
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
343
376
  function LessonkitProvider(props) {
344
377
  const config = props.config;
345
378
  const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
@@ -386,7 +419,17 @@ function LessonkitProvider(props) {
386
419
  const courseId = config.courseId;
387
420
  const trackingEnabled = config.tracking?.enabled;
388
421
  useIsoLayoutEffect(() => {
389
- if (prevXapiCourseIdRef.current !== courseId) {
422
+ const courseChanged = prevXapiCourseIdRef.current !== courseId;
423
+ if (courseChanged) {
424
+ if (config.xapi?.client) {
425
+ const g = globalThis;
426
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production") {
427
+ console.warn(
428
+ "[lessonkit] courseId changed while using config.xapi.client; flush the client between courses or use config.xapi.transport so the provider can manage the queue."
429
+ );
430
+ }
431
+ void xapiRef.current?.flush();
432
+ }
390
433
  xapiQueueRef.current = createInMemoryXAPIQueue();
391
434
  prevXapiCourseIdRef.current = courseId;
392
435
  }
@@ -410,7 +453,10 @@ function LessonkitProvider(props) {
410
453
  user: userRef.current
411
454
  })
412
455
  );
413
- if (statement) next.send(statement);
456
+ if (statement) {
457
+ next.send(statement);
458
+ markCourseStarted(defaultStorage, sessionId, cid);
459
+ }
414
460
  } catch {
415
461
  }
416
462
  }
@@ -442,17 +488,27 @@ function LessonkitProvider(props) {
442
488
  const batchEnabled = config.tracking?.batch?.enabled;
443
489
  const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
444
490
  const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
445
- useIsoLayoutEffect(() => {
446
- const prev = trackingRef.current;
447
- const pluginCtx = buildPluginContext({
491
+ const buildCurrentPluginCtx = useCallback(
492
+ () => buildPluginContext({
448
493
  courseId: courseIdRef.current,
449
494
  sessionId: sessionIdRef.current,
450
495
  attemptId: attemptIdRef.current
451
- });
452
- const sink = pluginHostRef.current?.composeTrackingSink(config.tracking?.sink, pluginCtx) ?? config.tracking?.sink;
496
+ }),
497
+ []
498
+ );
499
+ useIsoLayoutEffect(() => {
500
+ const prev = trackingRef.current;
501
+ const baseSink = config.tracking?.sink;
502
+ const sink = pluginHostRef.current && baseSink ? (event) => {
503
+ const composed = pluginHostRef.current.composeTrackingSink(baseSink, buildCurrentPluginCtx()) ?? baseSink;
504
+ return composed(event);
505
+ } : baseSink;
453
506
  const batchSink = pluginHostRef.current && config.tracking?.batchSink ? (events) => {
454
- const filtered = pluginHostRef.current.runTelemetryBatch(events, pluginCtx);
455
- return config.tracking.batchSink(filtered);
507
+ const delivered = pluginHostRef.current.deliverTelemetryBatch(
508
+ events,
509
+ buildCurrentPluginCtx()
510
+ );
511
+ return config.tracking.batchSink(delivered);
456
512
  } : config.tracking?.batchSink;
457
513
  const next = createTrackingClientFromConfig({
458
514
  tracking: { ...config.tracking, sink, batchSink }
@@ -466,32 +522,24 @@ function LessonkitProvider(props) {
466
522
  if (!trackingActive) {
467
523
  courseStartedEmittedToSinkRef.current = false;
468
524
  } else if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
469
- markCourseStarted(defaultStorage, sessionId, cid);
470
- emitTelemetryWithPlugins({
525
+ const emitted = emitCourseStarted({
471
526
  pluginHost: pluginHostRef.current,
472
527
  tracking: next,
473
528
  xapi: xapiRef.current,
474
- event: buildTrackEvent({
475
- name: "course_started",
476
- courseId: cid,
477
- sessionId,
478
- attemptId: attemptIdRef.current,
479
- user: userRef.current
480
- }),
481
- pluginCtx: buildPluginContext({
482
- courseId: cid,
483
- sessionId,
484
- attemptId: attemptIdRef.current
485
- }),
529
+ storage: defaultStorage,
530
+ sessionId,
531
+ courseId: cid,
532
+ attemptId: attemptIdRef.current,
533
+ user: userRef.current,
486
534
  lxpackBridge: lxpackBridgeModeRef.current
487
535
  });
488
- courseStartedEmittedToSinkRef.current = true;
536
+ courseStartedEmittedToSinkRef.current = emitted;
489
537
  } else if (trackingActive) {
490
538
  courseStartedEmittedToSinkRef.current = true;
491
539
  }
492
540
  return () => {
493
541
  if (prev !== trackingRef.current) {
494
- disposeTrackingClient(prev);
542
+ void disposeTrackingClient(prev);
495
543
  }
496
544
  };
497
545
  }, [
@@ -501,7 +549,9 @@ function LessonkitProvider(props) {
501
549
  batchEnabled,
502
550
  batchFlushIntervalMs,
503
551
  batchMaxBatchSize,
504
- config.plugins
552
+ config.plugins,
553
+ config.courseId,
554
+ buildCurrentPluginCtx
505
555
  ]);
506
556
  const emitWithBridge = useCallback((trackingClient, event) => {
507
557
  emitTelemetryWithPlugins({
@@ -540,28 +590,26 @@ function LessonkitProvider(props) {
540
590
  if (!isTrackingActive(config.tracking)) return;
541
591
  const sessionId = sessionIdRef.current;
542
592
  const cid = courseIdRef.current;
543
- if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
544
- markCourseStarted(defaultStorage, sessionId, cid);
545
- emitTelemetryWithPlugins({
546
- pluginHost: pluginHostRef.current,
547
- tracking: trackingRef.current,
548
- xapi: xapiRef.current,
549
- event: buildTrackEvent({
550
- name: "course_started",
551
- courseId: cid,
593
+ void (async () => {
594
+ try {
595
+ await trackingRef.current?.flush?.();
596
+ } catch {
597
+ }
598
+ if (!courseStartedEmittedToSinkRef.current && !hasCourseStarted(defaultStorage, sessionId, cid)) {
599
+ const emitted = emitCourseStarted({
600
+ pluginHost: pluginHostRef.current,
601
+ tracking: trackingRef.current,
602
+ xapi: xapiRef.current,
603
+ storage: defaultStorage,
552
604
  sessionId,
553
- attemptId: attemptIdRef.current,
554
- user: userRef.current
555
- }),
556
- pluginCtx: buildPluginContext({
557
605
  courseId: cid,
558
- sessionId,
559
- attemptId: attemptIdRef.current
560
- }),
561
- lxpackBridge: lxpackBridgeModeRef.current
562
- });
563
- courseStartedEmittedToSinkRef.current = true;
564
- }
606
+ attemptId: attemptIdRef.current,
607
+ user: userRef.current,
608
+ lxpackBridge: lxpackBridgeModeRef.current
609
+ });
610
+ courseStartedEmittedToSinkRef.current = emitted;
611
+ }
612
+ })();
565
613
  }, [config.courseId, config.tracking?.enabled, syncProgress]);
566
614
  const emitLessonCompleted = useCallback(
567
615
  (lessonId, durationMs) => {
@@ -578,25 +626,27 @@ function LessonkitProvider(props) {
578
626
  if (!result.didComplete) return;
579
627
  syncProgress();
580
628
  emitLessonCompleted(lessonId, result.durationMs);
581
- void trackingRef.current?.flush?.();
629
+ void Promise.resolve(trackingRef.current?.flush?.());
582
630
  },
583
631
  [syncProgress, emitLessonCompleted]
584
632
  );
585
- const unmountTimerIdsRef = useRef([]);
586
633
  useEffect(() => {
587
634
  return () => {
588
- for (const id of unmountTimerIdsRef.current) clearTimeout(id);
589
- unmountTimerIdsRef.current = [];
590
635
  const client = trackingClientForUnmountRef.current;
591
- void xapiRef.current?.flush();
592
- const flushTimer = setTimeout(() => {
593
- client?.flush?.();
594
- const disposeTimer = setTimeout(() => {
595
- client?.dispose?.();
596
- }, 0);
597
- unmountTimerIdsRef.current.push(disposeTimer);
598
- }, 0);
599
- unmountTimerIdsRef.current.push(flushTimer);
636
+ void (async () => {
637
+ try {
638
+ await xapiRef.current?.flush();
639
+ } catch {
640
+ }
641
+ try {
642
+ await client?.flush?.();
643
+ } catch {
644
+ }
645
+ try {
646
+ await client?.dispose?.();
647
+ } catch {
648
+ }
649
+ })();
600
650
  };
601
651
  }, []);
602
652
  const setActiveLesson = useCallback(
@@ -608,6 +658,7 @@ function LessonkitProvider(props) {
608
658
  const completed = progressRef.current.completeLesson(previous, Date.now());
609
659
  if (completed.didComplete) {
610
660
  emitLessonCompleted(previous, completed.durationMs);
661
+ void Promise.resolve(trackingRef.current?.flush?.());
611
662
  }
612
663
  }
613
664
  progressRef.current.setActiveLesson(lessonId, Date.now());
@@ -812,13 +863,24 @@ function KnowledgeCheck(props) {
812
863
  function Quiz(props) {
813
864
  warnInvalidComponentId(props.checkId, "checkId");
814
865
  const quiz = useQuizState();
866
+ const { plugins, config, progress, session } = useLessonkit();
815
867
  const [selected, setSelected] = useState2(null);
868
+ const [selectionCorrect, setSelectionCorrect] = useState2(null);
816
869
  const completedRef = useRef2(false);
817
870
  const questionId = useId();
818
871
  useEffect2(() => {
819
872
  completedRef.current = false;
820
873
  setSelected(null);
874
+ setSelectionCorrect(null);
821
875
  }, [props.checkId, props.answer, props.question]);
876
+ const isChoiceCorrect = (choice, custom) => {
877
+ if (!custom) return choice === props.answer;
878
+ if (custom.passed !== void 0) return custom.passed;
879
+ if (custom.maxScore != null && custom.maxScore > 0) {
880
+ return custom.score / custom.maxScore >= 1;
881
+ }
882
+ return choice === props.answer;
883
+ };
822
884
  return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": props.checkId, children: [
823
885
  /* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
824
886
  /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
@@ -833,7 +895,21 @@ function Quiz(props) {
833
895
  checked: selected === c,
834
896
  onChange: () => {
835
897
  setSelected(c);
836
- const correct = c === props.answer;
898
+ const pluginCtx = buildPluginContext({
899
+ courseId: config.courseId,
900
+ sessionId: session.sessionId,
901
+ attemptId: session.attemptId
902
+ });
903
+ const custom = plugins?.scoreAssessment(
904
+ {
905
+ checkId: props.checkId,
906
+ lessonId: progress.activeLessonId,
907
+ response: c
908
+ },
909
+ pluginCtx
910
+ ) ?? null;
911
+ const correct = isChoiceCorrect(c, custom);
912
+ setSelectionCorrect(correct);
837
913
  quiz.answer({
838
914
  checkId: props.checkId,
839
915
  question: props.question,
@@ -842,7 +918,12 @@ function Quiz(props) {
842
918
  });
843
919
  if (correct && !completedRef.current) {
844
920
  completedRef.current = true;
845
- quiz.complete({ checkId: props.checkId, score: 1, maxScore: 1, passingScore: 1 });
921
+ quiz.complete({
922
+ checkId: props.checkId,
923
+ score: custom?.score ?? 1,
924
+ maxScore: custom?.maxScore ?? 1,
925
+ passingScore: 1
926
+ });
846
927
  }
847
928
  }
848
929
  }
@@ -850,7 +931,7 @@ function Quiz(props) {
850
931
  c
851
932
  ] }, `${questionId}-${i}`))
852
933
  ] }),
853
- selected ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selected === props.answer ? "Correct" : "Try again" }) : null
934
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx2("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
854
935
  ] });
855
936
  }
856
937
  function ProgressTracker() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/react",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "private": false,
5
5
  "description": "React components and hooks for building learning experiences with LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -54,11 +54,11 @@
54
54
  "react-dom": ">=18"
55
55
  },
56
56
  "dependencies": {
57
- "@lessonkit/accessibility": "0.9.0",
58
- "@lessonkit/core": "0.9.0",
59
- "@lessonkit/lxpack": "0.9.0",
60
- "@lessonkit/themes": "0.9.0",
61
- "@lessonkit/xapi": "0.9.0"
57
+ "@lessonkit/accessibility": "0.9.2",
58
+ "@lessonkit/core": "0.9.2",
59
+ "@lessonkit/lxpack": "0.9.2",
60
+ "@lessonkit/themes": "0.9.2",
61
+ "@lessonkit/xapi": "0.9.2"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@testing-library/react": "^16.3.0",