smartcomply-web-sdk 1.0.42 β 1.0.45
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/esm/flow/SmartComplyFlow.d.ts +2 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +542 -257
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts +9 -5
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +36 -10
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts +2 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +542 -257
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts +9 -5
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +36 -10
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +450 -224
- package/dist/smartcomply.browser.js.map +3 -3
- package/package.json +1 -1
|
@@ -25,6 +25,13 @@ const COUNTRY_LABELS = {
|
|
|
25
25
|
united_states: "πΊπΈ United States",
|
|
26
26
|
united_kingdom: "π¬π§ United Kingdom",
|
|
27
27
|
};
|
|
28
|
+
// Backend/admin data tags document-capture fields inconsistently β the field
|
|
29
|
+
// renderer (below) already treats "image" and "file" as file inputs alongside
|
|
30
|
+
// "upload", so flow-routing decisions must recognize the same set or a channel
|
|
31
|
+
// tagged "image"/"file" silently falls back to the number-entry screen.
|
|
32
|
+
function isUploadFieldType(type) {
|
|
33
|
+
return type === "upload" || type === "image" || type === "file";
|
|
34
|
+
}
|
|
28
35
|
function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
29
36
|
const u = typeName.toUpperCase();
|
|
30
37
|
const advanced = u.includes("ADVANCED");
|
|
@@ -119,6 +126,55 @@ function resolveIdTypeInfo(typeName, isDocumentFlow) {
|
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
129
|
+
// ββ Widget-side message sanitizer βββββββββββββββββββββββββββββββββββββββββββββ
|
|
130
|
+
// Strips anything technical before showing text to the end user.
|
|
131
|
+
// Keeps messages that are already written plainly in natural language.
|
|
132
|
+
function _sanitizeUserMessage(raw) {
|
|
133
|
+
if (!raw)
|
|
134
|
+
return null;
|
|
135
|
+
const lower = raw.toLowerCase();
|
|
136
|
+
// Block anything that looks internal / technical
|
|
137
|
+
const blocklist = [
|
|
138
|
+
"smartcomply", "face comparison service", "session id", "session_id",
|
|
139
|
+
"provider", "ocr", "textract", "rekognition", "deepface",
|
|
140
|
+
"ivs", "celery", "exception", "traceback", "str(e)",
|
|
141
|
+
"error:", "failed:", "django", "python", "http ",
|
|
142
|
+
"comparison mode", "liveness mode", "callback",
|
|
143
|
+
"face_comparison", "face_match", "confidence_percentage",
|
|
144
|
+
"threshold_percentage", "polling_fallback", "resolved_by",
|
|
145
|
+
];
|
|
146
|
+
for (const blocked of blocklist) {
|
|
147
|
+
if (lower.includes(blocked))
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Block snake_case (internal field names accidentally leaking through)
|
|
151
|
+
if (/[a-z]+_[a-z]+/.test(raw) && !/\s/.test(raw.trim()))
|
|
152
|
+
return null;
|
|
153
|
+
// Block anything starting with a lowercase letter (likely a code/key)
|
|
154
|
+
if (raw.trim()[0] === raw.trim()[0].toLowerCase() && /^[a-z]/.test(raw.trim()))
|
|
155
|
+
return null;
|
|
156
|
+
return raw.trim();
|
|
157
|
+
}
|
|
158
|
+
// ββ Document type code β human label βββββββββββββββββββββββββββββββββββββββββ
|
|
159
|
+
function _formatDocType(code) {
|
|
160
|
+
const map = {
|
|
161
|
+
passport: "Passport",
|
|
162
|
+
national_id: "National ID",
|
|
163
|
+
nin_slip: "NIN Slip",
|
|
164
|
+
nin_with_face: "National Identity Number",
|
|
165
|
+
drivers_license: "Driver's Licence",
|
|
166
|
+
voters_card: "Voter's Card",
|
|
167
|
+
kenya_id: "Kenya National ID",
|
|
168
|
+
ghana_id: "Ghana Card",
|
|
169
|
+
};
|
|
170
|
+
if (!code)
|
|
171
|
+
return "";
|
|
172
|
+
const key = code.toLowerCase().replace(/[\s-]/g, "_");
|
|
173
|
+
if (map[key])
|
|
174
|
+
return map[key];
|
|
175
|
+
// Fallback: title-case the code
|
|
176
|
+
return code.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
177
|
+
}
|
|
122
178
|
// SmartComplyFlow
|
|
123
179
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
124
180
|
// Add CSS animation keyframes for spin animations
|
|
@@ -167,6 +223,7 @@ class SmartComplyFlow {
|
|
|
167
223
|
this._pollStatusTimer = null;
|
|
168
224
|
// Components
|
|
169
225
|
this.docCapture = null;
|
|
226
|
+
this._livenessSubmitResult = null;
|
|
170
227
|
this.options = options;
|
|
171
228
|
this.sdk = new Smartcomply_1.SmartComply({
|
|
172
229
|
apiKey: options.apiKey,
|
|
@@ -242,6 +299,9 @@ class SmartComplyFlow {
|
|
|
242
299
|
// Modal
|
|
243
300
|
this.modal = document.createElement("div");
|
|
244
301
|
this.modal.id = "sc-flow-modal";
|
|
302
|
+
this.modal.setAttribute("role", "dialog");
|
|
303
|
+
this.modal.setAttribute("aria-modal", "true");
|
|
304
|
+
this.modal.setAttribute("aria-label", "Identity verification");
|
|
245
305
|
this.modal.style.cssText = `
|
|
246
306
|
width:100%;max-width:580px;max-height:95vh;
|
|
247
307
|
background:${this.theme.bg};
|
|
@@ -267,6 +327,9 @@ class SmartComplyFlow {
|
|
|
267
327
|
// Content area (scrollable)
|
|
268
328
|
this.contentArea = document.createElement("div");
|
|
269
329
|
this.contentArea.id = "sc-flow-content";
|
|
330
|
+
this.contentArea.setAttribute("role", "main");
|
|
331
|
+
this.contentArea.setAttribute("aria-live", "polite");
|
|
332
|
+
this.contentArea.setAttribute("aria-atomic", "false");
|
|
270
333
|
this.contentArea.style.cssText = `
|
|
271
334
|
flex:1;overflow-y:auto;padding:16px 20px;
|
|
272
335
|
-webkit-overflow-scrolling:touch;
|
|
@@ -368,7 +431,7 @@ class SmartComplyFlow {
|
|
|
368
431
|
this.renderDocumentCapture(wrapper);
|
|
369
432
|
break;
|
|
370
433
|
case "processing":
|
|
371
|
-
this.renderProcessing(wrapper);
|
|
434
|
+
this.renderProcessing(wrapper, this.livenessEntryId || undefined);
|
|
372
435
|
break;
|
|
373
436
|
case "liveness":
|
|
374
437
|
this.renderLiveness(wrapper);
|
|
@@ -576,7 +639,7 @@ class SmartComplyFlow {
|
|
|
576
639
|
const typeCode = typeof idType === "string" ? idType : (idType.code || idType.name);
|
|
577
640
|
// Detect per-channel: does THIS specific channel have upload fields?
|
|
578
641
|
const channelFields = typeof idType !== "string" ? (idType.fields || []) : [];
|
|
579
|
-
const isThisDocFlow = channelFields.some((f) => f.type
|
|
642
|
+
const isThisDocFlow = channelFields.some((f) => isUploadFieldType(f.type)) || globalDocFlow;
|
|
580
643
|
const info = resolveIdTypeInfo(typeName, isThisDocFlow);
|
|
581
644
|
// Prefer the backend-provided back-side flag; fall back to local heuristics only when absent.
|
|
582
645
|
const requiresBackFromBackend = typeof idType !== "string" ? idType.requires_back_side : undefined;
|
|
@@ -614,7 +677,7 @@ class SmartComplyFlow {
|
|
|
614
677
|
}
|
|
615
678
|
// Route: use explicit verification_type when set (more reliable than inspecting all channels)
|
|
616
679
|
// Fallback to per-channel upload field check for mixed / unconfigured setups
|
|
617
|
-
const hasUpload = (this.selectedChannelFields).some((f) => f.type
|
|
680
|
+
const hasUpload = (this.selectedChannelFields).some((f) => isUploadFieldType(f.type));
|
|
618
681
|
const vType = this.sdkConfig?.verification_type;
|
|
619
682
|
const vTypes = vType ? (Array.isArray(vType) ? vType : [vType]) : [];
|
|
620
683
|
const isDocChannel = hasUpload || (vTypes.length > 0 && vTypes.includes("document_verification"));
|
|
@@ -657,6 +720,41 @@ class SmartComplyFlow {
|
|
|
657
720
|
container.appendChild(autoBtn);
|
|
658
721
|
return;
|
|
659
722
|
}
|
|
723
|
+
const _FIELD_RULES = [
|
|
724
|
+
{ match: "BVN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "BVN must be exactly 11 digits" } },
|
|
725
|
+
{ match: "NIN", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
726
|
+
{ match: "NATIONAL IDENTITY", rule: { minLen: 11, maxLen: 11, numeric: true, hint: "NIN must be exactly 11 digits" } },
|
|
727
|
+
{ match: "PASSPORT", rule: { minLen: 6, maxLen: 20, alphanumeric: true, hint: "Passport number must be 6β20 alphanumeric characters" } },
|
|
728
|
+
{ match: "DRIVER", rule: { minLen: 3, maxLen: 30, alphanumeric: true, hint: "Licence number must be 3β30 alphanumeric characters" } },
|
|
729
|
+
{ match: "VOTER", rule: { minLen: 6, maxLen: 30, alphanumeric: true, hint: "Voter ID must be 6β30 alphanumeric characters" } },
|
|
730
|
+
{ match: "GHANA", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "Ghana Card number must be 8β20 alphanumeric characters" } },
|
|
731
|
+
{ match: "SSNIT", rule: { minLen: 7, maxLen: 15, alphanumeric: true, hint: "SSNIT number must be 7β15 alphanumeric characters" } },
|
|
732
|
+
{ match: "TIN", rule: { minLen: 8, maxLen: 20, alphanumeric: true, hint: "TIN must be 8β20 characters" } },
|
|
733
|
+
{ match: "KRA", rule: { minLen: 10, maxLen: 12, alphanumeric: true, hint: "KRA PIN must be 10β12 characters" } },
|
|
734
|
+
];
|
|
735
|
+
function getFieldRule(label) {
|
|
736
|
+
const upper = label.toUpperCase();
|
|
737
|
+
for (const entry of _FIELD_RULES) {
|
|
738
|
+
if (upper.includes(entry.match))
|
|
739
|
+
return entry.rule;
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
function validateFieldValue(label, value) {
|
|
744
|
+
const rule = getFieldRule(label);
|
|
745
|
+
if (!rule)
|
|
746
|
+
return null; // no rule = no validation
|
|
747
|
+
const v = value.trim();
|
|
748
|
+
if (rule.numeric && !/^\d+$/.test(v))
|
|
749
|
+
return `${label} must contain digits only`;
|
|
750
|
+
if (rule.alphanumeric && !/^[A-Za-z0-9]+$/.test(v))
|
|
751
|
+
return `${label} must be alphanumeric (letters and digits only)`;
|
|
752
|
+
if (rule.minLen && v.length < rule.minLen)
|
|
753
|
+
return rule.hint;
|
|
754
|
+
if (rule.maxLen && v.length > rule.maxLen)
|
|
755
|
+
return rule.hint;
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
660
758
|
// Track input elements for validation
|
|
661
759
|
const inputEls = [];
|
|
662
760
|
for (const field of dataFields) {
|
|
@@ -669,7 +767,10 @@ class SmartComplyFlow {
|
|
|
669
767
|
const fieldKey = field.label;
|
|
670
768
|
const fieldWrap = document.createElement("div");
|
|
671
769
|
fieldWrap.style.cssText = "display:flex;flex-direction:column;gap:6px;";
|
|
770
|
+
const labelId = `sc-field-label-${fieldKey.replace(/\s+/g, '-').toLowerCase()}`;
|
|
672
771
|
const label = document.createElement("label");
|
|
772
|
+
label.id = labelId;
|
|
773
|
+
label.htmlFor = `sc-field-input-${fieldKey.replace(/\s+/g, '-').toLowerCase()}`;
|
|
673
774
|
label.style.cssText = `color:${this.theme.textMuted};font-size:10px;font-weight:700;letter-spacing:0.6px;text-transform:uppercase;`;
|
|
674
775
|
label.textContent = field.label;
|
|
675
776
|
fieldWrap.appendChild(label);
|
|
@@ -743,10 +844,18 @@ class SmartComplyFlow {
|
|
|
743
844
|
}
|
|
744
845
|
else {
|
|
745
846
|
// Text input field
|
|
847
|
+
const inputId = `sc-field-input-${fieldKey.replace(/\s+/g, '-').toLowerCase()}`;
|
|
848
|
+
const hintId = `sc-field-hint-${fieldKey.replace(/\s+/g, '-').toLowerCase()}`;
|
|
849
|
+
const rule = getFieldRule(field.label);
|
|
746
850
|
const input = document.createElement("input");
|
|
747
851
|
input.type = "text";
|
|
852
|
+
input.id = inputId;
|
|
853
|
+
input.setAttribute("aria-labelledby", labelId);
|
|
854
|
+
input.setAttribute("aria-describedby", hintId);
|
|
748
855
|
input.placeholder = `Enter ${field.label}`;
|
|
749
|
-
input.maxLength = 50;
|
|
856
|
+
input.maxLength = rule?.maxLen ? rule.maxLen + 2 : 50;
|
|
857
|
+
if (rule?.numeric)
|
|
858
|
+
input.inputMode = "numeric";
|
|
750
859
|
input.style.cssText = `
|
|
751
860
|
width:100%;padding:14px 16px;border-radius:12px;
|
|
752
861
|
font-size:16px;font-weight:500;
|
|
@@ -757,17 +866,50 @@ class SmartComplyFlow {
|
|
|
757
866
|
box-sizing:border-box;
|
|
758
867
|
letter-spacing:0.5px;
|
|
759
868
|
`;
|
|
869
|
+
// Inline hint/error element
|
|
870
|
+
const hintEl = document.createElement("div");
|
|
871
|
+
hintEl.id = hintId;
|
|
872
|
+
hintEl.setAttribute("aria-live", "polite");
|
|
873
|
+
hintEl.style.cssText = `font-size:11px;min-height:14px;margin-top:-2px;`;
|
|
874
|
+
if (rule) {
|
|
875
|
+
hintEl.style.color = this.theme.textMuted;
|
|
876
|
+
hintEl.textContent = rule.hint;
|
|
877
|
+
}
|
|
760
878
|
input.addEventListener("focus", () => {
|
|
761
879
|
input.style.borderColor = this.theme.primary;
|
|
762
880
|
});
|
|
763
881
|
input.addEventListener("blur", () => {
|
|
764
|
-
|
|
882
|
+
const v = input.value.trim();
|
|
883
|
+
if (v) {
|
|
884
|
+
const err = validateFieldValue(field.label, v);
|
|
885
|
+
if (err) {
|
|
886
|
+
input.style.borderColor = this.theme.error;
|
|
887
|
+
hintEl.style.color = this.theme.error;
|
|
888
|
+
hintEl.textContent = err;
|
|
889
|
+
input.setAttribute("aria-invalid", "true");
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
input.style.borderColor = this.theme.success || this.theme.primary;
|
|
893
|
+
hintEl.style.color = this.theme.textMuted;
|
|
894
|
+
hintEl.textContent = rule?.hint || "";
|
|
895
|
+
input.setAttribute("aria-invalid", "false");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
input.style.borderColor = this.theme.border;
|
|
900
|
+
}
|
|
765
901
|
});
|
|
766
902
|
input.addEventListener("input", () => {
|
|
903
|
+
// Reset error styling while typing
|
|
904
|
+
input.style.borderColor = this.theme.primary;
|
|
905
|
+
hintEl.style.color = this.theme.textMuted;
|
|
906
|
+
hintEl.textContent = rule?.hint || "";
|
|
907
|
+
input.removeAttribute("aria-invalid");
|
|
767
908
|
this.collectedFields[fieldKey] = input.value.trim();
|
|
768
909
|
updateSubmitState();
|
|
769
910
|
});
|
|
770
911
|
fieldWrap.appendChild(input);
|
|
912
|
+
fieldWrap.appendChild(hintEl);
|
|
771
913
|
inputEls.push({ field, el: input });
|
|
772
914
|
}
|
|
773
915
|
container.appendChild(fieldWrap);
|
|
@@ -785,9 +927,16 @@ class SmartComplyFlow {
|
|
|
785
927
|
submitBtn.disabled = true;
|
|
786
928
|
submitBtn.style.opacity = "0.5";
|
|
787
929
|
const updateSubmitState = () => {
|
|
930
|
+
// All inputs must have a value AND pass format validation
|
|
788
931
|
const allFilled = inputEls.every(({ el }) => el.value.trim().length > 0);
|
|
789
|
-
|
|
790
|
-
|
|
932
|
+
const allValid = inputEls.every(({ field, el }) => {
|
|
933
|
+
if (el.tagName === "SELECT")
|
|
934
|
+
return el.value.trim().length > 0;
|
|
935
|
+
return !validateFieldValue(field.label, el.value.trim());
|
|
936
|
+
});
|
|
937
|
+
const ok = allFilled && allValid;
|
|
938
|
+
submitBtn.disabled = !ok;
|
|
939
|
+
submitBtn.style.opacity = ok ? "1" : "0.5";
|
|
791
940
|
};
|
|
792
941
|
submitBtn.addEventListener("click", async () => {
|
|
793
942
|
submitBtn.disabled = true;
|
|
@@ -807,7 +956,7 @@ class SmartComplyFlow {
|
|
|
807
956
|
this.showStep("liveness");
|
|
808
957
|
}
|
|
809
958
|
else {
|
|
810
|
-
errorEl.textContent = this.verificationResult.message || "
|
|
959
|
+
errorEl.textContent = this.verificationResult.message || "We could not verify your details. Please check and try again.";
|
|
811
960
|
errorEl.style.display = "block";
|
|
812
961
|
submitBtn.textContent = "Verify & Continue";
|
|
813
962
|
submitBtn.disabled = false;
|
|
@@ -815,14 +964,26 @@ class SmartComplyFlow {
|
|
|
815
964
|
}
|
|
816
965
|
}
|
|
817
966
|
catch (err) {
|
|
818
|
-
//
|
|
967
|
+
// Show a single plain-language error β never raw field:message format
|
|
819
968
|
const fieldErrors = err.data?.errors;
|
|
820
969
|
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
|
|
821
|
-
|
|
822
|
-
|
|
970
|
+
// Pick the first meaningful error message β strip field prefix
|
|
971
|
+
const firstMsg = fieldErrors[0]?.message || fieldErrors[0]?.detail || "";
|
|
972
|
+
errorEl.textContent = _sanitizeUserMessage(firstMsg)
|
|
973
|
+
|| "Please check your details and try again.";
|
|
974
|
+
}
|
|
975
|
+
else if (err.code === "VERIFICATION_FAILED") {
|
|
976
|
+
errorEl.textContent = "We could not verify those details. Please check your ID number and try again.";
|
|
977
|
+
}
|
|
978
|
+
else if (err.code === "CHANNEL_NOT_CONFIGURED") {
|
|
979
|
+
errorEl.textContent = "This ID type is not currently available. Please choose a different option.";
|
|
980
|
+
}
|
|
981
|
+
else if (err.code === "INSUFFICIENT_BALANCE") {
|
|
982
|
+
errorEl.textContent = "Verification is temporarily unavailable. Please contact support.";
|
|
823
983
|
}
|
|
824
984
|
else {
|
|
825
|
-
errorEl.textContent = err.message
|
|
985
|
+
errorEl.textContent = _sanitizeUserMessage(err.message)
|
|
986
|
+
|| "Something went wrong. Please try again.";
|
|
826
987
|
}
|
|
827
988
|
errorEl.style.display = "block";
|
|
828
989
|
submitBtn.textContent = "Verify & Continue";
|
|
@@ -878,130 +1039,140 @@ class SmartComplyFlow {
|
|
|
878
1039
|
this.showErrorStep(err.message || "Document capture failed. Please try again.");
|
|
879
1040
|
});
|
|
880
1041
|
}
|
|
881
|
-
// ββ Processing
|
|
882
|
-
renderProcessing(container) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
position:absolute;inset:0;border-radius:50%;
|
|
891
|
-
border:3px solid ${this.theme.border};
|
|
892
|
-
border-top-color:${this.theme.primary};
|
|
893
|
-
animation:sc-spin 0.9s linear infinite;
|
|
894
|
-
`;
|
|
895
|
-
animationWrap.appendChild(spinner);
|
|
1042
|
+
// ββ Processing (real polling-driven, document flow only) ββββββββββββ
|
|
1043
|
+
renderProcessing(container, entryId) {
|
|
1044
|
+
container.style.cssText += "display:flex;flex-direction:column;align-items:center;gap:16px;padding:16px;";
|
|
1045
|
+
// Spinner
|
|
1046
|
+
const spinnerWrap = document.createElement("div");
|
|
1047
|
+
spinnerWrap.style.cssText = "position:relative;width:56px;height:56px;flex-shrink:0;";
|
|
1048
|
+
const ring = document.createElement("div");
|
|
1049
|
+
ring.id = "sc-proc-ring";
|
|
1050
|
+
ring.style.cssText = `position:absolute;inset:0;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;`;
|
|
896
1051
|
const centerIcon = document.createElement("div");
|
|
897
|
-
centerIcon.style.cssText =
|
|
898
|
-
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
899
|
-
font-size:20px;
|
|
900
|
-
`;
|
|
1052
|
+
centerIcon.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;";
|
|
901
1053
|
centerIcon.textContent = "π";
|
|
902
|
-
|
|
903
|
-
|
|
1054
|
+
spinnerWrap.appendChild(ring);
|
|
1055
|
+
spinnerWrap.appendChild(centerIcon);
|
|
1056
|
+
container.appendChild(spinnerWrap);
|
|
904
1057
|
const title = document.createElement("div");
|
|
905
1058
|
title.style.cssText = `color:${this.theme.text};font-size:15px;font-weight:700;text-align:center;`;
|
|
906
|
-
title.textContent = "
|
|
1059
|
+
title.textContent = "Verifying your document";
|
|
907
1060
|
container.appendChild(title);
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
{ id:
|
|
919
|
-
{ id:
|
|
920
|
-
{ id:
|
|
921
|
-
{ id:
|
|
1061
|
+
// Live status line β updated by polling
|
|
1062
|
+
const statusLine = document.createElement("div");
|
|
1063
|
+
statusLine.id = "sc-proc-status";
|
|
1064
|
+
statusLine.setAttribute("aria-live", "polite");
|
|
1065
|
+
statusLine.setAttribute("aria-atomic", "true");
|
|
1066
|
+
statusLine.style.cssText = `color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:260px;line-height:1.5;`;
|
|
1067
|
+
statusLine.textContent = "Scanning documentβ¦";
|
|
1068
|
+
container.appendChild(statusLine);
|
|
1069
|
+
// Step checklist β reflects real pipeline stages
|
|
1070
|
+
const stages = [
|
|
1071
|
+
{ id: "ocr", label: "Reading document", icon: "π" },
|
|
1072
|
+
{ id: "expiry", label: "Checking expiry", icon: "π
" },
|
|
1073
|
+
{ id: "face", label: "Face match", icon: "π€³" },
|
|
1074
|
+
{ id: "result", label: "Finalising result", icon: "β" },
|
|
922
1075
|
];
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1076
|
+
const grid = document.createElement("div");
|
|
1077
|
+
grid.style.cssText = "width:100%;display:grid;grid-template-columns:1fr 1fr;gap:8px;";
|
|
1078
|
+
stages.forEach((s) => {
|
|
1079
|
+
const el = document.createElement("div");
|
|
1080
|
+
el.id = `sc-stage-${s.id}`;
|
|
1081
|
+
el.style.cssText = `display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:10px;background:${this.theme.cardBg};border:1px solid ${this.theme.border};transition:all 0.3s ease;`;
|
|
1082
|
+
el.innerHTML = `
|
|
1083
|
+
<div style="width:26px;height:26px;border-radius:7px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;background:${this.theme.shimmer};">${s.icon}</div>
|
|
1084
|
+
<div style="flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;">${s.label}</div>
|
|
1085
|
+
<div id="sc-stage-status-${s.id}" style="width:14px;height:14px;border-radius:50%;border:2px solid ${this.theme.border};flex-shrink:0;"></div>
|
|
929
1086
|
`;
|
|
930
|
-
|
|
931
|
-
const iconEl = document.createElement("div");
|
|
932
|
-
iconEl.style.cssText = `
|
|
933
|
-
width:26px;height:26px;border-radius:7px;flex-shrink:0;
|
|
934
|
-
display:flex;align-items:center;justify-content:center;
|
|
935
|
-
font-size:12px;background:${this.theme.shimmer};
|
|
936
|
-
`;
|
|
937
|
-
iconEl.textContent = step.icon;
|
|
938
|
-
iconEl.id = `step-icon-${step.id}`;
|
|
939
|
-
const textEl = document.createElement("div");
|
|
940
|
-
textEl.style.cssText = `flex:1;color:${this.theme.textSecondary};font-size:11px;font-weight:500;`;
|
|
941
|
-
textEl.textContent = step.text;
|
|
942
|
-
textEl.id = `step-text-${step.id}`;
|
|
943
|
-
const statusEl = document.createElement("div");
|
|
944
|
-
statusEl.style.cssText = `width:16px;height:16px;border-radius:50%;border:2px solid ${this.theme.border};position:relative;flex-shrink:0;`;
|
|
945
|
-
statusEl.id = `step-status-${step.id}`;
|
|
946
|
-
stepEl.appendChild(iconEl);
|
|
947
|
-
stepEl.appendChild(textEl);
|
|
948
|
-
stepEl.appendChild(statusEl);
|
|
949
|
-
stepsContainer.appendChild(stepEl);
|
|
1087
|
+
grid.appendChild(el);
|
|
950
1088
|
});
|
|
951
|
-
container.appendChild(
|
|
952
|
-
|
|
1089
|
+
container.appendChild(grid);
|
|
1090
|
+
// Start polling immediately β update stages as result comes in
|
|
1091
|
+
if (entryId) {
|
|
1092
|
+
this._pollProcessingStages(entryId, statusLine, grid, stages);
|
|
1093
|
+
}
|
|
953
1094
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1095
|
+
_pollProcessingStages(entryId, statusLine, _grid, stages) {
|
|
1096
|
+
// Animate stages sequentially on a timer β OCR takes ~5β15s,
|
|
1097
|
+
// face match another ~5β15s. We can't observe sub-steps from the
|
|
1098
|
+
// public sdk-result endpoint, so we advance stages on elapsed time
|
|
1099
|
+
// AND on the final status transition.
|
|
1100
|
+
const stageTimings = [0, 4000, 10000, 18000]; // ms after poll start
|
|
1101
|
+
const startTime = Date.now();
|
|
1102
|
+
const setStageActive = (stageId) => {
|
|
1103
|
+
const el = document.getElementById(`sc-stage-${stageId}`);
|
|
1104
|
+
const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
|
|
1105
|
+
if (!el || !statusEl)
|
|
1106
|
+
return;
|
|
1107
|
+
el.style.borderColor = this.theme.primary;
|
|
1108
|
+
el.style.background = `${this.theme.primary}10`;
|
|
1109
|
+
statusEl.innerHTML = `<div style="position:absolute;inset:1px;border-radius:50%;border:2px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.8s linear infinite;"></div>`;
|
|
1110
|
+
statusEl.style.position = "relative";
|
|
1111
|
+
};
|
|
1112
|
+
const setStageComplete = (stageId) => {
|
|
1113
|
+
const el = document.getElementById(`sc-stage-${stageId}`);
|
|
1114
|
+
const statusEl = document.getElementById(`sc-stage-status-${stageId}`);
|
|
1115
|
+
if (!el || !statusEl)
|
|
1116
|
+
return;
|
|
1117
|
+
el.style.borderColor = this.theme.success || "#22c55e";
|
|
1118
|
+
el.style.background = `${this.theme.success || "#22c55e"}10`;
|
|
1119
|
+
statusEl.innerHTML = `<div style="position:absolute;inset:2px;border-radius:50%;background:${this.theme.success || "#22c55e"};display:flex;align-items:center;justify-content:center;color:#fff;font-size:8px;font-weight:bold;">β</div>`;
|
|
1120
|
+
statusEl.style.position = "relative";
|
|
1121
|
+
};
|
|
1122
|
+
// Advance stage indicators on timer
|
|
1123
|
+
stages.forEach((s, i) => {
|
|
982
1124
|
setTimeout(() => {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1125
|
+
if (this.isDestroyed)
|
|
1126
|
+
return;
|
|
1127
|
+
setStageActive(s.id);
|
|
1128
|
+
if (i > 0)
|
|
1129
|
+
setStageComplete(stages[i - 1].id);
|
|
1130
|
+
statusLine.textContent = s.id === "ocr"
|
|
1131
|
+
? "Scanning documentβ¦"
|
|
1132
|
+
: s.id === "expiry"
|
|
1133
|
+
? "Checking expiry dateβ¦"
|
|
1134
|
+
: s.id === "face"
|
|
1135
|
+
? "Running face matchβ¦"
|
|
1136
|
+
: "Finalising resultβ¦";
|
|
1137
|
+
}, stageTimings[i]);
|
|
1138
|
+
});
|
|
1139
|
+
// Poll for real result
|
|
1140
|
+
const MAX_POLLS = 30; // 30 Γ 3s = 90s max
|
|
1141
|
+
let polls = 0;
|
|
1142
|
+
const poll = async () => {
|
|
1143
|
+
if (this.isDestroyed)
|
|
1144
|
+
return;
|
|
1145
|
+
polls++;
|
|
1146
|
+
try {
|
|
1147
|
+
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1148
|
+
if (result.status === "passed" || result.status === "failed") {
|
|
1149
|
+
// Mark all remaining stages complete
|
|
1150
|
+
stages.forEach((s) => setStageComplete(s.id));
|
|
1151
|
+
statusLine.textContent = result.status === "passed"
|
|
1152
|
+
? "Verification complete β"
|
|
1153
|
+
: "Verification finished";
|
|
1154
|
+
// Short delay so user sees the completed states, then show result
|
|
1155
|
+
setTimeout(() => {
|
|
1156
|
+
if (!this.isDestroyed) {
|
|
1157
|
+
this._livenessSubmitResult = result;
|
|
1158
|
+
this.showResult(result);
|
|
1159
|
+
}
|
|
1160
|
+
}, 800);
|
|
1161
|
+
return;
|
|
1000
1162
|
}
|
|
1001
|
-
}
|
|
1163
|
+
}
|
|
1164
|
+
catch { /* network hiccup β keep polling */ }
|
|
1165
|
+
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1166
|
+
this._pollStatusTimer = setTimeout(poll, 3000);
|
|
1167
|
+
}
|
|
1168
|
+
else if (!this.isDestroyed) {
|
|
1169
|
+
// Timeout β transition to result screen with current status
|
|
1170
|
+
stages.forEach((s) => setStageComplete(s.id));
|
|
1171
|
+
statusLine.textContent = "Still processing β check back soon";
|
|
1172
|
+
}
|
|
1002
1173
|
};
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1174
|
+
// First poll after 5s β give Celery task time to start
|
|
1175
|
+
this._pollStatusTimer = setTimeout(poll, 5000);
|
|
1005
1176
|
}
|
|
1006
1177
|
/**
|
|
1007
1178
|
* Process document capture result with front/back support
|
|
@@ -1043,8 +1214,15 @@ class SmartComplyFlow {
|
|
|
1043
1214
|
<div style="color:${this.theme.text};font-size:14px;font-weight:600;">Scanning document...</div>
|
|
1044
1215
|
<div style="color:${this.theme.textSecondary};font-size:12px;text-align:center;max-width:200px;line-height:1.5;">Running OCR and verifying your identity</div>
|
|
1045
1216
|
</div>`;
|
|
1046
|
-
// Create liveness entry with document
|
|
1047
|
-
|
|
1217
|
+
// Create liveness entry with document.
|
|
1218
|
+
// Use the first collected field value as identifier when available
|
|
1219
|
+
// (document flow has no data-verification step, so collectedFields is
|
|
1220
|
+
// empty β fall back to the selected ID type code so the webhook
|
|
1221
|
+
// subject.identifier is at least human-meaningful, not a raw timestamp).
|
|
1222
|
+
const identifier = Object.values(this.collectedFields)[0]
|
|
1223
|
+
|| this.idNumber
|
|
1224
|
+
|| this.selectedIdType
|
|
1225
|
+
|| `DOC-${Date.now()}`;
|
|
1048
1226
|
const COUNTRY_CODES = {
|
|
1049
1227
|
nigeria: "NG", ghana: "GH", kenya: "KE",
|
|
1050
1228
|
south_africa: "ZA", united_states: "US", united_kingdom: "GB", global: "US",
|
|
@@ -1123,52 +1301,62 @@ class SmartComplyFlow {
|
|
|
1123
1301
|
await attemptProcessing();
|
|
1124
1302
|
}
|
|
1125
1303
|
handleDocumentProcessingError(error, captureContainer) {
|
|
1126
|
-
|
|
1304
|
+
// Map technical errors to user-friendly messages β never pass raw error strings
|
|
1305
|
+
let userMessage = "We could not verify your document. Please try again.";
|
|
1127
1306
|
let canRetry = true;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1307
|
+
const raw = (error?.message || "").toLowerCase();
|
|
1308
|
+
if (raw.includes("expired")) {
|
|
1309
|
+
userMessage = "The document you submitted has expired. Please use a valid, current ID.";
|
|
1310
|
+
canRetry = false;
|
|
1311
|
+
}
|
|
1312
|
+
else if (raw.includes("quality too low") || raw.includes("blurry") || raw.includes("blur")) {
|
|
1313
|
+
userMessage = "The document photo isn't clear enough. Please retake it in good lighting with no glare.";
|
|
1314
|
+
}
|
|
1315
|
+
else if (raw.includes("not detected") || raw.includes("document_not_detected")) {
|
|
1316
|
+
userMessage = "We could not detect a document in the photo. Please ensure the full ID is visible and try again.";
|
|
1317
|
+
}
|
|
1318
|
+
else if (raw.includes("network") || raw.includes("fetch") || raw.includes("connection")) {
|
|
1319
|
+
userMessage = "A connection error occurred. Please check your internet and try again.";
|
|
1320
|
+
}
|
|
1321
|
+
else if (raw.includes("too large") || raw.includes("413")) {
|
|
1322
|
+
userMessage = "The document image is too large. Please retake the photo.";
|
|
1323
|
+
}
|
|
1324
|
+
else if (raw.includes("format") || raw.includes("415")) {
|
|
1325
|
+
userMessage = "Unsupported image format. Please use JPEG or PNG.";
|
|
1326
|
+
}
|
|
1327
|
+
else if (raw.includes("type mismatch") || raw.includes("document_type_mismatch")) {
|
|
1328
|
+
userMessage = "The document does not match the selected ID type. Please choose the correct ID type.";
|
|
1145
1329
|
}
|
|
1146
1330
|
if (captureContainer) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1331
|
+
// Build with DOM β never interpolate user-derived content into innerHTML
|
|
1332
|
+
const wrap = document.createElement("div");
|
|
1333
|
+
wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 20px;text-align:center;";
|
|
1334
|
+
wrap.setAttribute("role", "alert");
|
|
1335
|
+
const icon = document.createElement("div");
|
|
1336
|
+
icon.style.cssText = `width:56px;height:56px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:26px;`;
|
|
1337
|
+
icon.textContent = "π";
|
|
1338
|
+
wrap.appendChild(icon);
|
|
1339
|
+
const titleEl = document.createElement("div");
|
|
1340
|
+
titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:700;`;
|
|
1341
|
+
titleEl.textContent = "Document Could Not Be Verified";
|
|
1342
|
+
wrap.appendChild(titleEl);
|
|
1343
|
+
const msgEl = document.createElement("div");
|
|
1344
|
+
msgEl.style.cssText = `color:${this.theme.textSecondary};font-size:13px;max-width:260px;line-height:1.6;`;
|
|
1345
|
+
msgEl.textContent = userMessage;
|
|
1346
|
+
wrap.appendChild(msgEl);
|
|
1161
1347
|
if (canRetry) {
|
|
1162
|
-
const retryBtn =
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1348
|
+
const retryBtn = document.createElement("button");
|
|
1349
|
+
retryBtn.textContent = "Try Again";
|
|
1350
|
+
retryBtn.setAttribute("aria-label", "Retake document photo");
|
|
1351
|
+
retryBtn.style.cssText = `margin-top:8px;padding:12px 24px;border-radius:8px;background:${this.theme.primary};color:#fff;border:none;font-size:14px;font-weight:600;cursor:pointer;`;
|
|
1352
|
+
retryBtn.addEventListener("click", () => this.showStep("document_capture"));
|
|
1353
|
+
wrap.appendChild(retryBtn);
|
|
1168
1354
|
}
|
|
1355
|
+
captureContainer.innerHTML = "";
|
|
1356
|
+
captureContainer.appendChild(wrap);
|
|
1169
1357
|
}
|
|
1170
1358
|
else {
|
|
1171
|
-
this.showErrorStep(
|
|
1359
|
+
this.showErrorStep(userMessage);
|
|
1172
1360
|
}
|
|
1173
1361
|
}
|
|
1174
1362
|
// ββ Liveness ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1184,7 +1372,10 @@ class SmartComplyFlow {
|
|
|
1184
1372
|
const livenessContainer = document.createElement("div");
|
|
1185
1373
|
livenessContainer.style.cssText = "margin:8px 0;width:100%;";
|
|
1186
1374
|
container.appendChild(livenessContainer);
|
|
1187
|
-
const identifier = this.idNumber
|
|
1375
|
+
const identifier = this.idNumber
|
|
1376
|
+
|| Object.values(this.collectedFields)[0]
|
|
1377
|
+
|| this.selectedIdType
|
|
1378
|
+
|| `DOC-${Date.now()}`;
|
|
1188
1379
|
const COUNTRY_CODES = {
|
|
1189
1380
|
nigeria: "NG",
|
|
1190
1381
|
ghana: "GH",
|
|
@@ -1205,9 +1396,25 @@ class SmartComplyFlow {
|
|
|
1205
1396
|
document: this.documentBlob || undefined,
|
|
1206
1397
|
document_back: this.documentBackBlob || undefined,
|
|
1207
1398
|
identity_check: this.identityCheckId || undefined,
|
|
1399
|
+
// Tell the module which flow we're in so it can skip video for data verification
|
|
1400
|
+
verification_type: this.identityCheckId
|
|
1401
|
+
? "data_verification"
|
|
1402
|
+
: this.documentBlob
|
|
1403
|
+
? "document_verification"
|
|
1404
|
+
: "liveness_only",
|
|
1208
1405
|
}, ["BLINK", "TURN_HEAD", "OPEN_MOUTH"], this.livenessEntryId || undefined)
|
|
1209
1406
|
.then((result) => {
|
|
1210
|
-
|
|
1407
|
+
// For document/data verification the backend processes async.
|
|
1408
|
+
// Route through the processing step so the user sees real pipeline
|
|
1409
|
+
// progress instead of jumping straight to result.
|
|
1410
|
+
const isAsync = !!(this.documentBlob || this.identityCheckId);
|
|
1411
|
+
if (isAsync && result.id) {
|
|
1412
|
+
this.livenessEntryId = result.id;
|
|
1413
|
+
this.showStep("processing");
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
this.showResult(result);
|
|
1417
|
+
}
|
|
1211
1418
|
})
|
|
1212
1419
|
.catch((err) => {
|
|
1213
1420
|
this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
|
|
@@ -1221,12 +1428,14 @@ class SmartComplyFlow {
|
|
|
1221
1428
|
return;
|
|
1222
1429
|
this.contentArea.innerHTML = "";
|
|
1223
1430
|
const wrapper = document.createElement("div");
|
|
1431
|
+
wrapper.setAttribute("role", "region");
|
|
1432
|
+
wrapper.setAttribute("aria-label", "Verification result");
|
|
1224
1433
|
wrapper.style.cssText = `
|
|
1225
1434
|
display:flex;flex-direction:column;align-items:center;
|
|
1226
1435
|
text-align:center;gap:16px;padding:20px 0;
|
|
1227
1436
|
opacity:0;transform:scale(0.95);transition:all 0.4s ease;
|
|
1228
1437
|
`;
|
|
1229
|
-
// Icon β
|
|
1438
|
+
// ββ Icon β spinner until status resolves ββββββββββββββββββββββββ
|
|
1230
1439
|
const iconWrap = document.createElement("div");
|
|
1231
1440
|
iconWrap.style.cssText = `
|
|
1232
1441
|
width:80px;height:80px;border-radius:50%;
|
|
@@ -1234,59 +1443,68 @@ class SmartComplyFlow {
|
|
|
1234
1443
|
font-size:36px;
|
|
1235
1444
|
background:${this.theme.cardBg};
|
|
1236
1445
|
border:3px solid ${this.theme.border};
|
|
1446
|
+
transition:all 0.4s ease;
|
|
1237
1447
|
`;
|
|
1238
1448
|
iconWrap.innerHTML = `<div style="width:32px;height:32px;border-radius:50%;border:3px solid ${this.theme.border};border-top-color:${this.theme.primary};animation:sc-spin 0.9s linear infinite;"></div>`;
|
|
1239
1449
|
wrapper.appendChild(iconWrap);
|
|
1450
|
+
// ββ Title and description β updated by polling βββββββββββββββββββ
|
|
1240
1451
|
const title = document.createElement("h2");
|
|
1452
|
+
title.setAttribute("aria-live", "polite");
|
|
1241
1453
|
title.style.cssText = `color:${this.theme.text};font-size:20px;font-weight:700;margin:0;`;
|
|
1242
|
-
title.textContent = "
|
|
1454
|
+
title.textContent = "Checking your identityβ¦";
|
|
1243
1455
|
wrapper.appendChild(title);
|
|
1244
1456
|
const desc = document.createElement("p");
|
|
1457
|
+
desc.setAttribute("aria-live", "polite");
|
|
1245
1458
|
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1246
|
-
desc.textContent = "
|
|
1459
|
+
desc.textContent = "This usually takes a few seconds. Please wait.";
|
|
1247
1460
|
wrapper.appendChild(desc);
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1250
|
-
|
|
1461
|
+
// ββ Result card β initially shows status only ββββββββββββββββββββ
|
|
1462
|
+
// Populated with name/doc type when verification completes.
|
|
1463
|
+
const resultCard = document.createElement("div");
|
|
1464
|
+
resultCard.style.cssText = `
|
|
1251
1465
|
width:100%;padding:16px;border-radius:12px;
|
|
1252
1466
|
background:${this.theme.cardBg};border:1px solid ${this.theme.border};
|
|
1253
|
-
text-align:left;
|
|
1467
|
+
text-align:left;display:flex;flex-direction:column;gap:10px;
|
|
1468
|
+
transition:border-color 0.3s ease;
|
|
1254
1469
|
`;
|
|
1255
|
-
// Status
|
|
1470
|
+
// Status row β always shown
|
|
1256
1471
|
const statusBadge = document.createElement("span");
|
|
1472
|
+
statusBadge.setAttribute("role", "status");
|
|
1473
|
+
statusBadge.setAttribute("aria-live", "polite");
|
|
1474
|
+
statusBadge.setAttribute("aria-atomic", "true");
|
|
1257
1475
|
statusBadge.style.cssText = `font-size:12px;font-weight:600;color:${this.theme.warning};`;
|
|
1258
1476
|
statusBadge.textContent = "Processingβ¦";
|
|
1259
1477
|
const statusRow = document.createElement("div");
|
|
1260
|
-
statusRow.style.cssText = "display:flex;justify-content:space-between;
|
|
1261
|
-
statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;">Status</span>`;
|
|
1478
|
+
statusRow.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
|
|
1479
|
+
statusRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Status</span>`;
|
|
1262
1480
|
statusRow.appendChild(statusBadge);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
`;
|
|
1278
|
-
details.appendChild(submittedRow);
|
|
1279
|
-
wrapper.appendChild(details);
|
|
1280
|
-
// Failure reason β hidden until status = failed
|
|
1481
|
+
resultCard.appendChild(statusRow);
|
|
1482
|
+
// Verified details β shown only when passed.
|
|
1483
|
+
// nameRow: shows full name from IVS result or OCR
|
|
1484
|
+
// docTypeRow: shows document type verified
|
|
1485
|
+
const nameRow = document.createElement("div");
|
|
1486
|
+
nameRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
|
|
1487
|
+
nameRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">Name</span><span id="sc-result-name" style="color:${this.theme.text};font-size:12px;font-weight:600;text-align:right;max-width:60%;"></span>`;
|
|
1488
|
+
resultCard.appendChild(nameRow);
|
|
1489
|
+
const docTypeRow = document.createElement("div");
|
|
1490
|
+
docTypeRow.style.cssText = "display:none;justify-content:space-between;align-items:center;";
|
|
1491
|
+
docTypeRow.innerHTML = `<span style="color:${this.theme.textMuted};font-size:12px;font-weight:500;">ID Type</span><span id="sc-result-doctype" style="color:${this.theme.text};font-size:12px;font-weight:600;"></span>`;
|
|
1492
|
+
resultCard.appendChild(docTypeRow);
|
|
1493
|
+
wrapper.appendChild(resultCard);
|
|
1494
|
+
// ββ Failure reason β shown only when failed ββββββββββββββββββββββ
|
|
1281
1495
|
const failureMsg = document.createElement("div");
|
|
1496
|
+
failureMsg.setAttribute("role", "alert");
|
|
1282
1497
|
failureMsg.style.cssText = `
|
|
1283
1498
|
display:none;width:100%;padding:12px 14px;border-radius:10px;
|
|
1284
1499
|
background:${this.theme.errorBg};border:1px solid ${this.theme.error};
|
|
1285
|
-
color:${this.theme.error};font-size:
|
|
1500
|
+
color:${this.theme.error};font-size:13px;text-align:left;line-height:1.6;
|
|
1286
1501
|
`;
|
|
1287
1502
|
wrapper.appendChild(failureMsg);
|
|
1288
|
-
//
|
|
1503
|
+
// ββ Action buttons βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1504
|
+
const btnRow = document.createElement("div");
|
|
1505
|
+
btnRow.style.cssText = "display:flex;flex-direction:column;gap:8px;width:100%;";
|
|
1289
1506
|
const doneBtn = this.createPrimaryButton("Done");
|
|
1507
|
+
doneBtn.setAttribute("aria-label", "Close verification and continue");
|
|
1290
1508
|
let finalStatus = submitResult.status;
|
|
1291
1509
|
doneBtn.addEventListener("click", () => {
|
|
1292
1510
|
if (this._pollStatusTimer) {
|
|
@@ -1302,74 +1520,107 @@ class SmartComplyFlow {
|
|
|
1302
1520
|
});
|
|
1303
1521
|
this.close();
|
|
1304
1522
|
});
|
|
1305
|
-
|
|
1523
|
+
btnRow.appendChild(doneBtn);
|
|
1524
|
+
// "Try again" button β shown only on failure, hidden initially
|
|
1525
|
+
const retryBtn = this.createSecondaryButton("Try Again");
|
|
1526
|
+
retryBtn.setAttribute("aria-label", "Start verification again");
|
|
1527
|
+
retryBtn.style.display = "none";
|
|
1528
|
+
retryBtn.addEventListener("click", () => {
|
|
1529
|
+
if (this._pollStatusTimer) {
|
|
1530
|
+
clearTimeout(this._pollStatusTimer);
|
|
1531
|
+
this._pollStatusTimer = null;
|
|
1532
|
+
}
|
|
1533
|
+
this.initialize();
|
|
1534
|
+
});
|
|
1535
|
+
btnRow.appendChild(retryBtn);
|
|
1536
|
+
wrapper.appendChild(btnRow);
|
|
1306
1537
|
this.contentArea.appendChild(wrapper);
|
|
1307
1538
|
requestAnimationFrame(() => {
|
|
1308
1539
|
wrapper.style.opacity = "1";
|
|
1309
1540
|
wrapper.style.transform = "scale(1)";
|
|
1310
1541
|
});
|
|
1311
|
-
// Start polling
|
|
1312
|
-
this._startStatusPolling(submitResult.id, iconWrap, statusBadge, title, desc, failureMsg, (resolved) => { finalStatus = resolved; });
|
|
1542
|
+
// ββ Start polling ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1543
|
+
this._startStatusPolling(submitResult.id, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, (resolved) => { finalStatus = resolved; });
|
|
1313
1544
|
}
|
|
1314
|
-
_startStatusPolling(entryId, iconWrap, statusBadge, title, desc, failureMsg, onResolved) {
|
|
1315
|
-
const MAX_POLLS = 25; // 25 Γ
|
|
1545
|
+
_startStatusPolling(entryId, iconWrap, statusBadge, title, desc, resultCard, nameRow, docTypeRow, failureMsg, retryBtn, onResolved) {
|
|
1546
|
+
const MAX_POLLS = 25; // 25 Γ 3s = 75s + 4s initial β 79s max
|
|
1316
1547
|
let polls = 0;
|
|
1317
|
-
// Hard
|
|
1548
|
+
// Hard 30s UI timer β stop spinner and tell user to wait even if backend is slower
|
|
1318
1549
|
let uiTimeoutId = setTimeout(() => {
|
|
1319
1550
|
if (!this.isDestroyed)
|
|
1320
1551
|
applyTimeout();
|
|
1321
1552
|
}, 30000);
|
|
1322
|
-
const clearUiTimeout = () => {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1553
|
+
const clearUiTimeout = () => {
|
|
1554
|
+
if (uiTimeoutId) {
|
|
1555
|
+
clearTimeout(uiTimeoutId);
|
|
1556
|
+
uiTimeoutId = null;
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1326
1559
|
const applyTimeout = () => {
|
|
1327
|
-
// Stop polling for real β the message below tells the user it's safe
|
|
1328
|
-
// to close and that the result will arrive via webhook/dashboard.
|
|
1329
|
-
// Silently continuing to poll in the background (up to another ~49s)
|
|
1330
|
-
// used to mean the UI could suddenly flip to Verified/Failed after
|
|
1331
|
-
// already telling the user to walk away, with no warning either way.
|
|
1332
1560
|
if (this._pollStatusTimer) {
|
|
1333
1561
|
clearTimeout(this._pollStatusTimer);
|
|
1334
1562
|
this._pollStatusTimer = null;
|
|
1335
1563
|
}
|
|
1336
|
-
iconWrap.innerHTML = "
|
|
1337
|
-
iconWrap.style.background = this.theme.cardBg;
|
|
1564
|
+
iconWrap.innerHTML = "β³";
|
|
1338
1565
|
iconWrap.style.border = `3px solid ${this.theme.warning}`;
|
|
1339
|
-
iconWrap.style.color = this.theme.warning;
|
|
1340
|
-
iconWrap.style.fontSize = "30px";
|
|
1341
1566
|
statusBadge.style.color = this.theme.textMuted;
|
|
1342
1567
|
statusBadge.textContent = "Still processingβ¦";
|
|
1343
|
-
title.textContent = "
|
|
1344
|
-
desc.textContent = "
|
|
1568
|
+
title.textContent = "Still verifying";
|
|
1569
|
+
desc.textContent = "This is taking a little longer than usual. You can safely close this β your result will be available in your account.";
|
|
1345
1570
|
};
|
|
1346
|
-
const applyPassed = () => {
|
|
1571
|
+
const applyPassed = (result) => {
|
|
1347
1572
|
clearUiTimeout();
|
|
1348
1573
|
iconWrap.innerHTML = "β";
|
|
1349
|
-
iconWrap.style.
|
|
1350
|
-
|
|
1351
|
-
iconWrap.style.color = this.theme.success;
|
|
1352
|
-
iconWrap.style.fontSize = "36px";
|
|
1574
|
+
iconWrap.style.cssText += `background:${this.theme.successBg};border:3px solid ${this.theme.success};color:${this.theme.success};font-size:36px;font-weight:700;`;
|
|
1575
|
+
resultCard.style.borderColor = this.theme.success;
|
|
1353
1576
|
statusBadge.style.color = this.theme.success;
|
|
1354
1577
|
statusBadge.textContent = "Verified β";
|
|
1355
1578
|
title.textContent = "Identity Verified";
|
|
1356
|
-
desc.textContent = "Your identity
|
|
1579
|
+
desc.textContent = "Your identity has been confirmed successfully.";
|
|
1580
|
+
// ββ Show name if available βββββββββββββββββββββββββββββββββββββββ
|
|
1581
|
+
// Pull from IVS customer_profile (data verification) or
|
|
1582
|
+
// document OCR result (document verification) β whichever has data.
|
|
1583
|
+
const profile = result?.customer_profile || result?.data || {};
|
|
1584
|
+
const docResult = result?.document || {};
|
|
1585
|
+
const ocr = docResult?.ocr_result || {};
|
|
1586
|
+
const firstName = (profile?.first_name || ocr?.first_name || docResult?.first_name || "").trim();
|
|
1587
|
+
const lastName = (profile?.last_name || profile?.surname ||
|
|
1588
|
+
ocr?.last_name || ocr?.surname || docResult?.last_name || "").trim();
|
|
1589
|
+
const fullName = [firstName, lastName].filter(Boolean).join(" ");
|
|
1590
|
+
const nameEl = document.getElementById("sc-result-name");
|
|
1591
|
+
if (fullName && nameEl) {
|
|
1592
|
+
nameEl.textContent = fullName;
|
|
1593
|
+
nameRow.style.display = "flex";
|
|
1594
|
+
}
|
|
1595
|
+
// ββ Show ID type βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1596
|
+
const rawDocType = (docResult?.document_type ||
|
|
1597
|
+
this.selectedIdType ||
|
|
1598
|
+
result?.identifier_type ||
|
|
1599
|
+
"");
|
|
1600
|
+
const docTypeLabel = _formatDocType(rawDocType);
|
|
1601
|
+
const docTypeEl = document.getElementById("sc-result-doctype");
|
|
1602
|
+
if (docTypeLabel && docTypeEl) {
|
|
1603
|
+
docTypeEl.textContent = docTypeLabel;
|
|
1604
|
+
docTypeRow.style.display = "flex";
|
|
1605
|
+
}
|
|
1357
1606
|
};
|
|
1358
1607
|
const applyFailed = (reason) => {
|
|
1359
1608
|
clearUiTimeout();
|
|
1360
1609
|
iconWrap.innerHTML = "β";
|
|
1361
|
-
iconWrap.style.
|
|
1362
|
-
|
|
1363
|
-
iconWrap.style.color = this.theme.error;
|
|
1364
|
-
iconWrap.style.fontSize = "32px";
|
|
1610
|
+
iconWrap.style.cssText += `background:${this.theme.errorBg};border:3px solid ${this.theme.error};color:${this.theme.error};font-size:32px;`;
|
|
1611
|
+
resultCard.style.borderColor = this.theme.error;
|
|
1365
1612
|
statusBadge.style.color = this.theme.error;
|
|
1366
|
-
statusBadge.textContent = "
|
|
1367
|
-
title.textContent = "Verification
|
|
1368
|
-
desc.textContent = "
|
|
1369
|
-
|
|
1370
|
-
|
|
1613
|
+
statusBadge.textContent = "Not verified";
|
|
1614
|
+
title.textContent = "Verification Unsuccessful";
|
|
1615
|
+
desc.textContent = "We were unable to confirm your identity this time.";
|
|
1616
|
+
// Show a clean, actionable reason β never raw technical strings
|
|
1617
|
+
const safeReason = _sanitizeUserMessage(reason);
|
|
1618
|
+
if (safeReason) {
|
|
1619
|
+
failureMsg.textContent = safeReason;
|
|
1371
1620
|
failureMsg.style.display = "block";
|
|
1372
1621
|
}
|
|
1622
|
+
// Show retry button on failure
|
|
1623
|
+
retryBtn.style.display = "block";
|
|
1373
1624
|
};
|
|
1374
1625
|
const poll = async () => {
|
|
1375
1626
|
if (this.isDestroyed)
|
|
@@ -1379,34 +1630,32 @@ class SmartComplyFlow {
|
|
|
1379
1630
|
const result = await this.sdk.liveness.pollResult(entryId);
|
|
1380
1631
|
if (result.status === "passed") {
|
|
1381
1632
|
onResolved("passed");
|
|
1382
|
-
applyPassed();
|
|
1633
|
+
applyPassed(result);
|
|
1383
1634
|
return;
|
|
1384
1635
|
}
|
|
1385
1636
|
if (result.status === "failed") {
|
|
1386
|
-
onResolved(
|
|
1387
|
-
|
|
1388
|
-
applyFailed(reason);
|
|
1637
|
+
onResolved("failed");
|
|
1638
|
+
applyFailed(result.failure_reason || null);
|
|
1389
1639
|
return;
|
|
1390
1640
|
}
|
|
1391
|
-
// Still
|
|
1641
|
+
// Still processing β schedule next poll
|
|
1392
1642
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1393
1643
|
this._pollStatusTimer = setTimeout(poll, 3000);
|
|
1394
1644
|
}
|
|
1395
1645
|
else if (!this.isDestroyed) {
|
|
1396
|
-
applyTimeout();
|
|
1646
|
+
applyTimeout();
|
|
1397
1647
|
}
|
|
1398
1648
|
}
|
|
1399
1649
|
catch {
|
|
1400
|
-
// Network hiccup β retry with a slightly longer delay
|
|
1401
1650
|
if (polls < MAX_POLLS && !this.isDestroyed) {
|
|
1402
1651
|
this._pollStatusTimer = setTimeout(poll, 5000);
|
|
1403
1652
|
}
|
|
1404
1653
|
else if (!this.isDestroyed) {
|
|
1405
|
-
applyTimeout();
|
|
1654
|
+
applyTimeout();
|
|
1406
1655
|
}
|
|
1407
1656
|
}
|
|
1408
1657
|
};
|
|
1409
|
-
// First poll after
|
|
1658
|
+
// First poll after 4s β gives Celery task time to start
|
|
1410
1659
|
this._pollStatusTimer = setTimeout(poll, 4000);
|
|
1411
1660
|
}
|
|
1412
1661
|
renderResult(_container) {
|
|
@@ -1419,23 +1668,51 @@ class SmartComplyFlow {
|
|
|
1419
1668
|
return;
|
|
1420
1669
|
this.contentArea.innerHTML = "";
|
|
1421
1670
|
const wrapper = document.createElement("div");
|
|
1671
|
+
wrapper.setAttribute("role", "alert");
|
|
1422
1672
|
wrapper.style.cssText = `
|
|
1423
1673
|
display:flex;flex-direction:column;align-items:center;
|
|
1424
1674
|
text-align:center;gap:16px;padding:20px 0;
|
|
1425
1675
|
`;
|
|
1676
|
+
// Classify the error into a user-friendly category
|
|
1426
1677
|
const isCameraErr = /camera|permission|notallowed|notfound/i.test(message);
|
|
1427
1678
|
const isNetworkErr = /network|timeout|connection|fetch/i.test(message);
|
|
1428
|
-
const isAuthErr = /session|token
|
|
1429
|
-
const
|
|
1679
|
+
const isAuthErr = /session.*expired|token.*expired|authentication/i.test(message);
|
|
1680
|
+
const isDocQuality = /blurry|blur|quality|glare|dark|bright|lighting/i.test(message);
|
|
1681
|
+
const isExpiredDoc = /document.*expired|expired.*document/i.test(message);
|
|
1682
|
+
const isFaceMatch = /face.*match|selfie.*match|does not match/i.test(message);
|
|
1683
|
+
const errorIcon = isCameraErr ? "π·"
|
|
1684
|
+
: isNetworkErr ? "π‘"
|
|
1685
|
+
: isAuthErr ? "β±"
|
|
1686
|
+
: isDocQuality ? "π"
|
|
1687
|
+
: isExpiredDoc ? "π
"
|
|
1688
|
+
: isFaceMatch ? "π€³"
|
|
1689
|
+
: "β";
|
|
1430
1690
|
const errorTitle = isCameraErr ? "Camera Access Required"
|
|
1431
1691
|
: isNetworkErr ? "Connection Problem"
|
|
1432
1692
|
: isAuthErr ? "Session Expired"
|
|
1433
|
-
: "
|
|
1693
|
+
: isDocQuality ? "Document Photo Unclear"
|
|
1694
|
+
: isExpiredDoc ? "Document Expired"
|
|
1695
|
+
: isFaceMatch ? "Face Match Failed"
|
|
1696
|
+
: "Verification Unsuccessful";
|
|
1697
|
+
// Map to user-facing guidance β never show raw internal message
|
|
1698
|
+
const userMessage = isCameraErr
|
|
1699
|
+
? "Please allow camera access in your browser settings and try again."
|
|
1700
|
+
: isNetworkErr
|
|
1701
|
+
? "Check your internet connection and try again."
|
|
1702
|
+
: isAuthErr
|
|
1703
|
+
? "Your session has expired. Please start again."
|
|
1704
|
+
: isDocQuality
|
|
1705
|
+
? _sanitizeUserMessage(message) || "Please retake your document photo in good lighting with no glare."
|
|
1706
|
+
: isExpiredDoc
|
|
1707
|
+
? "The document you submitted has expired. Please use a valid, current ID."
|
|
1708
|
+
: isFaceMatch
|
|
1709
|
+
? "Your selfie did not match the photo on your ID. Please ensure you are using your own valid document and face the camera directly."
|
|
1710
|
+
: _sanitizeUserMessage(message) || "We were unable to complete your verification. Please try again.";
|
|
1434
1711
|
const iconWrap = document.createElement("div");
|
|
1435
1712
|
iconWrap.style.cssText = `
|
|
1436
1713
|
width:72px;height:72px;border-radius:50%;
|
|
1437
1714
|
display:flex;align-items:center;justify-content:center;
|
|
1438
|
-
font-size
|
|
1715
|
+
font-size:28px;
|
|
1439
1716
|
background:${this.theme.errorBg};
|
|
1440
1717
|
border:3px solid ${this.theme.error};
|
|
1441
1718
|
`;
|
|
@@ -1447,22 +1724,16 @@ class SmartComplyFlow {
|
|
|
1447
1724
|
wrapper.appendChild(title);
|
|
1448
1725
|
const desc = document.createElement("p");
|
|
1449
1726
|
desc.style.cssText = `color:${this.theme.textSecondary};font-size:14px;line-height:1.6;margin:0;max-width:300px;`;
|
|
1450
|
-
desc.textContent =
|
|
1727
|
+
desc.textContent = userMessage;
|
|
1451
1728
|
wrapper.appendChild(desc);
|
|
1452
1729
|
const retryBtn = this.createPrimaryButton("Try Again");
|
|
1730
|
+
retryBtn.setAttribute("aria-label", "Try verification again");
|
|
1453
1731
|
retryBtn.addEventListener("click", () => {
|
|
1454
|
-
|
|
1455
|
-
// (re-initializing would fail with CLIENT_ID_ALREADY_USED)
|
|
1456
|
-
if (this.sdk.sessionToken && this.sdkConfig) {
|
|
1457
|
-
this.showStep("welcome");
|
|
1458
|
-
}
|
|
1459
|
-
else {
|
|
1460
|
-
// Only re-initialize if session was never created
|
|
1461
|
-
this.initialize();
|
|
1462
|
-
}
|
|
1732
|
+
this.initialize();
|
|
1463
1733
|
});
|
|
1464
1734
|
wrapper.appendChild(retryBtn);
|
|
1465
1735
|
const closeBtn = this.createSecondaryButton("Close");
|
|
1736
|
+
closeBtn.setAttribute("aria-label", "Close verification widget");
|
|
1466
1737
|
closeBtn.addEventListener("click", () => {
|
|
1467
1738
|
this.options.onError?.(new Error(message));
|
|
1468
1739
|
this.close();
|
|
@@ -1475,6 +1746,7 @@ class SmartComplyFlow {
|
|
|
1475
1746
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1476
1747
|
createHeader() {
|
|
1477
1748
|
const header = document.createElement("div");
|
|
1749
|
+
header.setAttribute("role", "banner");
|
|
1478
1750
|
header.style.cssText = `
|
|
1479
1751
|
display:flex;align-items:center;justify-content:space-between;
|
|
1480
1752
|
padding:11px 20px;border-bottom:1px solid ${this.theme.border};
|
|
@@ -1484,6 +1756,7 @@ class SmartComplyFlow {
|
|
|
1484
1756
|
leftArea.style.cssText = "display:flex;align-items:center;gap:8px;min-width:0;";
|
|
1485
1757
|
this.headerBackBtn = document.createElement("button");
|
|
1486
1758
|
this.headerBackBtn.innerHTML = "←";
|
|
1759
|
+
this.headerBackBtn.setAttribute("aria-label", "Go back to previous step");
|
|
1487
1760
|
this.headerBackBtn.style.cssText = `
|
|
1488
1761
|
width:30px;height:30px;border-radius:8px;flex-shrink:0;
|
|
1489
1762
|
display:none;align-items:center;justify-content:center;
|
|
@@ -1492,6 +1765,7 @@ class SmartComplyFlow {
|
|
|
1492
1765
|
background:${this.theme.cardBg};color:${this.theme.text};
|
|
1493
1766
|
transition:all 0.2s ease;
|
|
1494
1767
|
`;
|
|
1768
|
+
this.headerBackBtn.style.cssText += `:focus-visible{outline:2px solid ${this.theme.primary};outline-offset:2px;}`;
|
|
1495
1769
|
this.headerBackBtn.addEventListener("mouseenter", () => {
|
|
1496
1770
|
if (this.headerBackBtn) {
|
|
1497
1771
|
this.headerBackBtn.style.background = this.theme.shimmer;
|
|
@@ -1504,6 +1778,7 @@ class SmartComplyFlow {
|
|
|
1504
1778
|
});
|
|
1505
1779
|
leftArea.appendChild(this.headerBackBtn);
|
|
1506
1780
|
this.brandLabel = document.createElement("div");
|
|
1781
|
+
this.brandLabel.setAttribute("aria-label", "Verification provider");
|
|
1507
1782
|
this.brandLabel.style.cssText = `
|
|
1508
1783
|
font-size:15px;font-weight:700;color:${this.theme.text};
|
|
1509
1784
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -1513,6 +1788,8 @@ class SmartComplyFlow {
|
|
|
1513
1788
|
header.appendChild(leftArea);
|
|
1514
1789
|
// Step indicator
|
|
1515
1790
|
this.stepLabel = document.createElement("div");
|
|
1791
|
+
this.stepLabel.setAttribute("aria-live", "polite");
|
|
1792
|
+
this.stepLabel.setAttribute("aria-atomic", "true");
|
|
1516
1793
|
this.stepLabel.style.cssText = `
|
|
1517
1794
|
font-size:12px;color:${this.theme.textMuted};
|
|
1518
1795
|
font-variant-numeric:tabular-nums;flex-shrink:0;
|
|
@@ -1521,6 +1798,7 @@ class SmartComplyFlow {
|
|
|
1521
1798
|
// Close button
|
|
1522
1799
|
const closeBtn = document.createElement("button");
|
|
1523
1800
|
closeBtn.innerHTML = "β";
|
|
1801
|
+
closeBtn.setAttribute("aria-label", "Close identity verification");
|
|
1524
1802
|
closeBtn.style.cssText = `
|
|
1525
1803
|
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
1526
1804
|
display:flex;align-items:center;justify-content:center;
|
|
@@ -1567,6 +1845,7 @@ class SmartComplyFlow {
|
|
|
1567
1845
|
}
|
|
1568
1846
|
createOptionCard(title, subtitle, onClick) {
|
|
1569
1847
|
const card = document.createElement("button");
|
|
1848
|
+
card.setAttribute("aria-label", subtitle ? `${title}: ${subtitle}` : title);
|
|
1570
1849
|
card.style.cssText = `
|
|
1571
1850
|
width:100%;padding:13px 16px;border-radius:12px;
|
|
1572
1851
|
background:${this.theme.cardBg};
|
|
@@ -1658,7 +1937,7 @@ class SmartComplyFlow {
|
|
|
1658
1937
|
const allFields = Object.values(channels)
|
|
1659
1938
|
.flat()
|
|
1660
1939
|
.flatMap((ch) => ch.fields || []);
|
|
1661
|
-
return allFields.some((f) => f.type
|
|
1940
|
+
return allFields.some((f) => isUploadFieldType(f.type));
|
|
1662
1941
|
}
|
|
1663
1942
|
updateProgress() {
|
|
1664
1943
|
const isDocFlow = this.isDocumentFlow();
|
|
@@ -1745,6 +2024,12 @@ class SmartComplyFlow {
|
|
|
1745
2024
|
#sc-flow-modal * {
|
|
1746
2025
|
box-sizing: border-box;
|
|
1747
2026
|
}
|
|
2027
|
+
#sc-flow-modal button:focus-visible,
|
|
2028
|
+
#sc-flow-modal select:focus-visible,
|
|
2029
|
+
#sc-flow-modal input:focus-visible {
|
|
2030
|
+
outline: 2px solid #3b82f6;
|
|
2031
|
+
outline-offset: 2px;
|
|
2032
|
+
}
|
|
1748
2033
|
#sc-flow-modal::-webkit-scrollbar {
|
|
1749
2034
|
width: 4px;
|
|
1750
2035
|
}
|