@incodetech/core 2.0.0-alpha.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.
Files changed (84) hide show
  1. package/package.json +51 -0
  2. package/src/camera/cameraActor.ts +21 -0
  3. package/src/camera/cameraService.test.ts +437 -0
  4. package/src/camera/cameraService.ts +165 -0
  5. package/src/camera/cameraServices.test.ts +66 -0
  6. package/src/camera/cameraServices.ts +26 -0
  7. package/src/camera/cameraStateMachine.test.ts +602 -0
  8. package/src/camera/cameraStateMachine.ts +264 -0
  9. package/src/camera/index.ts +5 -0
  10. package/src/camera/types.ts +17 -0
  11. package/src/device/getBrowser.ts +31 -0
  12. package/src/device/getDeviceClass.ts +29 -0
  13. package/src/device/index.ts +2 -0
  14. package/src/email/__mocks__/emailMocks.ts +59 -0
  15. package/src/email/emailActor.ts +15 -0
  16. package/src/email/emailManager.test.ts +573 -0
  17. package/src/email/emailManager.ts +427 -0
  18. package/src/email/emailServices.ts +66 -0
  19. package/src/email/emailStateMachine.test.ts +741 -0
  20. package/src/email/emailStateMachine.ts +367 -0
  21. package/src/email/index.ts +39 -0
  22. package/src/email/types.ts +60 -0
  23. package/src/events/addEvent.ts +20 -0
  24. package/src/events/types.ts +7 -0
  25. package/src/flow/__mocks__/flowMocks.ts +84 -0
  26. package/src/flow/flowActor.ts +13 -0
  27. package/src/flow/flowAnalyzer.test.ts +266 -0
  28. package/src/flow/flowAnalyzer.ts +37 -0
  29. package/src/flow/flowCompletionService.ts +21 -0
  30. package/src/flow/flowManager.test.ts +560 -0
  31. package/src/flow/flowManager.ts +235 -0
  32. package/src/flow/flowServices.test.ts +109 -0
  33. package/src/flow/flowServices.ts +13 -0
  34. package/src/flow/flowStateMachine.test.ts +334 -0
  35. package/src/flow/flowStateMachine.ts +182 -0
  36. package/src/flow/index.ts +21 -0
  37. package/src/flow/moduleLoader.test.ts +136 -0
  38. package/src/flow/moduleLoader.ts +73 -0
  39. package/src/flow/orchestratedFlowManager.test.ts +240 -0
  40. package/src/flow/orchestratedFlowManager.ts +231 -0
  41. package/src/flow/orchestratedFlowStateMachine.test.ts +199 -0
  42. package/src/flow/orchestratedFlowStateMachine.ts +325 -0
  43. package/src/flow/types.ts +434 -0
  44. package/src/http/__mocks__/api.ts +88 -0
  45. package/src/http/api.test.ts +231 -0
  46. package/src/http/api.ts +90 -0
  47. package/src/http/endpoints.ts +17 -0
  48. package/src/index.ts +33 -0
  49. package/src/permissions/index.ts +2 -0
  50. package/src/permissions/permissionServices.ts +31 -0
  51. package/src/permissions/types.ts +3 -0
  52. package/src/phone/__mocks__/phoneMocks.ts +71 -0
  53. package/src/phone/index.ts +39 -0
  54. package/src/phone/phoneActor.ts +15 -0
  55. package/src/phone/phoneManager.test.ts +393 -0
  56. package/src/phone/phoneManager.ts +458 -0
  57. package/src/phone/phoneServices.ts +98 -0
  58. package/src/phone/phoneStateMachine.test.ts +918 -0
  59. package/src/phone/phoneStateMachine.ts +422 -0
  60. package/src/phone/types.ts +83 -0
  61. package/src/recordings/recordingsRepository.test.ts +87 -0
  62. package/src/recordings/recordingsRepository.ts +48 -0
  63. package/src/recordings/streamingEvents.ts +10 -0
  64. package/src/selfie/__mocks__/selfieMocks.ts +26 -0
  65. package/src/selfie/index.ts +14 -0
  66. package/src/selfie/selfieActor.ts +17 -0
  67. package/src/selfie/selfieErrorUtils.test.ts +116 -0
  68. package/src/selfie/selfieErrorUtils.ts +66 -0
  69. package/src/selfie/selfieManager.test.ts +297 -0
  70. package/src/selfie/selfieManager.ts +301 -0
  71. package/src/selfie/selfieServices.ts +362 -0
  72. package/src/selfie/selfieStateMachine.test.ts +283 -0
  73. package/src/selfie/selfieStateMachine.ts +804 -0
  74. package/src/selfie/selfieUploadService.test.ts +90 -0
  75. package/src/selfie/selfieUploadService.ts +81 -0
  76. package/src/selfie/types.ts +103 -0
  77. package/src/session/index.ts +5 -0
  78. package/src/session/sessionService.ts +78 -0
  79. package/src/setup.test.ts +61 -0
  80. package/src/setup.ts +171 -0
  81. package/tsconfig.json +13 -0
  82. package/tsdown.config.ts +22 -0
  83. package/vitest.config.ts +37 -0
  84. package/vitest.setup.ts +135 -0
@@ -0,0 +1,804 @@
1
+ import {
2
+ type AnyStateMachine,
3
+ assign,
4
+ type FaceCoordinates,
5
+ type FaceDetectionProvider,
6
+ fromCallback,
7
+ fromPromise,
8
+ type IncodeCanvas,
9
+ type StateMachine,
10
+ StreamCanvasCapture,
11
+ setup,
12
+ } from '@incodetech/infra';
13
+ import { addEvent } from '../events/addEvent';
14
+ import {
15
+ checkPermission,
16
+ requestPermission,
17
+ } from '../permissions/permissionServices';
18
+ import {
19
+ getFaceErrorCodeFromUnknown,
20
+ validateUploadResponse,
21
+ } from './selfieErrorUtils';
22
+ import {
23
+ type CameraStream,
24
+ encryptSelfieImage,
25
+ initializeCamera,
26
+ startDetection,
27
+ startRecordingSession,
28
+ stopRecording,
29
+ stopStream,
30
+ uploadSelfie,
31
+ } from './selfieServices';
32
+ import type {
33
+ DetectionStatus,
34
+ FaceErrorCode,
35
+ PermissionResult,
36
+ RecordingSession,
37
+ SelfieConfig,
38
+ SendFaceImageResponse,
39
+ } from './types';
40
+ import { FACE_ERROR_CODES } from './types';
41
+
42
+ export type SelfieContext = {
43
+ config: SelfieConfig;
44
+ stream: CameraStream | undefined;
45
+ provider: FaceDetectionProvider | undefined;
46
+ frameCapturer: StreamCanvasCapture | undefined;
47
+ error: string | undefined;
48
+ detectionStatus: DetectionStatus;
49
+ debugFrame: ImageData | undefined;
50
+ capturedImage: IncodeCanvas | undefined;
51
+ faceCoordinates: FaceCoordinates | undefined;
52
+ uploadResponse: SendFaceImageResponse | undefined;
53
+ recordingSession: RecordingSession | undefined;
54
+ attemptsRemaining: number;
55
+ uploadError: FaceErrorCode | undefined;
56
+ permissionResult: PermissionResult | 'refresh' | undefined;
57
+ resetDetection: (() => void) | undefined;
58
+ };
59
+
60
+ export type SelfieEvent =
61
+ | { type: 'LOAD' }
62
+ | { type: 'NEXT_STEP' }
63
+ | { type: 'REQUEST_PERMISSION' }
64
+ | { type: 'GO_TO_LEARN_MORE' }
65
+ | { type: 'BACK' }
66
+ | { type: 'QUIT' }
67
+ | { type: 'RESET' }
68
+ | { type: 'MANUAL_CAPTURE' }
69
+ | { type: 'DETECTION_UPDATE'; status: DetectionStatus }
70
+ | { type: 'DETECTION_FRAME'; frame: ImageData }
71
+ | {
72
+ type: 'DETECTION_SUCCESS';
73
+ canvas: IncodeCanvas;
74
+ faceCoordinates?: FaceCoordinates;
75
+ }
76
+ | { type: 'DETECTION_RESET_READY'; reset: () => void }
77
+ | { type: 'RETRY_CAPTURE' };
78
+
79
+ export type SelfieInput = {
80
+ config: SelfieConfig;
81
+ };
82
+
83
+ const _selfieMachine = setup({
84
+ types: {
85
+ context: {} as SelfieContext,
86
+ events: {} as SelfieEvent,
87
+ input: {} as SelfieInput,
88
+ },
89
+ actors: {
90
+ checkPermission: fromPromise<PermissionResult, void>(async () => {
91
+ return checkPermission();
92
+ }),
93
+ requestPermission: fromPromise<PermissionResult, void>(async () => {
94
+ return requestPermission();
95
+ }),
96
+ initializeCamera: fromPromise<
97
+ { stream: CameraStream; provider: FaceDetectionProvider },
98
+ SelfieConfig
99
+ >(async ({ input }) => {
100
+ return initializeCamera(input);
101
+ }),
102
+ runDetection: fromCallback<
103
+ SelfieEvent,
104
+ {
105
+ frameCapturer: StreamCanvasCapture | undefined;
106
+ provider: FaceDetectionProvider | undefined;
107
+ config: SelfieConfig;
108
+ }
109
+ >(({ input, sendBack }) => {
110
+ if (!input.frameCapturer || !input.provider) {
111
+ sendBack({ type: 'DETECTION_UPDATE', status: 'error' });
112
+ return () => {};
113
+ }
114
+ const { cleanup, reset } = startDetection({
115
+ config: input.config,
116
+ capturer: input.frameCapturer,
117
+ onUpdate: (status: DetectionStatus) =>
118
+ sendBack({ type: 'DETECTION_UPDATE', status }),
119
+ onFrame: (frame: ImageData) =>
120
+ sendBack({ type: 'DETECTION_FRAME', frame }),
121
+ onSuccess: (canvas: IncodeCanvas, faceCoordinates?: FaceCoordinates) =>
122
+ sendBack({ type: 'DETECTION_SUCCESS', canvas, faceCoordinates }),
123
+ provider: input.provider,
124
+ });
125
+ sendBack({ type: 'DETECTION_RESET_READY', reset });
126
+ return cleanup;
127
+ }),
128
+ uploadSelfie: fromPromise<
129
+ SendFaceImageResponse,
130
+ { canvas: IncodeCanvas; faceCoordinates?: FaceCoordinates }
131
+ >(async ({ input, signal }) => {
132
+ const encryptedBase64Image = await encryptSelfieImage({
133
+ canvas: input.canvas,
134
+ });
135
+ return uploadSelfie({
136
+ encryptedBase64Image,
137
+ faceCoordinates: input.faceCoordinates,
138
+ signal,
139
+ });
140
+ }),
141
+ startRecording: fromPromise<
142
+ RecordingSession | undefined,
143
+ {
144
+ config: SelfieConfig;
145
+ stream: CameraStream | undefined;
146
+ existing?: RecordingSession;
147
+ }
148
+ >(async ({ input }) => {
149
+ if (!input.stream) {
150
+ return undefined;
151
+ }
152
+ return startRecordingSession({
153
+ config: input.config,
154
+ stream: input.stream,
155
+ existing: input.existing,
156
+ });
157
+ }),
158
+ },
159
+ actions: {
160
+ stopMediaStream: assign(({ context }) => {
161
+ context.frameCapturer?.dispose();
162
+ if (context.stream) {
163
+ stopStream(context.stream);
164
+ }
165
+ void context.provider?.dispose();
166
+ return {
167
+ stream: undefined,
168
+ provider: undefined,
169
+ frameCapturer: undefined,
170
+ };
171
+ }),
172
+ setStreamAndCapturer: assign({
173
+ stream: ({ event }) => {
174
+ if ('output' in event) {
175
+ return (event.output as { stream: CameraStream }).stream;
176
+ }
177
+ return undefined;
178
+ },
179
+ provider: ({ event }) => {
180
+ if ('output' in event) {
181
+ return (event.output as { provider: FaceDetectionProvider }).provider;
182
+ }
183
+ return undefined;
184
+ },
185
+ frameCapturer: ({ event }) => {
186
+ if ('output' in event) {
187
+ return new StreamCanvasCapture(
188
+ (event.output as { stream: CameraStream }).stream,
189
+ );
190
+ }
191
+ return undefined;
192
+ },
193
+ }),
194
+ trackTutorialSelfie: () => {
195
+ addEvent({
196
+ code: 'tutorialSelfie',
197
+ payload: { tutorialSelfie: true },
198
+ });
199
+ },
200
+ trackContinue: () => {
201
+ addEvent({
202
+ code: 'continue',
203
+ });
204
+ },
205
+ resetContext: assign(({ context }) => ({
206
+ stream: undefined,
207
+ provider: undefined,
208
+ frameCapturer: undefined,
209
+ error: undefined,
210
+ detectionStatus: 'idle' as DetectionStatus,
211
+ debugFrame: undefined,
212
+ capturedImage: undefined,
213
+ faceCoordinates: undefined,
214
+ uploadResponse: undefined,
215
+ recordingSession: undefined,
216
+ attemptsRemaining: context.config.captureAttempts,
217
+ uploadError: undefined,
218
+ permissionResult: undefined,
219
+ resetDetection: undefined,
220
+ })),
221
+ resetDetection: ({ context }) => {
222
+ context.resetDetection?.();
223
+ },
224
+ captureImage: assign({
225
+ capturedImage: ({ context }) => {
226
+ if (context.capturedImage) {
227
+ return context.capturedImage;
228
+ }
229
+ return context.frameCapturer?.getLatestCanvas() ?? undefined;
230
+ },
231
+ }),
232
+ captureLatestFrame: assign({
233
+ capturedImage: ({ context }) => {
234
+ return context.frameCapturer?.getLatestCanvas() ?? undefined;
235
+ },
236
+ }),
237
+ clearUploadFailure: assign({
238
+ uploadError: () => undefined,
239
+ detectionStatus: () => 'idle' as DetectionStatus,
240
+ capturedImage: () => undefined,
241
+ }),
242
+ decrementAttemptsRemaining: assign(({ context }) => ({
243
+ attemptsRemaining: context.attemptsRemaining - 1,
244
+ })),
245
+ setUploadErrorFromUploadValidation: assign({
246
+ uploadError: ({ context }) =>
247
+ validateUploadResponse({
248
+ config: context.config,
249
+ response: context.uploadResponse,
250
+ }) ?? FACE_ERROR_CODES.SERVER,
251
+ }),
252
+ stopMediaRecording: ({ context }) => {
253
+ if (context.recordingSession) {
254
+ stopRecording(context.recordingSession);
255
+ }
256
+ },
257
+ clearRecordingSession: assign({
258
+ recordingSession: () => undefined,
259
+ }),
260
+ },
261
+ guards: {
262
+ hasShowTutorial: ({ context }) => context.config.showTutorial,
263
+ isPermissionGranted: ({ event }) => {
264
+ if ('output' in event) {
265
+ return event.output === 'granted';
266
+ }
267
+ return false;
268
+ },
269
+ isPermissionDeniedError: ({ event }) => {
270
+ if ('error' in event) {
271
+ const error = event.error as Error;
272
+ return (
273
+ error?.name === 'NotAllowedError' ||
274
+ error?.name === 'PermissionDeniedError'
275
+ );
276
+ }
277
+ return false;
278
+ },
279
+ hasStream: ({ context }) => context.stream !== undefined,
280
+ hasAttemptsRemaining: ({ context }) => context.attemptsRemaining > 0,
281
+ hasCapturedImage: ({ context }) => context.capturedImage !== undefined,
282
+ hasUploadValidationError: ({ context }) =>
283
+ validateUploadResponse({
284
+ config: context.config,
285
+ response: context.uploadResponse,
286
+ }) !== undefined,
287
+ },
288
+ }).createMachine({
289
+ id: 'selfie',
290
+ initial: 'idle',
291
+ context: ({ input }) => ({
292
+ config: input.config,
293
+ stream: undefined,
294
+ provider: undefined,
295
+ frameCapturer: undefined,
296
+ error: undefined,
297
+ detectionStatus: 'idle',
298
+ debugFrame: undefined,
299
+ capturedImage: undefined,
300
+ faceCoordinates: undefined,
301
+ uploadResponse: undefined,
302
+ recordingSession: undefined,
303
+ attemptsRemaining: input.config.captureAttempts,
304
+ uploadError: undefined,
305
+ permissionResult: undefined,
306
+ resetDetection: undefined,
307
+ }),
308
+ on: {
309
+ QUIT: {
310
+ target: '#selfie.closed',
311
+ },
312
+ },
313
+ states: {
314
+ idle: {
315
+ on: {
316
+ LOAD: [
317
+ {
318
+ target: 'tutorial',
319
+ guard: 'hasShowTutorial',
320
+ },
321
+ {
322
+ target: 'loading',
323
+ },
324
+ ],
325
+ },
326
+ },
327
+
328
+ loading: {
329
+ invoke: {
330
+ id: 'checkPermissionLoading',
331
+ src: 'checkPermission',
332
+ onDone: [
333
+ {
334
+ target: 'capture',
335
+ guard: 'isPermissionGranted',
336
+ actions: assign({
337
+ permissionResult: ({ event }) => event.output,
338
+ }),
339
+ },
340
+ {
341
+ target: 'permissions',
342
+ actions: assign({
343
+ permissionResult: ({ event }) => event.output,
344
+ }),
345
+ },
346
+ ],
347
+ },
348
+ },
349
+
350
+ tutorial: {
351
+ initial: 'checkingPermission',
352
+ entry: 'trackTutorialSelfie',
353
+ states: {
354
+ checkingPermission: {
355
+ invoke: {
356
+ id: 'checkPermissionTutorial',
357
+ src: 'checkPermission',
358
+ onDone: [
359
+ {
360
+ target: 'initializingCamera',
361
+ guard: 'isPermissionGranted',
362
+ actions: assign({
363
+ permissionResult: ({ event }) => event.output,
364
+ }),
365
+ },
366
+ {
367
+ target: 'ready',
368
+ actions: assign({
369
+ permissionResult: ({ event }) => event.output,
370
+ }),
371
+ },
372
+ ],
373
+ },
374
+ },
375
+ initializingCamera: {
376
+ initial: 'booting',
377
+ invoke: {
378
+ id: 'tutorialInitCamera',
379
+ src: 'initializeCamera',
380
+ input: ({ context }) => context.config,
381
+ onDone: {
382
+ actions: 'setStreamAndCapturer',
383
+ },
384
+ onError: [
385
+ {
386
+ target: 'ready',
387
+ guard: 'isPermissionDeniedError',
388
+ actions: assign({
389
+ permissionResult: () => 'denied',
390
+ }),
391
+ },
392
+ {
393
+ target: 'ready',
394
+ actions: assign({
395
+ error: ({ event }) => String(event.error),
396
+ }),
397
+ },
398
+ ],
399
+ },
400
+ states: {
401
+ booting: {
402
+ always: [
403
+ {
404
+ target: '#tutorialCameraReady',
405
+ guard: 'hasStream',
406
+ },
407
+ ],
408
+ on: {
409
+ NEXT_STEP: {
410
+ target: 'waitingForStream',
411
+ actions: 'trackContinue',
412
+ },
413
+ },
414
+ },
415
+ waitingForStream: {
416
+ always: [
417
+ {
418
+ target: '#selfie.capture',
419
+ guard: 'hasStream',
420
+ },
421
+ ],
422
+ },
423
+ },
424
+ },
425
+ cameraReady: {
426
+ id: 'tutorialCameraReady',
427
+ on: {
428
+ NEXT_STEP: {
429
+ target: '#selfie.capture',
430
+ actions: 'trackContinue',
431
+ },
432
+ },
433
+ },
434
+ ready: {
435
+ on: {
436
+ NEXT_STEP: {
437
+ target: 'waitingForPermission',
438
+ actions: 'trackContinue',
439
+ },
440
+ },
441
+ },
442
+ waitingForPermission: {
443
+ invoke: {
444
+ id: 'checkPermissionWaiting',
445
+ src: 'checkPermission',
446
+ onDone: [
447
+ {
448
+ target: '#selfie.capture',
449
+ guard: 'isPermissionGranted',
450
+ actions: assign({
451
+ permissionResult: ({ event }) => event.output,
452
+ }),
453
+ },
454
+ {
455
+ target: '#selfie.permissions',
456
+ actions: assign({
457
+ permissionResult: ({ event }) => event.output,
458
+ }),
459
+ },
460
+ ],
461
+ },
462
+ },
463
+ },
464
+ },
465
+
466
+ permissions: {
467
+ initial: 'idle',
468
+ states: {
469
+ idle: {
470
+ invoke: {
471
+ id: 'checkPermissionIdle',
472
+ src: 'checkPermission',
473
+ onDone: [
474
+ {
475
+ target: '#selfie.capture',
476
+ guard: 'isPermissionGranted',
477
+ actions: assign({
478
+ permissionResult: ({ event }) => event.output,
479
+ }),
480
+ },
481
+ {
482
+ target: 'denied',
483
+ guard: ({ event }) => event.output === 'denied',
484
+ actions: assign({
485
+ permissionResult: ({ event }) => event.output,
486
+ }),
487
+ },
488
+ ],
489
+ },
490
+ on: {
491
+ REQUEST_PERMISSION: 'requesting',
492
+ GO_TO_LEARN_MORE: 'learnMore',
493
+ },
494
+ },
495
+ learnMore: {
496
+ on: {
497
+ BACK: 'idle',
498
+ REQUEST_PERMISSION: 'requesting',
499
+ },
500
+ },
501
+ requesting: {
502
+ invoke: {
503
+ id: 'requestPermission',
504
+ src: 'requestPermission',
505
+ onDone: [
506
+ {
507
+ target: '#selfie.capture',
508
+ guard: 'isPermissionGranted',
509
+ actions: assign({
510
+ permissionResult: ({ event }) => event.output,
511
+ }),
512
+ },
513
+ {
514
+ target: 'denied',
515
+ guard: ({ event }) => event.output === 'denied',
516
+ actions: assign({
517
+ permissionResult: ({ event }) => event.output,
518
+ }),
519
+ },
520
+ {
521
+ target: 'idle',
522
+ actions: assign({
523
+ permissionResult: ({ event }) => event.output,
524
+ }),
525
+ },
526
+ ],
527
+ onError: {
528
+ target: 'denied',
529
+ },
530
+ },
531
+ },
532
+ denied: {
533
+ entry: assign({
534
+ permissionResult: () => 'refresh',
535
+ }),
536
+ },
537
+ },
538
+ },
539
+ capture: {
540
+ initial: 'checkingStream',
541
+ exit: ['stopMediaRecording', 'clearRecordingSession', 'stopMediaStream'],
542
+ states: {
543
+ checkingStream: {
544
+ always: [
545
+ {
546
+ target: 'detecting',
547
+ guard: 'hasStream',
548
+ },
549
+ { target: 'initializing' },
550
+ ],
551
+ },
552
+ initializing: {
553
+ invoke: {
554
+ id: 'initializeCamera',
555
+ src: 'initializeCamera',
556
+ input: ({ context }) => context.config,
557
+ onDone: {
558
+ target: 'detecting',
559
+ actions: 'setStreamAndCapturer',
560
+ },
561
+ onError: [
562
+ {
563
+ target: '#selfie.permissions',
564
+ guard: 'isPermissionDeniedError',
565
+ actions: assign({
566
+ permissionResult: () => 'denied',
567
+ }),
568
+ },
569
+ {
570
+ target: '#selfie.error',
571
+ actions: assign({
572
+ error: ({ event }) => String(event.error),
573
+ }),
574
+ },
575
+ ],
576
+ },
577
+ },
578
+ detecting: {
579
+ entry: [
580
+ assign({
581
+ detectionStatus: () => 'detecting',
582
+ }),
583
+ ],
584
+ invoke: [
585
+ {
586
+ id: 'startRecording',
587
+ src: 'startRecording',
588
+ input: ({ context }) => ({
589
+ config: context.config,
590
+ stream: context.stream,
591
+ existing: context.recordingSession,
592
+ }),
593
+ onDone: {
594
+ actions: assign({
595
+ recordingSession: ({ context, event }) => {
596
+ const output = (event as { output?: RecordingSession })
597
+ .output;
598
+ return output ?? context.recordingSession;
599
+ },
600
+ }),
601
+ },
602
+ onError: {
603
+ actions: () => undefined,
604
+ },
605
+ },
606
+ {
607
+ id: 'runDetection',
608
+ src: 'runDetection',
609
+ input: ({ context }) => ({
610
+ frameCapturer: context.frameCapturer,
611
+ provider: context.provider,
612
+ config: context.config,
613
+ }),
614
+ },
615
+ ],
616
+ on: {
617
+ DETECTION_UPDATE: {
618
+ actions: assign({
619
+ detectionStatus: ({ event }) => event.status,
620
+ }),
621
+ },
622
+ DETECTION_FRAME: {
623
+ actions: assign({
624
+ debugFrame: ({ event }) => event.frame,
625
+ }),
626
+ },
627
+ DETECTION_RESET_READY: {
628
+ actions: assign({
629
+ resetDetection: ({ event }) => event.reset,
630
+ }),
631
+ },
632
+ DETECTION_SUCCESS: {
633
+ target: 'capturing',
634
+ actions: assign({
635
+ capturedImage: ({ event }) => event.canvas,
636
+ faceCoordinates: ({ event }) => event.faceCoordinates,
637
+ }),
638
+ },
639
+ MANUAL_CAPTURE: {
640
+ target: 'capturingManual',
641
+ },
642
+ },
643
+ },
644
+ capturing: {
645
+ entry: ['captureImage'],
646
+ always: [
647
+ {
648
+ target: 'uploading',
649
+ guard: 'hasCapturedImage',
650
+ },
651
+ {
652
+ target: 'uploadError',
653
+ actions: assign(({ context }) => ({
654
+ uploadError: FACE_ERROR_CODES.SERVER,
655
+ attemptsRemaining: context.attemptsRemaining - 1,
656
+ })),
657
+ },
658
+ ],
659
+ },
660
+ capturingManual: {
661
+ entry: ['captureLatestFrame'],
662
+ always: [
663
+ {
664
+ target: 'uploading',
665
+ guard: 'hasCapturedImage',
666
+ },
667
+ {
668
+ target: 'uploadError',
669
+ actions: assign(({ context }) => ({
670
+ uploadError: FACE_ERROR_CODES.SERVER,
671
+ attemptsRemaining: context.attemptsRemaining - 1,
672
+ })),
673
+ },
674
+ ],
675
+ },
676
+ uploading: {
677
+ invoke: {
678
+ id: 'uploadSelfie',
679
+ src: 'uploadSelfie',
680
+ input: ({ context }) => {
681
+ const canvas = context.capturedImage;
682
+ if (!canvas) {
683
+ throw new Error(FACE_ERROR_CODES.SERVER);
684
+ }
685
+ return {
686
+ canvas,
687
+ faceCoordinates: context.faceCoordinates,
688
+ };
689
+ },
690
+ onDone: {
691
+ target: 'validatingUpload',
692
+ actions: assign({
693
+ uploadResponse: ({ event }) => event.output,
694
+ }),
695
+ },
696
+ onError: {
697
+ target: 'uploadError',
698
+ actions: assign(({ context, event }) => ({
699
+ uploadError:
700
+ getFaceErrorCodeFromUnknown(event.error) ??
701
+ FACE_ERROR_CODES.SERVER,
702
+ attemptsRemaining: context.attemptsRemaining - 1,
703
+ })),
704
+ },
705
+ },
706
+ },
707
+ validatingUpload: {
708
+ always: [
709
+ {
710
+ target: 'uploadError',
711
+ guard: 'hasUploadValidationError',
712
+ actions: [
713
+ 'setUploadErrorFromUploadValidation',
714
+ 'decrementAttemptsRemaining',
715
+ ],
716
+ },
717
+ { target: 'success' },
718
+ ],
719
+ },
720
+ uploadError: {
721
+ on: {
722
+ RETRY_CAPTURE: {
723
+ target: 'detecting',
724
+ guard: 'hasAttemptsRemaining',
725
+ actions: ['resetDetection', 'clearUploadFailure'],
726
+ },
727
+ },
728
+ },
729
+ success: {
730
+ entry: 'stopMediaRecording',
731
+ after: {
732
+ 3000: {
733
+ target: '#selfie.finished',
734
+ },
735
+ },
736
+ },
737
+ },
738
+ },
739
+
740
+ finished: {
741
+ entry: 'stopMediaStream',
742
+ on: {
743
+ RESET: {
744
+ target: 'idle',
745
+ actions: 'resetContext',
746
+ },
747
+ },
748
+ },
749
+
750
+ closed: {
751
+ entry: 'stopMediaStream',
752
+ type: 'final',
753
+ },
754
+ error: {
755
+ entry: 'stopMediaStream',
756
+ on: {
757
+ RESET: {
758
+ target: 'idle',
759
+ actions: 'resetContext',
760
+ },
761
+ },
762
+ },
763
+ },
764
+ });
765
+
766
+ /**
767
+ * The selfie capture state machine.
768
+ *
769
+ * Note: Uses AnyStateMachine type for declaration file portability.
770
+ * Type safety is ensured via the machine configuration.
771
+ */
772
+ export const selfieMachine: AnyStateMachine = _selfieMachine;
773
+
774
+ /**
775
+ * Type representing the selfie machine.
776
+ * For advanced use cases requiring specific machine types.
777
+ */
778
+ export type SelfieMachine = StateMachine<
779
+ SelfieContext,
780
+ SelfieEvent,
781
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
782
+ any,
783
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
784
+ any,
785
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
786
+ any,
787
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
788
+ any,
789
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
790
+ any,
791
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
792
+ any,
793
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
794
+ any,
795
+ SelfieInput,
796
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
797
+ any,
798
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
799
+ any,
800
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
801
+ any,
802
+ // biome-ignore lint/suspicious/noExplicitAny: Required for declaration portability
803
+ any
804
+ >;