@smileid/usesmileid 12.0.1 → 12.0.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/camera/camera_defaults.d.ts +2 -0
- package/dist/camera/camera_defaults.d.ts.map +1 -1
- package/dist/camera/camera_defaults.js +2 -0
- package/dist/camera/camera_defaults.js.map +1 -1
- package/dist/core/exception/flow_exceptions.d.ts +4 -2
- package/dist/core/exception/flow_exceptions.d.ts.map +1 -1
- package/dist/core/exception/flow_exceptions.js +3 -2
- package/dist/core/exception/flow_exceptions.js.map +1 -1
- package/dist/core/exception/missing_job_type_exception.d.ts +1 -3
- package/dist/core/exception/missing_job_type_exception.d.ts.map +1 -1
- package/dist/core/exception/missing_job_type_exception.js +2 -4
- package/dist/core/exception/missing_job_type_exception.js.map +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
- package/dist/data/api/smile_id_api.d.ts +4 -0
- package/dist/data/api/smile_id_api.d.ts.map +1 -1
- package/dist/data/api/smile_id_api.js +30 -27
- package/dist/data/api/smile_id_api.js.map +1 -1
- package/dist/data/model/job_request.d.ts +28 -12
- package/dist/data/model/job_request.d.ts.map +1 -1
- package/dist/data/model/token_payload.d.ts +78 -0
- package/dist/data/model/token_payload.d.ts.map +1 -0
- package/dist/data/model/token_payload.js +132 -0
- package/dist/data/model/token_payload.js.map +1 -0
- package/dist/data/model/user_details_payload.d.ts +39 -0
- package/dist/data/model/user_details_payload.d.ts.map +1 -0
- package/dist/data/model/user_details_payload.js +53 -0
- package/dist/data/model/user_details_payload.js.map +1 -0
- package/dist/data/util/use_smile_id_token_decoder.d.ts +15 -3
- package/dist/data/util/use_smile_id_token_decoder.d.ts.map +1 -1
- package/dist/data/util/use_smile_id_token_decoder.js +20 -8
- package/dist/data/util/use_smile_id_token_decoder.js.map +1 -1
- package/dist/l10n/strings_en.d.ts +4 -1
- package/dist/l10n/strings_en.d.ts.map +1 -1
- package/dist/l10n/strings_en.js +4 -1
- package/dist/l10n/strings_en.js.map +1 -1
- package/dist/l10n/use_smile_id_localizations.d.ts +10 -0
- package/dist/l10n/use_smile_id_localizations.d.ts.map +1 -1
- package/dist/l10n/use_smile_id_localizations.js +59 -3
- package/dist/l10n/use_smile_id_localizations.js.map +1 -1
- package/dist/metadata/metadata_configuration_mappers.js +1 -1
- package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
- package/dist/presentation/components/use_smile_id_button.d.ts.map +1 -1
- package/dist/presentation/components/use_smile_id_button.js +8 -2
- package/dist/presentation/components/use_smile_id_button.js.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.js +4 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js +23 -1
- package/dist/presentation/flow/dsl/use_smile_id_flow_builder.js.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.d.ts +17 -2
- package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.js +77 -45
- package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js +25 -5
- package/dist/presentation/flow/screen/orchestrated_processing_screen.js.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.js +35 -8
- package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.d.ts +16 -2
- package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.js +19 -27
- package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.d.ts +7 -0
- package/dist/presentation/flow/validation/job_type_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/job_type_validator.js +52 -27
- package/dist/presentation/flow/validation/job_type_validator.js.map +1 -1
- package/dist/presentation/flow/validation/user_details_validator.d.ts +9 -0
- package/dist/presentation/flow/validation/user_details_validator.d.ts.map +1 -0
- package/dist/presentation/flow/validation/user_details_validator.js +40 -0
- package/dist/presentation/flow/validation/user_details_validator.js.map +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/capture_screen_helpers.js +1 -0
- package/dist/presentation/screen/helpers/capture_screen_helpers.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.js +64 -18
- package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/react_native_mock.tsx +22 -0
- package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +14 -8
- package/src/camera/__tests__/camera_permissions.test.ts +72 -0
- package/src/camera/__tests__/camera_system_focus.test.ts +173 -0
- package/src/camera/__tests__/use_smile_id_camera_preview.test.tsx +461 -0
- package/src/camera/__tests__/use_smile_id_frame_stream_hook.test.tsx +229 -0
- package/src/camera/camera_defaults.ts +3 -0
- package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +14 -0
- package/src/core/exception/flow_exceptions.test.ts +10 -0
- package/src/core/exception/flow_exceptions.ts +3 -2
- package/src/core/exception/missing_job_type_exception.ts +2 -4
- package/src/data/api/__tests__/smile_id_api.test.ts +275 -1
- package/src/data/api/interceptors/__tests__/credential_header_redaction.test.ts +144 -0
- package/src/data/api/interceptors/smile_auth_interceptor.ts +1 -1
- package/src/data/api/smile_id_api.ts +36 -49
- package/src/data/dsl/__tests__/use_smile_id_network_builder.test.ts +157 -0
- package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
- package/src/data/model/__tests__/token_payload.test.ts +224 -0
- package/src/data/model/__tests__/user_details_payload.test.ts +100 -0
- package/src/data/model/job_request.ts +27 -12
- package/src/data/model/token_payload.ts +182 -0
- package/src/data/model/user_details_payload.ts +84 -0
- package/src/data/repository/__tests__/network_retries.test.ts +1 -1
- package/src/data/repository/__tests__/repository_dispatch.test.ts +366 -0
- package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
- package/src/data/security/__tests__/signing_crypto.test.ts +203 -0
- package/src/data/util/__tests__/use_smile_id_token_decoder.test.ts +179 -0
- package/src/data/util/use_smile_id_token_decoder.ts +29 -8
- package/src/l10n/__tests__/use_smile_id_localizations.test.ts +122 -5
- package/src/l10n/__tests__/use_smile_id_localizations_provider.test.tsx +112 -0
- package/src/l10n/__tests__/use_smile_id_localizations_surface.test.ts +231 -0
- package/src/l10n/strings_en.ts +5 -1
- package/src/l10n/use_smile_id_localizations.ts +80 -3
- package/src/metadata/__tests__/device_orientation_sampling.test.ts +165 -0
- package/src/metadata/__tests__/geolocation.test.ts +150 -0
- package/src/metadata/__tests__/metadata_configuration_objects.test.ts +1 -1
- package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
- package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
- package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
- package/src/metadata/__tests__/optional_native_modules.test.ts +234 -0
- package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
- package/src/metadata/__tests__/use_smile_id_metadata_factory.test.ts +615 -0
- package/src/metadata/metadata_configuration_mappers.ts +1 -1
- package/src/metadata/use_smile_id_metadata_factory.ts +1 -1
- package/src/presentation/components/__tests__/use_smile_id_content_card.test.tsx +112 -0
- package/src/presentation/components/__tests__/use_smile_id_face_oval_image.test.tsx +190 -0
- package/src/presentation/components/__tests__/use_smile_id_learn_more_content.test.tsx +159 -0
- package/src/presentation/components/__tests__/use_smile_id_processing_center_indicator.test.tsx +88 -0
- package/src/presentation/components/use_smile_id_button.tsx +7 -2
- package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +323 -1
- package/src/presentation/flow/__tests__/use_smile_id_flow.test.tsx +651 -0
- package/src/presentation/flow/dsl/__tests__/use_smile_id_builder_component.test.tsx +120 -0
- package/src/presentation/flow/dsl/builder/screens_builder.ts +4 -6
- package/src/presentation/flow/dsl/use_smile_id_flow_builder.ts +27 -1
- package/src/presentation/flow/flow_navigation_manager.ts +102 -92
- package/src/presentation/flow/graph/__tests__/screen_renderer.test.tsx +102 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_capture_screen.test.tsx +836 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_consent_screen.test.tsx +165 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_instructions_screen.test.tsx +163 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_preview_screen.test.tsx +224 -0
- package/src/presentation/flow/screen/__tests__/orchestrated_processing_screen.test.tsx +338 -0
- package/src/presentation/flow/screen/orchestrated_processing_screen.tsx +42 -5
- package/src/presentation/flow/use_smile_id_flow.tsx +52 -8
- package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +158 -0
- package/src/presentation/flow/validation/__tests__/job_type_validator.test.ts +200 -0
- package/src/presentation/flow/validation/flow_validator.ts +30 -31
- package/src/presentation/flow/validation/job_type_validator.ts +73 -39
- package/src/presentation/flow/validation/user_details_validator.ts +53 -0
- package/src/presentation/screen/__tests__/feedback_copy_table.test.ts +145 -0
- package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +348 -0
- package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +3 -1
- package/src/presentation/screen/__tests__/use_smile_id_permission_screen.test.tsx +68 -0
- package/src/presentation/screen/helpers/capture_screen_helpers.ts +1 -0
- package/src/presentation/screen/use_smile_id_capture_screen.tsx +69 -18
- package/src/presentation/theme/__tests__/use_use_smile_id_fonts.test.tsx +166 -0
|
@@ -227,6 +227,8 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
227
227
|
// branch idempotent without latching the one-shot delivery gate.
|
|
228
228
|
const [captureTimedOut, setCaptureTimedOut] = useState(false);
|
|
229
229
|
const timedOutRef = useRef(false);
|
|
230
|
+
// Set when streaming produced no analyzed frame at all, which the analyzer's own timeout cannot report.
|
|
231
|
+
const [captureStalled, setCaptureStalled] = useState(false);
|
|
230
232
|
// True while a manual/gallery delivery is in flight — disables the controls.
|
|
231
233
|
const [deliveringDocument, setDeliveringDocument] = useState(false);
|
|
232
234
|
// Transient gallery-import error; overrides the live chip, self-clears so the user can re-pick.
|
|
@@ -282,8 +284,28 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
282
284
|
setSessionEpoch((epoch) => epoch + 1);
|
|
283
285
|
// A new session must not compare against the old session's last state.
|
|
284
286
|
previousScanStateRef.current = null;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
// Clear the terminal latches so a retry after a timeout or a stall is a fresh session.
|
|
288
|
+
const wasTerminal = timedOutRef.current;
|
|
289
|
+
timedOutRef.current = false;
|
|
290
|
+
setCaptureTimedOut(false);
|
|
291
|
+
setCaptureStalled(false);
|
|
292
|
+
setScanState({ type: UseSmileIDScanStateType.Initial });
|
|
293
|
+
setHasAnalyzedFrame(false);
|
|
294
|
+
// Frames banked before the abandoned session must not pad this one.
|
|
295
|
+
capturedEventsRef.current = [];
|
|
296
|
+
if (wasTerminal) {
|
|
297
|
+
// A timed-out native session is terminal — stream only after it resets,
|
|
298
|
+
// or the first retry frame re-reports Timeout and re-latches the error.
|
|
299
|
+
void props.analyzer
|
|
300
|
+
.reset()
|
|
301
|
+
.catch((error) => UseSmileIDCrashReporting.captureException(error))
|
|
302
|
+
.finally(() => {
|
|
303
|
+
if (!completedRef.current) setStreaming(true);
|
|
304
|
+
});
|
|
305
|
+
} else {
|
|
306
|
+
setStreaming(true);
|
|
307
|
+
}
|
|
308
|
+
}, [streaming, onStartCapture, props.analyzer]);
|
|
287
309
|
|
|
288
310
|
const analyzer = props.analyzer;
|
|
289
311
|
const onCaptureComplete = props.onCaptureComplete;
|
|
@@ -523,7 +545,8 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
523
545
|
// Terminal states: finished → flush + complete; timeout → failure
|
|
524
546
|
// without flushing (no captured artifacts to wait on).
|
|
525
547
|
if (result.state.type === UseSmileIDScanStateType.Finished) {
|
|
526
|
-
|
|
548
|
+
// `timedOutRef` too: stopping the stream is async, so a frame queued before the timeout can still land here.
|
|
549
|
+
if (completedRef.current || timedOutRef.current) return;
|
|
527
550
|
completedRef.current = true;
|
|
528
551
|
setStreaming(false);
|
|
529
552
|
// flush() waits for every emitted JPG to land on disk before we
|
|
@@ -542,15 +565,9 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
542
565
|
} else if (result.state.type === UseSmileIDScanStateType.Timeout) {
|
|
543
566
|
if (completedRef.current || timedOutRef.current) return;
|
|
544
567
|
setStreaming(false);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// so the tap can still produce a capture.
|
|
549
|
-
timedOutRef.current = true;
|
|
550
|
-
setCaptureTimedOut(true);
|
|
551
|
-
} else {
|
|
552
|
-
completedRef.current = true;
|
|
553
|
-
}
|
|
568
|
+
// The terminal state rides on `timedOutRef`, not the one-shot delivery gate, so the shutter and Start button stay usable.
|
|
569
|
+
timedOutRef.current = true;
|
|
570
|
+
setCaptureTimedOut(true);
|
|
554
571
|
onCaptureTimeout?.(result.state);
|
|
555
572
|
}
|
|
556
573
|
},
|
|
@@ -573,6 +590,9 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
573
590
|
);
|
|
574
591
|
const isInstructionState =
|
|
575
592
|
scanState.type === UseSmileIDScanStateType.Initial && !streaming;
|
|
593
|
+
// A selfie timeout leaves `scanState` on Timeout, so the instruction predicate alone would hide the only way to retry.
|
|
594
|
+
const showStartButton =
|
|
595
|
+
(isInstructionState || captureTimedOut) && !isDocumentCapture;
|
|
576
596
|
const canStart = !streaming && !completedRef.current;
|
|
577
597
|
|
|
578
598
|
const previewReady = hasPermission && device != null;
|
|
@@ -587,6 +607,28 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
587
607
|
[analyzer.frameProcessorPluginName],
|
|
588
608
|
);
|
|
589
609
|
|
|
610
|
+
// Every terminal signal arrives inside a frame result, so a camera that delivers no frame at all would sit on the initial state forever.
|
|
611
|
+
// Latest-ref, not a dep: an inline `onCaptureTimeout` prop is a fresh identity every render, which would restart the wall clock.
|
|
612
|
+
const onCaptureTimeoutRef = useRef(onCaptureTimeout);
|
|
613
|
+
onCaptureTimeoutRef.current = onCaptureTimeout;
|
|
614
|
+
useEffect(() => {
|
|
615
|
+
if (!streaming || hasAnalyzedFrame) return;
|
|
616
|
+
const timer = setTimeout(() => {
|
|
617
|
+
if (completedRef.current || timedOutRef.current) return;
|
|
618
|
+
timedOutRef.current = true;
|
|
619
|
+
setCaptureStalled(true);
|
|
620
|
+
setCaptureTimedOut(true);
|
|
621
|
+
setStreaming(false);
|
|
622
|
+
onCaptureTimeoutRef.current?.({ type: UseSmileIDScanStateType.Timeout });
|
|
623
|
+
}, CameraDefaults.firstFrameTimeoutMs);
|
|
624
|
+
return () => clearTimeout(timer);
|
|
625
|
+
}, [streaming, hasAnalyzedFrame]);
|
|
626
|
+
|
|
627
|
+
// A frame disproves the stall; document capture has no Start button to clear it by hand.
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
if (hasAnalyzedFrame) setCaptureStalled(false);
|
|
630
|
+
}, [hasAnalyzedFrame]);
|
|
631
|
+
|
|
590
632
|
// A missing plugin means no frame ever produces a result — report once
|
|
591
633
|
// on capture start rather than sitting silently on the initial state.
|
|
592
634
|
const pluginErrorReportedRef = useRef(false);
|
|
@@ -732,10 +774,12 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
732
774
|
enableEnhancedLiveness,
|
|
733
775
|
animationDirection: uiState.animationDirection,
|
|
734
776
|
poseMatched: uiState.poseMatched,
|
|
735
|
-
// A transient gallery-import error
|
|
736
|
-
feedbackKey:
|
|
777
|
+
// A stalled camera, then a transient gallery-import error, preempt the live feedback chip.
|
|
778
|
+
feedbackKey: captureStalled
|
|
779
|
+
? "si_capture_camera_unavailable"
|
|
780
|
+
: (galleryErrorKey ?? settledFeedbackKey),
|
|
737
781
|
// Documents auto-start (no Start button); selfie keeps its.
|
|
738
|
-
showCaptureButton:
|
|
782
|
+
showCaptureButton: showStartButton,
|
|
739
783
|
showManualShutter: manualShutterAvailable,
|
|
740
784
|
showGalleryUpload: galleryAvailable,
|
|
741
785
|
isBackSide: documentSide === DocumentSide.Back,
|
|
@@ -754,7 +798,8 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
754
798
|
uiState.poseMatched,
|
|
755
799
|
settledFeedbackKey,
|
|
756
800
|
galleryErrorKey,
|
|
757
|
-
|
|
801
|
+
captureStalled,
|
|
802
|
+
showStartButton,
|
|
758
803
|
isDocumentCapture,
|
|
759
804
|
manualShutterAvailable,
|
|
760
805
|
galleryAvailable,
|
|
@@ -834,8 +879,14 @@ export function UseSmileIDCaptureScreen(props: UseSmileIDCaptureScreenProps) {
|
|
|
834
879
|
<UseSmileIDThemeProvider value={theme}>
|
|
835
880
|
{/* Translucent status bar → full-bleed window so the oval's reference
|
|
836
881
|
frame is the full screen (no-op on iOS). On-device-pending: residual
|
|
837
|
-
inset also depends on the host SafeAreaProvider config.
|
|
838
|
-
|
|
882
|
+
inset also depends on the host SafeAreaProvider config.
|
|
883
|
+
`dark-content` because the capture overlay draws a near-opaque white
|
|
884
|
+
scrim behind the status bar, whatever the host app's own bar style. */}
|
|
885
|
+
<StatusBar
|
|
886
|
+
translucent
|
|
887
|
+
backgroundColor="transparent"
|
|
888
|
+
barStyle="dark-content"
|
|
889
|
+
/>
|
|
839
890
|
<UseSmileIDCaptureContent
|
|
840
891
|
state={contentState}
|
|
841
892
|
captureProgress={uiState.captureProgress}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
it,
|
|
4
|
+
expect,
|
|
5
|
+
vi,
|
|
6
|
+
beforeAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
afterEach,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { create as createRenderer, act } from "react-test-renderer";
|
|
12
|
+
import Module from "node:module";
|
|
13
|
+
import { DM_SANS_FONT_NAMES } from "../theme";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The hook `require()`s the bundled `.ttf` assets — Metro resolves those to
|
|
17
|
+
* numeric asset ids, Node does not. Register a CommonJS handler that mimics
|
|
18
|
+
* Metro's numeric id so the font map can be asserted without a bundler.
|
|
19
|
+
*/
|
|
20
|
+
const nodeExtensions = (
|
|
21
|
+
Module as unknown as {
|
|
22
|
+
_extensions: Record<
|
|
23
|
+
string,
|
|
24
|
+
(mod: { exports: unknown }, filename: string) => void
|
|
25
|
+
>;
|
|
26
|
+
}
|
|
27
|
+
)._extensions;
|
|
28
|
+
|
|
29
|
+
let nextAssetId = 0;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
nodeExtensions[".ttf"] = (mod) => {
|
|
33
|
+
nextAssetId += 1;
|
|
34
|
+
mod.exports = nextAssetId;
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
type FontsModule = typeof import("../use_use_smile_id_fonts");
|
|
39
|
+
type ExpoFontModule = { loadAsync: ReturnType<typeof vi.fn> };
|
|
40
|
+
|
|
41
|
+
/** Fresh copy of the module so its one-shot `loadPromise` latch is reset. */
|
|
42
|
+
async function freshModules(): Promise<{
|
|
43
|
+
fonts: FontsModule;
|
|
44
|
+
expoFont: ExpoFontModule;
|
|
45
|
+
}> {
|
|
46
|
+
vi.resetModules();
|
|
47
|
+
const expoFont = (await import("expo-font")) as unknown as ExpoFontModule;
|
|
48
|
+
// The shared setup's `expo-font` mock survives `resetModules`, so its spy
|
|
49
|
+
// carries calls across tests unless cleared here.
|
|
50
|
+
expoFont.loadAsync.mockClear();
|
|
51
|
+
const fonts = (await import("../use_use_smile_id_fonts")) as FontsModule;
|
|
52
|
+
return { fonts, expoFont };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
warn.mockRestore();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("loadUseSmileIDFonts", () => {
|
|
66
|
+
it("registers every bundled DM Sans weight under its typography face name", async () => {
|
|
67
|
+
const { fonts, expoFont } = await freshModules();
|
|
68
|
+
|
|
69
|
+
await fonts.loadUseSmileIDFonts();
|
|
70
|
+
|
|
71
|
+
expect(expoFont.loadAsync).toHaveBeenCalledTimes(1);
|
|
72
|
+
const [map] = expoFont.loadAsync.mock.calls[0] as [Record<string, number>];
|
|
73
|
+
expect(Object.keys(map).sort()).toEqual(
|
|
74
|
+
Object.values(DM_SANS_FONT_NAMES).sort(),
|
|
75
|
+
);
|
|
76
|
+
for (const face of Object.values(DM_SANS_FONT_NAMES)) {
|
|
77
|
+
expect(typeof map[face]).toBe("number");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shares one in-flight load across concurrent flows", async () => {
|
|
82
|
+
const { fonts, expoFont } = await freshModules();
|
|
83
|
+
|
|
84
|
+
const [first, second] = [
|
|
85
|
+
fonts.loadUseSmileIDFonts(),
|
|
86
|
+
fonts.loadUseSmileIDFonts(),
|
|
87
|
+
];
|
|
88
|
+
await Promise.all([first, second]);
|
|
89
|
+
await fonts.loadUseSmileIDFonts();
|
|
90
|
+
|
|
91
|
+
expect(first).toBe(second);
|
|
92
|
+
expect(expoFont.loadAsync).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("useUseSmileIDFonts", () => {
|
|
97
|
+
function renderHook(useHook: () => boolean) {
|
|
98
|
+
const seen: boolean[] = [];
|
|
99
|
+
function Probe() {
|
|
100
|
+
seen.push(useHook());
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
let renderer!: ReturnType<typeof createRenderer>;
|
|
104
|
+
act(() => {
|
|
105
|
+
renderer = createRenderer(<Probe />);
|
|
106
|
+
});
|
|
107
|
+
return { seen, renderer };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function settle() {
|
|
111
|
+
await act(async () => {
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it("renders before the faces are registered, then flips to loaded", async () => {
|
|
117
|
+
const { fonts } = await freshModules();
|
|
118
|
+
|
|
119
|
+
const { seen } = renderHook(fonts.useUseSmileIDFonts);
|
|
120
|
+
expect(seen[0]).toBe(false);
|
|
121
|
+
|
|
122
|
+
await settle();
|
|
123
|
+
|
|
124
|
+
expect(seen.at(-1)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("never blocks the flow when font registration fails — warns and falls back", async () => {
|
|
128
|
+
const { fonts, expoFont } = await freshModules();
|
|
129
|
+
expoFont.loadAsync.mockRejectedValueOnce(new Error("asset registry down"));
|
|
130
|
+
|
|
131
|
+
const { seen } = renderHook(fonts.useUseSmileIDFonts);
|
|
132
|
+
await settle();
|
|
133
|
+
|
|
134
|
+
expect(seen.at(-1)).toBe(false);
|
|
135
|
+
expect(warn).toHaveBeenCalledWith(
|
|
136
|
+
expect.stringContaining("asset registry down"),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("reports a non-Error rejection without crashing on `.message`", async () => {
|
|
141
|
+
const { fonts, expoFont } = await freshModules();
|
|
142
|
+
expoFont.loadAsync.mockRejectedValueOnce("native module missing");
|
|
143
|
+
|
|
144
|
+
renderHook(fonts.useUseSmileIDFonts);
|
|
145
|
+
await settle();
|
|
146
|
+
|
|
147
|
+
expect(warn).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining("native module missing"),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("stops tracking the load after unmount", async () => {
|
|
153
|
+
const { fonts } = await freshModules();
|
|
154
|
+
|
|
155
|
+
const { renderer, seen } = renderHook(fonts.useUseSmileIDFonts);
|
|
156
|
+
expect(seen).toEqual([false]);
|
|
157
|
+
act(() => {
|
|
158
|
+
renderer.unmount();
|
|
159
|
+
});
|
|
160
|
+
await settle();
|
|
161
|
+
|
|
162
|
+
// The load resolves after the screen is gone; the cleanup latch keeps it
|
|
163
|
+
// from re-rendering the unmounted tree.
|
|
164
|
+
expect(seen).toEqual([false]);
|
|
165
|
+
});
|
|
166
|
+
});
|