@eshal-bot/chat-widget 0.2.38 → 0.2.40

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.
@@ -88751,6 +88751,29 @@
88751
88751
  function fill(text, values) {
88752
88752
  return Object.entries(values).reduce((s, [k, v]) => s.replace(`{${k}}`, String(v)), String(text));
88753
88753
  }
88754
+
88755
+ /** EV-4941: keep a masked address on one line ("+91 ••••• 6138"). */
88756
+ const noBreak = text => String(text ?? '').replace(/ /g, '\u00a0');
88757
+
88758
+ /**
88759
+ * EV-4939: a number whose calling code is not among the allowed ones, so the
88760
+ * card can say so before Send. Only numbers typed with +; a partial code that
88761
+ * could still become an allowed one is not flagged.
88762
+ */
88763
+ function blockedByCountry(address, codes) {
88764
+ if (!Array.isArray(codes) || codes.length === 0 || !String(address).trim().startsWith('+')) return false;
88765
+ const digits = String(address).replace(/\D/g, '');
88766
+ if (!digits) return false;
88767
+ return !codes.some(c => digits.startsWith(c) || c.startsWith(digits));
88768
+ }
88769
+
88770
+ /** "14:59" for a lockout's time left. */
88771
+ function clock(seconds) {
88772
+ const s = Math.max(0, Math.ceil(seconds));
88773
+ // "4:59:59" past an hour (a daily limit).
88774
+ const mmss = `${String(Math.floor(s % 3600 / 60)).padStart(s >= 3600 ? 2 : 1, '0')}:${String(s % 60).padStart(2, '0')}`;
88775
+ return s >= 3600 ? `${Math.floor(s / 3600)}:${mmss}` : mmss;
88776
+ }
88754
88777
  function boxStyle(isDark) {
88755
88778
  return {
88756
88779
  border: `1px solid ${BRAND_BLUE$1}`,
@@ -88824,6 +88847,13 @@
88824
88847
  const [error, setError] = React.useState(null);
88825
88848
  const [resendIn, setResendIn] = React.useState(0);
88826
88849
  const [cancelled, setCancelled] = React.useState(false);
88850
+ // EV-4939: a lockout's end from the server, and the allowed calling codes.
88851
+ const [lockUntil, setLockUntil] = React.useState(null);
88852
+ const [lockIn, setLockIn] = React.useState(0);
88853
+ // EV-4939 item 8: the number a per-number limit holds; null holds every
88854
+ // number (a lockout is for the conversation).
88855
+ const [lockFor, setLockFor] = React.useState(null);
88856
+ const [callingCodes, setCallingCodes] = React.useState(null);
88827
88857
  const resumedRef = React.useRef(false);
88828
88858
  const pendingKey = React.useRef(Symbol('verify-card'));
88829
88859
  // A card the conversation already moved past (a later visitor turn exists,
@@ -88871,6 +88901,31 @@
88871
88901
  clearInterval(id);
88872
88902
  };
88873
88903
  }, [mode, step, ctx, t]);
88904
+ React.useEffect(() => {
88905
+ if (mode !== 'sms' || !ctx?.config) return undefined;
88906
+ let gone = false;
88907
+ ctx.config().then(res => {
88908
+ const codes = res?.body?.sms_calling_codes;
88909
+ if (!gone && Array.isArray(codes)) setCallingCodes(codes.map(String));
88910
+ }).catch(() => undefined);
88911
+ return () => {
88912
+ gone = true;
88913
+ };
88914
+ }, [mode, ctx]);
88915
+ React.useEffect(() => {
88916
+ if (!lockUntil) return undefined;
88917
+ const tick = () => {
88918
+ const left = (lockUntil - Date.now()) / 1000;
88919
+ if (left <= 0) {
88920
+ setLockUntil(null);
88921
+ setLockIn(0);
88922
+ setError(null);
88923
+ } else setLockIn(left);
88924
+ };
88925
+ tick();
88926
+ const id = setInterval(tick, 1000);
88927
+ return () => clearInterval(id);
88928
+ }, [lockUntil]);
88874
88929
  React.useEffect(() => {
88875
88930
  if (resendIn <= 0) return undefined;
88876
88931
  const id = setTimeout(() => setResendIn(s => s - 1), 1000);
@@ -88908,12 +88963,22 @@
88908
88963
  });
88909
88964
  if (res.ok) {
88910
88965
  setMasked(res.body?.masked || address.trim());
88966
+ setLockUntil(null);
88967
+ setLockFor(null);
88968
+ // EV-4941: if the card goes back to the number step (lockout, expiry,
88969
+ // a new number), the field reads "+91 90749 76138", not "+919074976138".
88970
+ if (res.body?.formatted) setAddress(String(res.body.formatted));
88911
88971
  setResendIn(Number(res.body?.resend_after_s) || 60);
88912
88972
  setCode('');
88913
88973
  setStep('code');
88914
88974
  } else {
88915
88975
  setError(res.body?.error || t('verifyGenericError'));
88916
88976
  if (res.body?.resendAfterS) setResendIn(Number(res.body.resendAfterS));
88977
+ if ((res.body?.code === 'locked' || res.body?.code === 'too_many') && res.body?.lockedUntil) {
88978
+ setLockUntil(Date.parse(res.body.lockedUntil));
88979
+ setLockFor(res.body.code === 'too_many' ? address.trim() : null);
88980
+ setResendIn(0);
88981
+ }
88917
88982
  }
88918
88983
  } catch {
88919
88984
  setError(t('verifyGenericError'));
@@ -88948,13 +89013,19 @@
88948
89013
  });
88949
89014
  }
88950
89015
  } else if (res.body?.code === 'wrong_code' && typeof res.body?.triesLeft === 'number') {
88951
- setError(fill(t('verifyTriesLeft'), {
89016
+ setError(fill(t(res.body.triesLeft === 1 ? 'verifyTryLeft' : 'verifyTriesLeft'), {
88952
89017
  count: res.body.triesLeft
88953
89018
  }));
88954
89019
  setCode('');
88955
89020
  } else {
88956
89021
  setError(res.body?.error || t('verifyGenericError'));
88957
89022
  if (res.body?.code === 'expired' || res.body?.code === 'locked') setStep('address');
89023
+ // EV-4939: a lockout holds Send for its whole length, as a countdown.
89024
+ if (res.body?.code === 'locked' && res.body?.lockedUntil) {
89025
+ setLockUntil(Date.parse(res.body.lockedUntil));
89026
+ setLockFor(null);
89027
+ setResendIn(0);
89028
+ }
88958
89029
  }
88959
89030
  } catch {
88960
89031
  setError(t('verifyGenericError'));
@@ -88972,11 +89043,22 @@
88972
89043
  fontWeight: 600
88973
89044
  },
88974
89045
  children: ["\u2713 ", fill(t('verifyDone'), {
88975
- target: masked
89046
+ target: noBreak(masked)
88976
89047
  })]
88977
89048
  })
88978
89049
  });
88979
89050
  }
89051
+ const countryBlocked = mode === 'sms' && blockedByCountry(address, callingCodes);
89052
+ const errorLine = text => text ? /*#__PURE__*/jsxRuntimeExports.jsx("div", {
89053
+ role: "alert",
89054
+ style: {
89055
+ color: '#b42318',
89056
+ fontSize: 13
89057
+ },
89058
+ children: text
89059
+ }) : null;
89060
+ const held = lockIn > 0 && (lockFor === null || lockFor === address.trim());
89061
+ const sendDisabled = busy || !address.trim() || resendIn > 0 || held || countryBlocked;
88980
89062
  return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
88981
89063
  style: boxStyle(isDark),
88982
89064
  "data-testid": "verify-card",
@@ -89006,28 +89088,30 @@
89006
89088
  value: address,
89007
89089
  onChange: e => setAddress(e.target.value),
89008
89090
  onKeyDown: e => {
89009
- if (e.key === 'Enter') send();
89091
+ if (e.key === 'Enter' && !sendDisabled) send();
89010
89092
  }
89011
89093
  })]
89012
- }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89094
+ }), errorLine(countryBlocked ? t('verifyCountryNotAllowed') : error), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89013
89095
  type: "button",
89014
- style: buttonStyle(busy || !address.trim() || resendIn > 0),
89015
- disabled: busy || !address.trim() || resendIn > 0,
89096
+ style: buttonStyle(sendDisabled),
89097
+ disabled: sendDisabled,
89016
89098
  onClick: send,
89017
- children: busy ? t('verifySending') : resendIn > 0 ? fill(t('verifyResendIn'), {
89099
+ children: busy ? t('verifySending') : held ? fill(t('verifyLockedFor'), {
89100
+ time: clock(lockIn)
89101
+ }) : resendIn > 0 ? fill(t('verifyResendIn'), {
89018
89102
  seconds: resendIn
89019
89103
  }) : t('verifySendCode')
89020
89104
  })]
89021
89105
  }) : /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
89022
89106
  children: [/*#__PURE__*/jsxRuntimeExports.jsx("span", {
89023
89107
  children: fill(t('verifyEnterCode'), {
89024
- target: masked
89108
+ target: noBreak(masked)
89025
89109
  })
89026
89110
  }), /*#__PURE__*/jsxRuntimeExports.jsx("input", {
89111
+ // EV-4941: the widget's own font; spacing keeps the digits apart.
89027
89112
  style: {
89028
89113
  ...inputStyle(isDark),
89029
89114
  letterSpacing: '0.3em',
89030
- fontFamily: 'monospace',
89031
89115
  fontSize: 18
89032
89116
  },
89033
89117
  "aria-label": t('verifyCodeLabel'),
@@ -89039,7 +89123,7 @@
89039
89123
  onKeyDown: e => {
89040
89124
  if (e.key === 'Enter') confirm();
89041
89125
  }
89042
- }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89126
+ }), errorLine(error), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89043
89127
  type: "button",
89044
89128
  style: buttonStyle(busy || code.length !== 6),
89045
89129
  disabled: busy || code.length !== 6,
@@ -89065,23 +89149,22 @@
89065
89149
  onClick: () => {
89066
89150
  setStep('address');
89067
89151
  setError(null);
89152
+ // EV-4939: the wait was for the old address; a new one starts fresh.
89153
+ setResendIn(0);
89068
89154
  },
89069
- children: t('verifyDifferent')
89155
+ children: mode === 'sms' ? t('verifyDifferentNumber') : t('verifyDifferent')
89070
89156
  })]
89071
- }), error && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
89072
- role: "alert",
89073
- style: {
89074
- color: '#b42318',
89075
- fontSize: 12
89076
- },
89077
- children: error
89078
89157
  }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89079
89158
  type: "button",
89080
89159
  style: {
89081
89160
  ...linkStyle,
89082
89161
  color: isDark ? '#9ca3af' : '#6b7280'
89083
89162
  },
89084
- onClick: () => setCancelled(true),
89163
+ onClick: () => {
89164
+ // EV-4942: tell the server, so the agent sees the cancel.
89165
+ if (ctx.cancel) Promise.resolve(ctx.cancel()).catch(() => undefined);
89166
+ setCancelled(true);
89167
+ },
89085
89168
  disabled: busy,
89086
89169
  children: t('verifyCancel')
89087
89170
  })]