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