smartcomply-web-sdk 1.0.89 → 1.0.91
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/dist/camera/ActionDetector.d.ts +2 -1
- package/dist/camera/ActionDetector.d.ts.map +1 -1
- package/dist/camera/ActionDetector.js +21 -5
- package/dist/camera/ActionDetector.js.map +1 -1
- package/dist/client/Smartcomply.d.ts +14 -2
- package/dist/client/Smartcomply.d.ts.map +1 -1
- package/dist/client/Smartcomply.js +15 -2
- package/dist/client/Smartcomply.js.map +1 -1
- package/dist/esm/camera/ActionDetector.d.ts +2 -1
- package/dist/esm/camera/ActionDetector.d.ts.map +1 -1
- package/dist/esm/camera/ActionDetector.js +21 -5
- package/dist/esm/camera/ActionDetector.js.map +1 -1
- package/dist/esm/client/Smartcomply.d.ts +14 -2
- package/dist/esm/client/Smartcomply.d.ts.map +1 -1
- package/dist/esm/client/Smartcomply.js +15 -2
- package/dist/esm/client/Smartcomply.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts +24 -9
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +151 -25
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/flow/spinner.js +14 -14
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +83 -83
- package/dist/esm/types/liveness.d.ts +15 -0
- package/dist/esm/types/liveness.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +24 -9
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +152 -25
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/flow/spinner.js +14 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +83 -83
- package/dist/smartcomply.browser.js +108 -11
- package/dist/smartcomply.browser.js.map +3 -3
- package/dist/types/liveness.d.ts +15 -0
- package/dist/types/liveness.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/esm/components/CameraView.d.ts +0 -59
- package/dist/esm/components/CameraView.d.ts.map +0 -1
- package/dist/esm/components/CameraView.js +0 -238
- package/dist/esm/components/CameraView.js.map +0 -1
- package/dist/esm/components/DocumentUpload.d.ts +0 -21
- package/dist/esm/components/DocumentUpload.d.ts.map +0 -1
- package/dist/esm/components/DocumentUpload.js +0 -216
- package/dist/esm/components/DocumentUpload.js.map +0 -1
|
@@ -6,6 +6,7 @@ import { requiresBackSide } from "../utils/documentRules";
|
|
|
6
6
|
import { DEFAULT_CHALLENGE_ACTIONS } from "../utils/constants";
|
|
7
7
|
import { icon } from "./icons";
|
|
8
8
|
import { renderSpinner, renderInlineSpinner } from "./spinner";
|
|
9
|
+
import { SDKError } from "../errors/SDKError";
|
|
9
10
|
// A dropped camera/network/upload error is not the user's fault and must not
|
|
10
11
|
// cost them a retry attempt — only a genuine verification outcome should.
|
|
11
12
|
// Classified by message rather than a typed error class since the errors
|
|
@@ -66,6 +67,18 @@ const COUNTRY_LABELS = {
|
|
|
66
67
|
function isUploadFieldType(type) {
|
|
67
68
|
return type === "upload" || type === "image" || type === "file";
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a showErrorStep() message is about an expired/invalid resume or
|
|
72
|
+
* resubmit link (RESUME_TARGET_UNAVAILABLE / NOT_A_RESUME_TOKEN — see
|
|
73
|
+
* SmartComplyFlow._resolveResumeStep), rather than a plain session timeout.
|
|
74
|
+
* Extracted to a standalone function so this specific classification is
|
|
75
|
+
* independently testable, same as resolveIdTypeInfo below — showErrorStep's
|
|
76
|
+
* own regex-based classification block is otherwise tightly coupled to DOM
|
|
77
|
+
* rendering it doesn't make sense to extract wholesale for this fix.
|
|
78
|
+
*/
|
|
79
|
+
export function isResumeLinkErrorMessage(message) {
|
|
80
|
+
return /resume link|resubmit link/i.test(message);
|
|
81
|
+
}
|
|
69
82
|
export function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
70
83
|
const u = typeName.toUpperCase();
|
|
71
84
|
const advanced = u.includes("ADVANCED");
|
|
@@ -654,12 +667,114 @@ export class SmartComplyFlow {
|
|
|
654
667
|
}
|
|
655
668
|
if (this.isDestroyed)
|
|
656
669
|
return;
|
|
657
|
-
this.
|
|
670
|
+
if (this.options.resumeToken) {
|
|
671
|
+
// null means _resolveResumeStep already rendered an error itself
|
|
672
|
+
// (a provably expired/invalid resume link — see that method) and
|
|
673
|
+
// there is no step left to show; calling showStep() in that case
|
|
674
|
+
// would wipe the error message straight back out.
|
|
675
|
+
const resumeStep = await this._resolveResumeStep();
|
|
676
|
+
if (resumeStep !== null)
|
|
677
|
+
this.showStep(resumeStep);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
this.showStep("welcome");
|
|
681
|
+
}
|
|
658
682
|
}
|
|
659
683
|
catch (err) {
|
|
660
684
|
this.showErrorStep(err.message || "Failed to initialize. Please try again.");
|
|
661
685
|
}
|
|
662
686
|
}
|
|
687
|
+
// Resolves a resume_token to the step the customer should land on,
|
|
688
|
+
// pre-selecting the same country/ID type the original attempt used (the
|
|
689
|
+
// same state renderCountrySelect/renderIdTypeSelect's own click handlers
|
|
690
|
+
// set) so document_capture/liveness render correctly instead of assuming
|
|
691
|
+
// state that was never set. Falls back to "welcome" on anything that
|
|
692
|
+
// doesn't resolve cleanly for an UNCERTAIN reason (network error, timeout)
|
|
693
|
+
// — this is a UX improvement, not a requirement: the backend's own
|
|
694
|
+
// resume-match at create/ time still works correctly even if the widget
|
|
695
|
+
// can't figure out which screen to pre-select.
|
|
696
|
+
//
|
|
697
|
+
// Returns null instead when the lookup fails for a CERTAIN, known reason
|
|
698
|
+
// — the resume link itself is provably expired or already used
|
|
699
|
+
// (RESUME_TARGET_UNAVAILABLE) or was never a resume link at all
|
|
700
|
+
// (NOT_A_RESUME_TOKEN). Unlike a transient failure, there is no "try the
|
|
701
|
+
// real request and it'll still work" fallback for these: create/'s own
|
|
702
|
+
// resume-match reads the exact same resume_target_entry binding this
|
|
703
|
+
// lookup just found missing/invalid, so falling through to a generic
|
|
704
|
+
// welcome screen would silently walk the customer into a brand-new,
|
|
705
|
+
// unrelated verification attempt with no explanation — worse than telling
|
|
706
|
+
// them plainly that their link no longer works. The caller shows this
|
|
707
|
+
// error itself (via showErrorStep, called here directly) rather than
|
|
708
|
+
// rendering a step.
|
|
709
|
+
async _resolveResumeStep() {
|
|
710
|
+
try {
|
|
711
|
+
const info = await this.sdk.getResumeInfo();
|
|
712
|
+
if (info.verification_type === "liveness_only" ||
|
|
713
|
+
info.failure_stage === "liveness" ||
|
|
714
|
+
// data_match is always a selfie-vs-identity-photo mismatch (see the
|
|
715
|
+
// backend's own comment in _process_identity_face_match) — fixed by
|
|
716
|
+
// redoing the selfie/video, same as a "liveness" failure. There is
|
|
717
|
+
// no document step in data verification at all, so this can never
|
|
718
|
+
// legitimately need document_capture.
|
|
719
|
+
info.failure_stage === "data_match") {
|
|
720
|
+
// Nothing to re-select — there's no document channel to pick, just
|
|
721
|
+
// liveness itself to redo. selectedCountry still needs a non-blank
|
|
722
|
+
// value (renderLiveness sends it as a required field) the same way
|
|
723
|
+
// the liveness-only welcome-screen path already handles this.
|
|
724
|
+
this.selectedCountry = info.country || this.selectedCountry || "global";
|
|
725
|
+
return "liveness";
|
|
726
|
+
}
|
|
727
|
+
if (info.failure_stage === "document" && info.country && info.identifier_type) {
|
|
728
|
+
const channels = this.sdkConfig?.channels || {};
|
|
729
|
+
const idTypes = channels[info.country] || [];
|
|
730
|
+
const match = idTypes.find((idType) => {
|
|
731
|
+
const code = typeof idType === "string" ? idType : (idType.code || idType.name);
|
|
732
|
+
return code === info.identifier_type;
|
|
733
|
+
});
|
|
734
|
+
if (match) {
|
|
735
|
+
const typeName = typeof match === "string" ? match : match.name;
|
|
736
|
+
const typeCode = typeof match === "string" ? match : (match.code || match.name);
|
|
737
|
+
const channelFields = typeof match !== "string" ? (match.fields || []) : [];
|
|
738
|
+
const isDocFlow = channelFields.some((f) => isUploadFieldType(f.type)) || this.isDocumentFlow();
|
|
739
|
+
const info2 = resolveIdTypeInfo(typeName, isDocFlow);
|
|
740
|
+
const requiresBackFromBackend = typeof match !== "string" ? match.requires_back_side : undefined;
|
|
741
|
+
this.selectedCountry = info.country;
|
|
742
|
+
this.selectedIdType = typeCode;
|
|
743
|
+
this.selectedIdTypeName = typeName;
|
|
744
|
+
this.selectedRequiresBack = requiresBackFromBackend ?? info2.requiresBack;
|
|
745
|
+
if (typeof match !== "string") {
|
|
746
|
+
this.selectedChannelId = match.id;
|
|
747
|
+
this.selectedChannelFields = match.fields || [];
|
|
748
|
+
}
|
|
749
|
+
return "document_capture";
|
|
750
|
+
}
|
|
751
|
+
// Channel from the original submission no longer exists in this
|
|
752
|
+
// config (e.g. it was disabled since) — fall through to welcome
|
|
753
|
+
// rather than render document_capture with no title/back-side info.
|
|
754
|
+
}
|
|
755
|
+
// Nothing above matched cleanly (e.g. a document channel that no
|
|
756
|
+
// longer exists in this config) — the entry is still correctly bound
|
|
757
|
+
// server-side regardless of which screen the widget shows first.
|
|
758
|
+
return "welcome";
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
const code = err instanceof SDKError ? err.errorCode : undefined;
|
|
762
|
+
if (code === "RESUME_TARGET_UNAVAILABLE" || code === "NOT_A_RESUME_TOKEN") {
|
|
763
|
+
// A certain, known failure — this specific link is provably no
|
|
764
|
+
// longer usable (already used, expired, or was never a resume link
|
|
765
|
+
// at all). Tell the customer plainly rather than silently starting
|
|
766
|
+
// a new, unrelated verification on a welcome screen with no
|
|
767
|
+
// explanation — see this method's own docstring above.
|
|
768
|
+
this.showErrorStep("This resume link has expired or is no longer valid.");
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
// Anything else (network error, timeout, an SDKError with a
|
|
772
|
+
// different/absent code) — never block the flow on this lookup.
|
|
773
|
+
// create/'s own resume-match still runs correctly once the customer
|
|
774
|
+
// gets there, so falling through to a generic welcome screen is safe.
|
|
775
|
+
return "welcome";
|
|
776
|
+
}
|
|
777
|
+
}
|
|
663
778
|
// Defers the expiry warning while the user is on a camera-active step
|
|
664
779
|
// (document_capture/liveness) — showing it there would blow away a live
|
|
665
780
|
// video element and in-progress MediaRecorder mid-scan.
|
|
@@ -2264,7 +2379,14 @@ export class SmartComplyFlow {
|
|
|
2264
2379
|
// Classify the error into a user-friendly category
|
|
2265
2380
|
const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
|
|
2266
2381
|
const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
|
|
2267
|
-
|
|
2382
|
+
// Checked before the generic isAuthErr below — a resume/resubmit link
|
|
2383
|
+
// (RESUME_TARGET_UNAVAILABLE / NOT_A_RESUME_TOKEN from the backend, see
|
|
2384
|
+
// _resolveResumeStep) also matches "token...expired"/"link" wording, but
|
|
2385
|
+
// the corrective action is different from a plain session timeout: the
|
|
2386
|
+
// customer can't just "start again" themselves, they need a fresh link
|
|
2387
|
+
// from whoever sent them this one.
|
|
2388
|
+
const isResumeLinkErr = isResumeLinkErrorMessage(message);
|
|
2389
|
+
const isAuthErr = !isResumeLinkErr && /session.*expired|token.*expired|authentication/i.test(message);
|
|
2268
2390
|
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
2269
2391
|
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
2270
2392
|
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
@@ -2274,36 +2396,40 @@ export class SmartComplyFlow {
|
|
|
2274
2396
|
const isBillingErr = /wallet|balance|insufficient.*fund|payment.*required/i.test(message);
|
|
2275
2397
|
const errorIconName = isCameraErr ? "camera"
|
|
2276
2398
|
: isNetworkErr ? "network"
|
|
2277
|
-
:
|
|
2278
|
-
:
|
|
2279
|
-
:
|
|
2280
|
-
:
|
|
2281
|
-
:
|
|
2282
|
-
: "
|
|
2399
|
+
: isResumeLinkErr ? "clock"
|
|
2400
|
+
: isAuthErr ? "clock"
|
|
2401
|
+
: isDocQuality ? "search"
|
|
2402
|
+
: isExpiredDoc ? "calendar"
|
|
2403
|
+
: isFaceMatch ? "face"
|
|
2404
|
+
: isBillingErr ? "warning"
|
|
2405
|
+
: "close";
|
|
2283
2406
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
2284
2407
|
: isNetworkErr ? "Connection Problem"
|
|
2285
|
-
:
|
|
2286
|
-
:
|
|
2287
|
-
:
|
|
2288
|
-
:
|
|
2289
|
-
:
|
|
2290
|
-
: "Verification
|
|
2408
|
+
: isResumeLinkErr ? "Link No Longer Valid"
|
|
2409
|
+
: isAuthErr ? "Session Expired"
|
|
2410
|
+
: isDocQuality ? "Document Photo Unclear"
|
|
2411
|
+
: isExpiredDoc ? "Document Expired"
|
|
2412
|
+
: isFaceMatch ? "Face Match Failed"
|
|
2413
|
+
: isBillingErr ? "Verification Unavailable"
|
|
2414
|
+
: "Verification Unsuccessful";
|
|
2291
2415
|
// Map to user-facing guidance — never show raw internal message
|
|
2292
2416
|
const userMessage = isCameraErr
|
|
2293
2417
|
? "Please allow camera access in your browser settings and try again."
|
|
2294
2418
|
: isNetworkErr
|
|
2295
2419
|
? "Check your internet connection and try again."
|
|
2296
|
-
:
|
|
2297
|
-
? "
|
|
2298
|
-
:
|
|
2299
|
-
?
|
|
2300
|
-
:
|
|
2301
|
-
? "
|
|
2302
|
-
:
|
|
2303
|
-
? "
|
|
2304
|
-
:
|
|
2305
|
-
? "
|
|
2306
|
-
:
|
|
2420
|
+
: isResumeLinkErr
|
|
2421
|
+
? "This link has expired or has already been used. Please contact us to request a new one."
|
|
2422
|
+
: isAuthErr
|
|
2423
|
+
? "Your session has expired. Please start again."
|
|
2424
|
+
: isDocQuality
|
|
2425
|
+
? _sanitizeUserMessage(message) || "Please retake your document photo in good lighting with no glare."
|
|
2426
|
+
: isExpiredDoc
|
|
2427
|
+
? "The document you submitted has expired. Please use a valid, current ID."
|
|
2428
|
+
: isFaceMatch
|
|
2429
|
+
? "Your selfie did not match the photo on your ID. Please ensure you are using your own valid document and face the camera directly."
|
|
2430
|
+
: isBillingErr
|
|
2431
|
+
? "Verification is temporarily unavailable. Please contact support."
|
|
2432
|
+
: _sanitizeUserMessage(message) || "We were unable to complete your verification. Please try again.";
|
|
2307
2433
|
const iconWrap = document.createElement("div");
|
|
2308
2434
|
iconWrap.style.cssText = `
|
|
2309
2435
|
width:72px;height:72px;border-radius:50%;
|