smartcomply-web-sdk 1.0.99 β†’ 1.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/client/Smartcomply.d.ts +1 -1
  2. package/dist/client/Smartcomply.d.ts.map +1 -1
  3. package/dist/client/Smartcomply.js +1 -1
  4. package/dist/client/Smartcomply.js.map +1 -1
  5. package/dist/esm/client/Smartcomply.d.ts +1 -1
  6. package/dist/esm/client/Smartcomply.d.ts.map +1 -1
  7. package/dist/esm/client/Smartcomply.js +1 -1
  8. package/dist/esm/client/Smartcomply.js.map +1 -1
  9. package/dist/esm/flow/DocumentCapture.d.ts +3 -19
  10. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  11. package/dist/esm/flow/DocumentCapture.js +22 -170
  12. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  13. package/dist/esm/flow/SmartComplyFlow.d.ts +0 -1
  14. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  15. package/dist/esm/flow/SmartComplyFlow.js +67 -94
  16. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  17. package/dist/esm/types/onboarding.d.ts +2 -0
  18. package/dist/esm/types/onboarding.d.ts.map +1 -1
  19. package/dist/esm/utils/channels.d.ts +15 -0
  20. package/dist/esm/utils/channels.d.ts.map +1 -1
  21. package/dist/esm/utils/channels.js +41 -0
  22. package/dist/esm/utils/channels.js.map +1 -1
  23. package/dist/esm/utils/constants.d.ts +9 -8
  24. package/dist/esm/utils/constants.d.ts.map +1 -1
  25. package/dist/esm/utils/constants.js +10 -9
  26. package/dist/esm/utils/constants.js.map +1 -1
  27. package/dist/esm/utils/documentRules.d.ts +1 -2
  28. package/dist/esm/utils/documentRules.d.ts.map +1 -1
  29. package/dist/esm/utils/documentRules.js +1 -2
  30. package/dist/esm/utils/documentRules.js.map +1 -1
  31. package/dist/flow/DocumentCapture.d.ts +3 -19
  32. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  33. package/dist/flow/DocumentCapture.js +22 -170
  34. package/dist/flow/DocumentCapture.js.map +1 -1
  35. package/dist/flow/SmartComplyFlow.d.ts +0 -1
  36. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  37. package/dist/flow/SmartComplyFlow.js +65 -92
  38. package/dist/flow/SmartComplyFlow.js.map +1 -1
  39. package/dist/smartcomply.browser.js +94 -256
  40. package/dist/smartcomply.browser.js.map +3 -3
  41. package/dist/types/onboarding.d.ts +2 -0
  42. package/dist/types/onboarding.d.ts.map +1 -1
  43. package/dist/utils/channels.d.ts +15 -0
  44. package/dist/utils/channels.d.ts.map +1 -1
  45. package/dist/utils/channels.js +44 -0
  46. package/dist/utils/channels.js.map +1 -1
  47. package/dist/utils/constants.d.ts +9 -8
  48. package/dist/utils/constants.d.ts.map +1 -1
  49. package/dist/utils/constants.js +10 -9
  50. package/dist/utils/constants.js.map +1 -1
  51. package/dist/utils/documentRules.d.ts +1 -2
  52. package/dist/utils/documentRules.d.ts.map +1 -1
  53. package/dist/utils/documentRules.js +1 -2
  54. package/dist/utils/documentRules.js.map +1 -1
  55. package/package.json +1 -1
@@ -1,11 +1,10 @@
1
1
  import { SmartComply } from "../client/Smartcomply";
2
2
  import { getTheme, TYPE, RADIUS } from "./theme";
3
3
  import { DocumentCapture, DocumentCaptureCancelledError } from "./DocumentCapture";
4
- import { isUploadFieldType } from "../utils/channels";
5
- import { isPdf } from "../utils/documentFiles";
4
+ import { countryCodeFor, countryLabel, filterCountries, isUploadFieldType } from "../utils/channels";
6
5
  import { collectDeviceMetadata } from "../utils/device";
7
6
  import { requiresBackSide } from "../utils/documentRules";
8
- import { DEFAULT_CHALLENGE_ACTIONS } from "../utils/constants";
7
+ import { DEFAULT_CHALLENGE_ACTIONS, MAX_DOCUMENT_BYTES } from "../utils/constants";
9
8
  import { icon } from "./icons";
10
9
  import { renderSpinner, renderInlineSpinner } from "./spinner";
11
10
  import { SDKError } from "../errors/SDKError";
@@ -53,15 +52,9 @@ const STEP_ORDER = [
53
52
  "liveness",
54
53
  "done",
55
54
  ];
56
- const COUNTRY_LABELS = {
57
- nigeria: "πŸ‡³πŸ‡¬ Nigeria",
58
- global: "🌍 Other Countries",
59
- ghana: "πŸ‡¬πŸ‡­ Ghana",
60
- kenya: "πŸ‡°πŸ‡ͺ Kenya",
61
- south_africa: "πŸ‡ΏπŸ‡¦ South Africa",
62
- united_states: "πŸ‡ΊπŸ‡Έ United States",
63
- united_kingdom: "πŸ‡¬πŸ‡§ United Kingdom",
64
- };
55
+ // From this many countries up, the country screen shows a search box: a
56
+ // setup can offer passports from every supported country (188).
57
+ const COUNTRY_SEARCH_MIN = 7;
65
58
  /**
66
59
  * Whether a SPECIFIC channel is a document-scan item, vs. a plain
67
60
  * number-entry one. channels[country] is a flat list that can mix
@@ -304,24 +297,6 @@ function _parseOcrDate(raw) {
304
297
  const d = new Date(s);
305
298
  return isNaN(d.getTime()) ? null : d;
306
299
  }
307
- // ── W12: Image compression before document upload ────────────────────────────
308
- async function _compressImage(blob, maxWidth = 1600, quality = 0.85) {
309
- try {
310
- const bmp = await createImageBitmap(blob);
311
- const scale = Math.min(1, maxWidth / bmp.width);
312
- const w = Math.round(bmp.width * scale);
313
- const h = Math.round(bmp.height * scale);
314
- const canvas = document.createElement("canvas");
315
- canvas.width = w;
316
- canvas.height = h;
317
- canvas.getContext("2d").drawImage(bmp, 0, 0, w, h);
318
- bmp.close();
319
- return await new Promise((res, rej) => canvas.toBlob(b => b ? res(b) : rej(new Error("Compression failed")), "image/jpeg", quality));
320
- }
321
- catch {
322
- return blob; // fallback to original on any error
323
- }
324
- }
325
300
  // ── Document type code β†’ human label ─────────────────────────────────────────
326
301
  function _formatDocType(code) {
327
302
  const map = {
@@ -1164,15 +1139,47 @@ export class SmartComplyFlow {
1164
1139
  subtitle.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin-top:-8px;`;
1165
1140
  subtitle.textContent = "Choose the country that issued your ID";
1166
1141
  container.appendChild(subtitle);
1167
- for (const country of countries) {
1168
- const count = channels[country].data_verification.length
1169
- + channels[country].document_verification.length;
1170
- const card = this.createOptionCard(COUNTRY_LABELS[country] || country, `${count} ID type${count > 1 ? "s" : ""} available`, () => {
1171
- this.selectedCountry = country;
1172
- this.showStep("id_type");
1173
- });
1174
- container.appendChild(card);
1142
+ const list = document.createElement("div");
1143
+ list.setAttribute("role", "list");
1144
+ list.style.cssText = "display:flex;flex-direction:column;gap:10px;max-height:min(52vh,420px);overflow-y:auto;padding:2px;";
1145
+ const renderList = (query) => {
1146
+ list.innerHTML = "";
1147
+ const matches = filterCountries(channels, query);
1148
+ if (matches.length === 0) {
1149
+ const empty = document.createElement("div");
1150
+ empty.setAttribute("role", "status");
1151
+ empty.style.cssText = `color:${this.theme.textSecondary};font-size:13px;line-height:1.6;padding:8px 2px;`;
1152
+ empty.textContent = `No country matches "${query.trim()}". Check the spelling or try another name.`;
1153
+ list.appendChild(empty);
1154
+ return;
1155
+ }
1156
+ for (const country of matches) {
1157
+ const count = channels[country].data_verification.length
1158
+ + channels[country].document_verification.length;
1159
+ const card = this.createOptionCard(countryLabel(channels, country), `${count} ID type${count > 1 ? "s" : ""} available`, () => {
1160
+ this.selectedCountry = country;
1161
+ this.showStep("id_type");
1162
+ });
1163
+ card.setAttribute("role", "listitem");
1164
+ list.appendChild(card);
1165
+ }
1166
+ };
1167
+ if (countries.length >= COUNTRY_SEARCH_MIN) {
1168
+ const search = document.createElement("input");
1169
+ search.type = "search";
1170
+ search.placeholder = "Search countries";
1171
+ search.setAttribute("aria-label", "Search countries");
1172
+ search.autocomplete = "off";
1173
+ search.style.cssText = `
1174
+ width:100%;box-sizing:border-box;padding:11px 14px;border-radius:${RADIUS.md}px;
1175
+ background:${this.theme.cardBg};color:${this.theme.text};
1176
+ border:1.5px solid ${this.theme.border};font-size:14px;outline-offset:2px;
1177
+ `;
1178
+ search.addEventListener("input", () => renderList(search.value));
1179
+ container.appendChild(search);
1175
1180
  }
1181
+ renderList("");
1182
+ container.appendChild(list);
1176
1183
  }
1177
1184
  // ── ID Type Select ──────────────────────────────────────────────
1178
1185
  renderIdTypeSelect(container) {
@@ -1760,8 +1767,15 @@ export class SmartComplyFlow {
1760
1767
  faceWrap.appendChild(faceImg);
1761
1768
  container.appendChild(faceWrap);
1762
1769
  }
1763
- // Expiry soon warning
1764
- if (this._ocrExpirySoon && !this._ocrIsExpired) {
1770
+ // Expired or expiring-soon notice
1771
+ if (this._ocrIsExpired) {
1772
+ const expired = document.createElement("div");
1773
+ expired.setAttribute("role", "alert");
1774
+ expired.style.cssText = `width:100%;padding:10px 14px;border-radius:10px;background:${this.theme.errorBg};border:1px solid ${this.theme.error}40;color:${this.theme.error};font-size:12px;text-align:left;line-height:1.5;`;
1775
+ expired.textContent = "⚠️ This document has expired. You can continue, but a check with an expired document won't be approved. Use a valid document if you have one.";
1776
+ container.appendChild(expired);
1777
+ }
1778
+ else if (this._ocrExpirySoon) {
1765
1779
  const warn = document.createElement("div");
1766
1780
  warn.style.cssText = `width:100%;padding:10px 14px;border-radius:10px;background:${this.theme.warning}18;border:1px solid ${this.theme.warning}40;color:${this.theme.warning};font-size:12px;text-align:left;line-height:1.5;`;
1767
1781
  warn.textContent = "⚠️ Your document expires soon. Some services may not accept it, but you can still continue.";
@@ -1864,12 +1878,10 @@ export class SmartComplyFlow {
1864
1878
  polls++;
1865
1879
  try {
1866
1880
  const result = await this.sdk.liveness.pollResult(entryId);
1867
- // W7: Expired document β€” stop, do not proceed to liveness
1868
- if (result.is_expired === true) {
1869
- container.innerHTML = "";
1870
- this._renderExpiredDocument(container);
1871
- return;
1872
- }
1881
+ // An expired document does not stop the flow: liveness and the face
1882
+ // match still run so the client gets the full result, and the backend
1883
+ // fails the check at the end with "Document has expired." The confirm
1884
+ // screen tells the user (see _ocrIsExpired).
1873
1885
  // W8: OCR quality fail β€” offer retake
1874
1886
  if (result.ocr_quality_failed === true) {
1875
1887
  container.innerHTML = "";
@@ -1945,32 +1957,6 @@ export class SmartComplyFlow {
1945
1957
  // First poll after 3s β€” give OCR submission time to start
1946
1958
  this._ocrPollTimer = setTimeout(poll, 3000);
1947
1959
  }
1948
- _renderExpiredDocument(container) {
1949
- container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
1950
- const icon = document.createElement("div");
1951
- icon.style.cssText = `width:72px;height:72px;border-radius:50%;background:${this.theme.errorBg};border:3px solid ${this.theme.error};display:flex;align-items:center;justify-content:center;font-size:28px;`;
1952
- icon.textContent = "πŸ“…";
1953
- container.appendChild(icon);
1954
- const t = document.createElement("h2");
1955
- t.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
1956
- t.textContent = "Document Expired";
1957
- container.appendChild(t);
1958
- const d = document.createElement("p");
1959
- d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
1960
- d.textContent = "The document you submitted has expired. Please use a valid, current ID.";
1961
- container.appendChild(d);
1962
- const retryBtn = this.createPrimaryButton("Try a different document");
1963
- retryBtn.addEventListener("click", () => {
1964
- this.livenessEntryId = null;
1965
- this.documentBlob = null;
1966
- this.documentBackBlob = null;
1967
- this.showStep("document_capture");
1968
- });
1969
- container.appendChild(retryBtn);
1970
- const endBtn = this.createSecondaryButton("End session");
1971
- endBtn.addEventListener("click", () => { this.options.onError?.(new Error("Document expired")); this.close(); });
1972
- container.appendChild(endBtn);
1973
- }
1974
1960
  _renderOcrQualityFail(container, reason) {
1975
1961
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
1976
1962
  const icon = document.createElement("div");
@@ -2158,15 +2144,11 @@ export class SmartComplyFlow {
2158
2144
  * Process document capture result with front/back support
2159
2145
  */
2160
2146
  async processDocumentCaptureResult(result, captureContainer, container) {
2161
- // B2+P2: Compress both images once here. Do NOT assign documentBlob twice
2162
- // (old code set it to the raw blob first, then overwrote with compressed).
2163
- // processDocumentBlob also ran _compressImage β€” that second pass is removed
2164
- // below so compression only happens once.
2165
- // A PDF is sent as-is: only photos are re-encoded.
2166
- this.documentBlob = isPdf(result.front) ? result.front : await _compressImage(result.front, 1600, 0.85);
2167
- this.documentBackBlob = !result.back
2168
- ? null
2169
- : isPdf(result.back) ? result.back : await _compressImage(result.back, 1600, 0.85);
2147
+ // Uploaded exactly as captured or picked, photo or PDF: re-encoding a
2148
+ // small passport photo as JPEG blurred its machine-readable lines and the
2149
+ // OCR rejected a clear Rwandan passport as "country: unknown".
2150
+ this.documentBlob = result.front;
2151
+ this.documentBackBlob = result.back || null;
2170
2152
  this._documentViaCamera = result.viaCamera;
2171
2153
  // Process the front image β€” pass the already-compressed blob
2172
2154
  await this.processDocumentBlob(this.documentBlob, captureContainer, container);
@@ -2179,12 +2161,11 @@ export class SmartComplyFlow {
2179
2161
  let retryCount = 0;
2180
2162
  const attemptProcessing = async () => {
2181
2163
  try {
2182
- // Validate blob β€” compression already done in processDocumentCaptureResult (P2)
2183
2164
  if (!blob || blob.size === 0) {
2184
2165
  throw new Error("Failed to capture document. Please try again.");
2185
2166
  }
2186
- if (blob.size > 5 * 1024 * 1024) {
2187
- throw new Error("Document image is too large. Please retake in better lighting or use a clearer photo.");
2167
+ if (blob.size > MAX_DOCUMENT_BYTES) {
2168
+ throw new Error(`Document is too large. Please use a file smaller than ${Math.round(MAX_DOCUMENT_BYTES / (1024 * 1024))}MB.`);
2188
2169
  }
2189
2170
  this.documentBlob = blob;
2190
2171
  // Show scanning state
@@ -2203,11 +2184,7 @@ export class SmartComplyFlow {
2203
2184
  || this.idNumber
2204
2185
  || this.selectedIdType
2205
2186
  || `DOC-${Date.now()}`;
2206
- const COUNTRY_CODES = {
2207
- nigeria: "NG", ghana: "GH", kenya: "KE",
2208
- south_africa: "ZA", united_states: "US", united_kingdom: "GB", global: "US",
2209
- };
2210
- const country = COUNTRY_CODES[this.selectedCountry] || this.selectedCountry.toUpperCase().slice(0, 2);
2187
+ const country = countryCodeFor(this.sdkConfig?.channels || {}, this.selectedCountry);
2211
2188
  const deviceMeta = collectDeviceMetadata();
2212
2189
  let createResult;
2213
2190
  try {
@@ -2397,11 +2374,7 @@ export class SmartComplyFlow {
2397
2374
  ? ""
2398
2375
  : (this.idNumber || Object.values(this.collectedFields)[0] || this.selectedIdType || `DOC-${Date.now()}`);
2399
2376
  const identifierType = isDataVerification ? "" : this.selectedIdType;
2400
- const COUNTRY_CODES = {
2401
- nigeria: "NG", ghana: "GH", kenya: "KE",
2402
- south_africa: "ZA", united_states: "US", united_kingdom: "GB", global: "US",
2403
- };
2404
- const country = COUNTRY_CODES[this.selectedCountry] || this.selectedCountry.toUpperCase().slice(0, 2);
2377
+ const country = countryCodeFor(this.sdkConfig?.channels || {}, this.selectedCountry);
2405
2378
  // Start liveness check (this handles camera, detection, recording, submission)
2406
2379
  // If livenessEntryId exists (document flow), reuse it instead of creating a new one
2407
2380
  this.sdk.liveness