smartcomply-web-sdk 1.0.66 → 1.0.69

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 (45) hide show
  1. package/dist/esm/flow/DocumentCapture.d.ts +10 -0
  2. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
  3. package/dist/esm/flow/DocumentCapture.js +83 -5
  4. package/dist/esm/flow/DocumentCapture.js.map +1 -1
  5. package/dist/esm/flow/SmartComplyFlow.d.ts +19 -0
  6. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
  7. package/dist/esm/flow/SmartComplyFlow.js +216 -32
  8. package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
  9. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
  10. package/dist/esm/modules/liveness/LivenessUI.js +13 -3
  11. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
  12. package/dist/esm/modules/liveness/liveness.js +1 -1
  13. package/dist/esm/modules/liveness/liveness.js.map +1 -1
  14. package/dist/esm/utils/ImageQuality.d.ts +12 -1
  15. package/dist/esm/utils/ImageQuality.d.ts.map +1 -1
  16. package/dist/esm/utils/ImageQuality.js +12 -1
  17. package/dist/esm/utils/ImageQuality.js.map +1 -1
  18. package/dist/esm/utils/constants.d.ts +9 -6
  19. package/dist/esm/utils/constants.d.ts.map +1 -1
  20. package/dist/esm/utils/constants.js +9 -6
  21. package/dist/esm/utils/constants.js.map +1 -1
  22. package/dist/flow/DocumentCapture.d.ts +10 -0
  23. package/dist/flow/DocumentCapture.d.ts.map +1 -1
  24. package/dist/flow/DocumentCapture.js +83 -5
  25. package/dist/flow/DocumentCapture.js.map +1 -1
  26. package/dist/flow/SmartComplyFlow.d.ts +19 -0
  27. package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
  28. package/dist/flow/SmartComplyFlow.js +216 -32
  29. package/dist/flow/SmartComplyFlow.js.map +1 -1
  30. package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
  31. package/dist/modules/liveness/LivenessUI.js +13 -3
  32. package/dist/modules/liveness/LivenessUI.js.map +1 -1
  33. package/dist/modules/liveness/liveness.js +1 -1
  34. package/dist/modules/liveness/liveness.js.map +1 -1
  35. package/dist/smartcomply.browser.js +360 -43
  36. package/dist/smartcomply.browser.js.map +4 -4
  37. package/dist/utils/ImageQuality.d.ts +12 -1
  38. package/dist/utils/ImageQuality.d.ts.map +1 -1
  39. package/dist/utils/ImageQuality.js +12 -1
  40. package/dist/utils/ImageQuality.js.map +1 -1
  41. package/dist/utils/constants.d.ts +9 -6
  42. package/dist/utils/constants.d.ts.map +1 -1
  43. package/dist/utils/constants.js +9 -6
  44. package/dist/utils/constants.js.map +1 -1
  45. package/package.json +1 -1
@@ -3,8 +3,35 @@ import { getTheme, TYPE, RADIUS } from "./theme";
3
3
  import { DocumentCapture } from "./DocumentCapture";
4
4
  import { collectDeviceMetadata } from "../utils/device";
5
5
  import { requiresBackSide } from "../utils/documentRules";
6
+ import { DEFAULT_CHALLENGE_ACTIONS } from "../utils/constants";
6
7
  import { icon } from "./icons";
7
8
  import { renderSpinner, renderInlineSpinner } from "./spinner";
9
+ // A dropped camera/network/upload error is not the user's fault and must not
10
+ // cost them a retry attempt — only a genuine verification outcome should.
11
+ // Classified by message rather than a typed error class since the errors
12
+ // originate from several independent layers (CameraManager, VideoRecorder,
13
+ // LivenessUploader) that don't share a common error type today.
14
+ const _INFRASTRUCTURE_ERROR_PATTERNS = [
15
+ "camera connection was lost",
16
+ "network error",
17
+ "failed after all retries",
18
+ "camera access is not supported",
19
+ "camera access failed",
20
+ "recording failed unexpectedly",
21
+ ];
22
+ function _isInfrastructureError(err) {
23
+ const msg = String(err?.message || "").toLowerCase();
24
+ return _INFRASTRUCTURE_ERROR_PATTERNS.some((p) => msg.includes(p));
25
+ }
26
+ // Keep this plain-language and reassuring for infrastructure hiccups — the
27
+ // user did nothing wrong, so the copy should say so rather than sound like
28
+ // a rejection.
29
+ function _userFacingLivenessError(err) {
30
+ if (_isInfrastructureError(err)) {
31
+ return "We lost connection during your scan — this wasn't anything you did. Please check your camera and internet connection, then try again.";
32
+ }
33
+ return err?.message || "We couldn't verify your face. Please try again.";
34
+ }
8
35
  const STEP_ORDER = [
9
36
  "loading",
10
37
  "welcome",
@@ -273,9 +300,14 @@ export class SmartComplyFlow {
273
300
  this.isDestroyed = false;
274
301
  this.headerBackBtn = null;
275
302
  this._keydownHandler = null;
303
+ this._visibilityHandler = null;
304
+ this._hiddenAtMs = null;
305
+ this._popstateHandler = null;
306
+ this._historyGuardActive = false;
276
307
  // Retry safety — max 3 confirmation rejections or liveness failures per session
277
308
  this._confirmRetries = 0;
278
309
  this._livenessFailures = 0;
310
+ this._livenessAttemptId = 0;
279
311
  // IVS result stored for confirm_identity screen
280
312
  this._ivsName = "";
281
313
  this._ivsGender = "";
@@ -334,6 +366,26 @@ export class SmartComplyFlow {
334
366
  document.removeEventListener("keydown", this._keydownHandler);
335
367
  this._keydownHandler = null;
336
368
  }
369
+ if (this._visibilityHandler) {
370
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
371
+ this._visibilityHandler = null;
372
+ }
373
+ if (this._popstateHandler) {
374
+ window.removeEventListener("popstate", this._popstateHandler);
375
+ this._popstateHandler = null;
376
+ }
377
+ // Closing via the ✕/Escape/completion (not via the back gesture, which
378
+ // handles its own guard entry above) still leaves our one guard entry
379
+ // sitting on top of the host's history — back out of it so the user
380
+ // isn't left pressing back once "into" a widget that's already gone.
381
+ if (this._historyGuardActive) {
382
+ this._historyGuardActive = false;
383
+ try {
384
+ if (history.state?.scFlowGuard)
385
+ history.back();
386
+ }
387
+ catch { /* ignore — best effort only */ }
388
+ }
337
389
  this.docCapture?.destroy();
338
390
  // Release any pre-warmed camera stream
339
391
  if (this._prewarmedStream) {
@@ -464,6 +516,64 @@ export class SmartComplyFlow {
464
516
  }
465
517
  };
466
518
  document.addEventListener("keydown", this._keydownHandler);
519
+ // A tab backgrounded mid-scan (a notification, switching apps to copy a
520
+ // code, a phone call) leaves the camera stream and detection loop in an
521
+ // undefined state on most mobile browsers — rAF throttles/pauses while
522
+ // hidden, and some browsers suspend the camera outright. Rather than let
523
+ // the scan silently continue against stale state and fail confusingly,
524
+ // restart the liveness step cleanly once the tab has been away long
525
+ // enough to plausibly have disrupted an active camera session. A brief
526
+ // glance elsewhere (quick app switch, screen lock tap) is not enough to
527
+ // warrant restarting — only a real return-from-away.
528
+ const REBACKGROUND_RESTART_MS = 3000;
529
+ this._visibilityHandler = () => {
530
+ if (this.isDestroyed)
531
+ return;
532
+ if (document.hidden) {
533
+ this._hiddenAtMs = Date.now();
534
+ return;
535
+ }
536
+ const hiddenAt = this._hiddenAtMs;
537
+ this._hiddenAtMs = null;
538
+ if (hiddenAt === null)
539
+ return;
540
+ const awayMs = Date.now() - hiddenAt;
541
+ if (awayMs >= REBACKGROUND_RESTART_MS && this.currentStep === "liveness") {
542
+ this.showStep("liveness");
543
+ }
544
+ };
545
+ document.addEventListener("visibilitychange", this._visibilityHandler);
546
+ // Intercept the browser/OS back gesture so it steps back within the flow
547
+ // instead of navigating the host page away and abandoning the session
548
+ // mid-verification (camera left open, timers orphaned). One guard entry
549
+ // is pushed — not one per step — to minimize interference with a host
550
+ // page's own router; popstate re-arms it each time so back can be
551
+ // pressed repeatedly. If the current step has no back target, back
552
+ // closes the widget cleanly instead (same as the header ✕).
553
+ this._historyGuardActive = true;
554
+ try {
555
+ history.pushState({ scFlowGuard: true }, "");
556
+ }
557
+ catch {
558
+ this._historyGuardActive = false; // host disallows pushState — degrade silently
559
+ }
560
+ this._popstateHandler = () => {
561
+ if (this.isDestroyed || !this._historyGuardActive)
562
+ return;
563
+ const backAction = this._getBackAction(this.currentStep);
564
+ if (backAction) {
565
+ backAction();
566
+ }
567
+ else {
568
+ this.close();
569
+ return; // don't re-arm — we're closing, let the real back navigation proceed
570
+ }
571
+ try {
572
+ history.pushState({ scFlowGuard: true }, "");
573
+ }
574
+ catch { /* ignore — nothing more we can do */ }
575
+ };
576
+ window.addEventListener("popstate", this._popstateHandler);
467
577
  }
468
578
  async initialize() {
469
579
  this.showStep("loading");
@@ -655,7 +765,7 @@ export class SmartComplyFlow {
655
765
  container.appendChild(topRow);
656
766
  // Title + subtitle (compact)
657
767
  const titleWrap = document.createElement("div");
658
- titleWrap.style.cssText = "display:flex;flex-direction:column;gap:5px;";
768
+ titleWrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:5px;width:100%;";
659
769
  const title = document.createElement("h2");
660
770
  title.style.cssText = `
661
771
  color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;
@@ -670,7 +780,7 @@ export class SmartComplyFlow {
670
780
  const desc = document.createElement("p");
671
781
  desc.style.cssText = `
672
782
  color:${this.theme.textSecondary};font-size:13px;line-height:1.5;
673
- margin:0;max-width:300px;
783
+ margin:0 auto;max-width:300px;text-align:center;
674
784
  `;
675
785
  desc.textContent = isDocFlow
676
786
  ? "You'll need your document and camera access. This takes about 2 minutes."
@@ -732,14 +842,16 @@ export class SmartComplyFlow {
732
842
  btn.style.cssText += "margin-top:2px;";
733
843
  btn.addEventListener("click", () => this.showStep("country"));
734
844
  container.appendChild(btn);
735
- // Trust badge — "Powered by Adhere" already lives in the persistent
736
- // modal footer below; repeating it here just cost vertical space.
845
+ // Plain-language line on what the camera/photo is for and what happens
846
+ // to it — shown before the "Get Started" tap that leads to the first
847
+ // camera permission prompt, so the browser's own prompt isn't the only
848
+ // context the user gets for why a camera is being requested.
737
849
  const trust = document.createElement("div");
738
850
  trust.style.cssText = `
739
- display:flex;align-items:center;gap:6px;
740
- color:${this.theme.textMuted};font-size:10.5px;
851
+ display:flex;align-items:flex-start;gap:6px;text-align:left;
852
+ color:${this.theme.textMuted};font-size:10.5px;line-height:1.5;
741
853
  `;
742
- trust.textContent = "🔒 End-to-end encrypted";
854
+ trust.innerHTML = `<span style="flex-shrink:0;">🔒</span><span>Your photo and camera scan are used only to verify it's really you, and are kept encrypted.</span>`;
743
855
  container.appendChild(trust);
744
856
  }
745
857
  // ── Country Select ──────────────────────────────────────────────
@@ -1290,6 +1402,14 @@ export class SmartComplyFlow {
1290
1402
  this._renderRetryLimitReached(container);
1291
1403
  return;
1292
1404
  }
1405
+ // OCR reported "done" but extracted nothing usable — do not let the user
1406
+ // click through to liveness with no verified identity data. This is the
1407
+ // ocr_status==="done"-with-empty-fields sibling of the poll timeout case
1408
+ // (which _renderOcrTimeout already catches in renderOcrGate).
1409
+ if (!this._ocrName && !this._ocrDocNumber && !this._ocrExpiry) {
1410
+ this._renderOcrTimeout(container);
1411
+ return;
1412
+ }
1293
1413
  // W13: Pre-warm camera while user reads — but only if the document photo
1294
1414
  // itself came from the camera. Pre-warming here when the user uploaded
1295
1415
  // from gallery means requesting camera access before they've touched
@@ -1429,7 +1549,7 @@ export class SmartComplyFlow {
1429
1549
  // end. 45 x 2s = 90s comfortably covers that worst case while the loop
1430
1550
  // still exits immediately once ocrDone is true — the common case
1431
1551
  // (OCR done in a few seconds) is exactly as fast as before.
1432
- const MAX_POLLS = 45; // 45 × 2s = 90s timeout
1552
+ const MAX_POLLS = 45; // 45 x 2s = 90s timeout
1433
1553
  let polls = 0;
1434
1554
  const poll = async () => {
1435
1555
  if (this.isDestroyed)
@@ -1483,13 +1603,13 @@ export class SmartComplyFlow {
1483
1603
  this._ocrPollTimer = setTimeout(poll, 2000);
1484
1604
  }
1485
1605
  else if (!this.isDestroyed) {
1486
- // Genuinely exhausted the whole window — proceed anyway with whatever we have
1487
- statusEl.textContent = "Taking longer than usual…";
1488
- subEl.textContent = "We'll continue — you can confirm your details on the next screen.";
1489
- this._ocrPollTimer = setTimeout(() => {
1490
- if (!this.isDestroyed)
1491
- this.showStep("document_confirm");
1492
- }, 2000);
1606
+ // Genuinely exhausted the whole window with nothing usable read —
1607
+ // stop here instead of letting an unverified identity through to
1608
+ // liveness. document_confirm never blocks on missing fields (by
1609
+ // design, so a partial-but-real OCR read still lets the user
1610
+ // continue), so the gate has to live here, before that screen.
1611
+ container.innerHTML = "";
1612
+ this._renderOcrTimeout(container);
1493
1613
  }
1494
1614
  }
1495
1615
  catch {
@@ -1558,6 +1678,35 @@ export class SmartComplyFlow {
1558
1678
  });
1559
1679
  container.appendChild(retryBtn);
1560
1680
  }
1681
+ /**
1682
+ * OCR ran to completion but never produced a single usable field within
1683
+ * the poll window — do not let the flow reach liveness with no verified
1684
+ * identity data. Distinct from _renderOcrQualityFail (a bad photo) since
1685
+ * this covers a genuinely stuck/slow OCR pass instead of a rejected image.
1686
+ */
1687
+ _renderOcrTimeout(container) {
1688
+ container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;padding:20px 0;";
1689
+ const icon = document.createElement("div");
1690
+ 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;`;
1691
+ icon.textContent = "⏱️";
1692
+ container.appendChild(icon);
1693
+ const t = document.createElement("h2");
1694
+ t.style.cssText = `color:${this.theme.text};font-size:${TYPE.heading}px;font-weight:700;margin:0;`;
1695
+ t.textContent = "Couldn't Read Your Document";
1696
+ container.appendChild(t);
1697
+ const d = document.createElement("p");
1698
+ d.style.cssText = `color:${this.theme.textSecondary};font-size:13px;margin:0;max-width:280px;line-height:1.6;`;
1699
+ d.textContent = "We weren't able to read any details from your document. Please retake the photo in good lighting, with all four corners visible.";
1700
+ container.appendChild(d);
1701
+ const retryBtn = this.createPrimaryButton("Retake photo");
1702
+ retryBtn.addEventListener("click", () => {
1703
+ this.livenessEntryId = null;
1704
+ this.documentBlob = null;
1705
+ this.documentBackBlob = null;
1706
+ this.showStep("document_capture");
1707
+ });
1708
+ container.appendChild(retryBtn);
1709
+ }
1561
1710
  // ── W11: You're all done (end-user final screen) ─────────────────
1562
1711
  renderDone(container) {
1563
1712
  container.style.cssText += "display:flex;flex-direction:column;align-items:center;text-align:center;gap:16px;padding:24px 0;";
@@ -1816,6 +1965,11 @@ export class SmartComplyFlow {
1816
1965
  this._renderRetryLimitReached(container);
1817
1966
  return;
1818
1967
  }
1968
+ // Each render gets its own id so a stale attempt (e.g. abandoned after
1969
+ // the tab was backgrounded and we restarted the scan on return) can't
1970
+ // resolve/reject into the current one — see the visibilitychange
1971
+ // handler in mount(), which re-renders this step on return-to-foreground.
1972
+ const attemptId = ++this._livenessAttemptId;
1819
1973
  const title = this.createStepTitle("Face verification");
1820
1974
  container.appendChild(title);
1821
1975
  const desc = document.createElement("div");
@@ -1864,8 +2018,12 @@ export class SmartComplyFlow {
1864
2018
  error: this.theme.error,
1865
2019
  warning: this.theme.warning,
1866
2020
  },
1867
- }, ["BLINK"], this.livenessEntryId || undefined)
2021
+ }, [...DEFAULT_CHALLENGE_ACTIONS], this.livenessEntryId || undefined)
1868
2022
  .then((result) => {
2023
+ // A stale attempt (superseded by a restart after the tab was
2024
+ // backgrounded) resolving late must not act on the current screen.
2025
+ if (attemptId !== this._livenessAttemptId)
2026
+ return;
1869
2027
  // W11: After submit, end user is done — show "You're all done" screen.
1870
2028
  // Backend processing continues in background; client receives webhook.
1871
2029
  // Track liveness failures for retry limit (W14).
@@ -1878,8 +2036,18 @@ export class SmartComplyFlow {
1878
2036
  this.showStep("done");
1879
2037
  })
1880
2038
  .catch((err) => {
1881
- this._livenessFailures++;
1882
- this.showErrorStep(err.message || "Liveness verification failed. Please try again.");
2039
+ if (attemptId !== this._livenessAttemptId)
2040
+ return;
2041
+ // Only count a genuine verification failure against the retry limit —
2042
+ // a dropped camera, network blip, or upload failure isn't the user's
2043
+ // fault and shouldn't cost them one of their limited attempts. These
2044
+ // are infrastructure-layer errors (thrown/rejected); a real face-
2045
+ // match/quality failure comes back as a resolved result with
2046
+ // status "failed" instead (handled in .then() above).
2047
+ if (!_isInfrastructureError(err)) {
2048
+ this._livenessFailures++;
2049
+ }
2050
+ this.showErrorStep(_userFacingLivenessError(err));
1883
2051
  });
1884
2052
  }
1885
2053
  // ── Error ───────────────────────────────────────────────────────
@@ -2072,9 +2240,14 @@ export class SmartComplyFlow {
2072
2240
  const powered = document.createElement("span");
2073
2241
  powered.style.cssText = `font-size:11px;color:${this.theme.textMuted};`;
2074
2242
  powered.textContent = "Powered by ";
2075
- const brand = document.createElement("span");
2076
- brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};`;
2243
+ const brand = document.createElement("a");
2244
+ brand.href = "https://adhere.smartcomply.com/";
2245
+ brand.target = "_blank";
2246
+ brand.rel = "noopener noreferrer";
2247
+ brand.style.cssText = `font-size:11px;font-weight:700;color:${this.theme.primary};text-decoration:none;`;
2077
2248
  brand.textContent = "Adhere";
2249
+ brand.addEventListener("mouseenter", () => { brand.style.textDecoration = "underline"; });
2250
+ brand.addEventListener("mouseleave", () => { brand.style.textDecoration = "none"; });
2078
2251
  footer.appendChild(powered);
2079
2252
  footer.appendChild(brand);
2080
2253
  return footer;
@@ -2111,18 +2284,19 @@ export class SmartComplyFlow {
2111
2284
  });
2112
2285
  card.addEventListener("click", onClick);
2113
2286
  const textWrap = document.createElement("div");
2287
+ textWrap.style.cssText = "flex:1 1 auto;min-width:0;";
2114
2288
  const titleEl = document.createElement("div");
2115
- titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;`;
2289
+ titleEl.style.cssText = `color:${this.theme.text};font-size:14px;font-weight:600;line-height:1.35;`;
2116
2290
  titleEl.textContent = title;
2117
2291
  textWrap.appendChild(titleEl);
2118
2292
  if (subtitle) {
2119
2293
  const subEl = document.createElement("div");
2120
- subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;margin-top:2px;`;
2294
+ subEl.style.cssText = `color:${this.theme.textMuted};font-size:12px;line-height:1.4;margin-top:2px;`;
2121
2295
  subEl.textContent = subtitle;
2122
2296
  textWrap.appendChild(subEl);
2123
2297
  }
2124
2298
  const arrow = document.createElement("span");
2125
- arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;`;
2299
+ arrow.style.cssText = `color:${this.theme.textMuted};font-size:18px;flex-shrink:0;margin-left:12px;line-height:1;`;
2126
2300
  arrow.textContent = "→";
2127
2301
  card.appendChild(textWrap);
2128
2302
  card.appendChild(arrow);
@@ -2195,6 +2369,24 @@ export class SmartComplyFlow {
2195
2369
  .flatMap((ch) => ch.fields || []);
2196
2370
  return allFields.some((f) => isUploadFieldType(f.type));
2197
2371
  }
2372
+ /**
2373
+ * The "go back one step" target for a given step, if that step is
2374
+ * navigable — shared by the header back button and the browser
2375
+ * back-button (popstate) handler in mount() so both agree on the same
2376
+ * step order.
2377
+ */
2378
+ _getBackAction(step) {
2379
+ const countries = Object.keys(this.sdkConfig?.channels || {});
2380
+ const backTargets = {
2381
+ id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
2382
+ country: () => this.showStep("welcome"),
2383
+ id_input: () => this.showStep("id_type"),
2384
+ confirm_identity: () => this.showStep("id_input"),
2385
+ document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
2386
+ document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
2387
+ };
2388
+ return backTargets[step];
2389
+ }
2198
2390
  updateProgress() {
2199
2391
  const isDocFlow = this.isDocumentFlow();
2200
2392
  const countries = Object.keys(this.sdkConfig?.channels || {});
@@ -2246,15 +2438,7 @@ export class SmartComplyFlow {
2246
2438
  }
2247
2439
  // Back button — show for navigable steps only
2248
2440
  if (this.headerBackBtn) {
2249
- const backTargets = {
2250
- id_type: () => this.showStep(countries.length > 1 ? "country" : "welcome"),
2251
- country: () => this.showStep("welcome"),
2252
- id_input: () => this.showStep("id_type"),
2253
- confirm_identity: () => this.showStep("id_input"),
2254
- document_capture: () => { this.docCapture?.destroy(); this.showStep("id_type"); },
2255
- document_confirm: () => { this.docCapture?.destroy(); this.showStep("document_capture"); },
2256
- };
2257
- const backAction = backTargets[this.currentStep];
2441
+ const backAction = this._getBackAction(this.currentStep);
2258
2442
  if (backAction) {
2259
2443
  this.headerBackBtn.style.display = "flex";
2260
2444
  const newBtn = this.headerBackBtn.cloneNode(true);