@axos-web-dev/shared-components 2.2.29 → 2.2.31

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.
@@ -73,8 +73,8 @@ import "../Carousel/index.js";
73
73
  /* empty css */
74
74
  import "../Chatbot/store/chat.js";
75
75
  import "../Chatbot/Chatbot.js";
76
- import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
77
76
  import "../LoadingIndicator/LoadingIndicator.css.js";
77
+ import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
78
78
  /* empty css */
79
79
  import "../Chatbot/store/messages.js";
80
80
  import { Hyperlink } from "../Hyperlink/index.js";
@@ -54,9 +54,9 @@ import "../Carousel/index.js";
54
54
  /* empty css */
55
55
  import "../Chatbot/store/chat.js";
56
56
  import "../Chatbot/Chatbot.js";
57
- import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
58
57
  import "../LoadingIndicator/LoadingIndicator.css.js";
59
58
  import "react-use";
59
+ import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
60
60
  /* empty css */
61
61
  import "../Chatbot/store/messages.js";
62
62
  /* empty css */
@@ -3,6 +3,7 @@ import '../assets/Chatbot/ChatWindow.css';import '../assets/Typography/Typograph
3
3
  /* empty css */
4
4
  /* empty css */
5
5
  /* empty css */
6
+ import "./Chatbot.css.ts.vanilla.css.js";
6
7
  /* empty css */
7
8
  var windowStyle = "_13n1jqk1";
8
9
  var windowExpandedStyle = "_13n1jqk2";
@@ -3,6 +3,7 @@ import { default as React } from 'react';
3
3
 
4
4
  interface MessageResponse extends BaseMessageResponse {
5
5
  to_agent?: HumanAgent;
6
+ pending?: boolean;
6
7
  }
7
8
  export interface ChatBubble {
8
9
  id: string | number;
@@ -18,7 +18,6 @@ import { useOpenChat } from "./store/chat.js";
18
18
  import { ThankYouMessage } from "./ThankYouMessage.js";
19
19
  const ChatWindow = ({
20
20
  messages,
21
- status = "connected",
22
21
  onSend,
23
22
  inputDisabled = false,
24
23
  onClose,
@@ -43,7 +42,6 @@ const ChatWindow = ({
43
42
  showThankyouMessage,
44
43
  toggleThankyouMessage,
45
44
  hasEscalated,
46
- isBlockedInput,
47
45
  isOpen,
48
46
  hasOpenedOnce,
49
47
  chatStatus,
@@ -65,7 +63,6 @@ const ChatWindow = ({
65
63
  const handleSend = (e) => {
66
64
  e.preventDefault();
67
65
  const cleaned = cleanInput(input);
68
- if (isBlockedInput) return;
69
66
  if (cleaned != "") {
70
67
  onSend(cleaned);
71
68
  setInput("");
@@ -460,7 +457,7 @@ const ChatWindow = ({
460
457
  }
461
458
  ) }) : null,
462
459
  /* @__PURE__ */ jsxs("div", { className: clsx(messagesContainerStyle), children: [
463
- (status === "idle" && messages.length == 0 || messages.length == 0) && /* @__PURE__ */ jsx(
460
+ !messages.some((msg) => !msg.pending) && /* @__PURE__ */ jsx(
464
461
  "div",
465
462
  {
466
463
  className: clsx(),
@@ -525,9 +522,7 @@ const ChatWindow = ({
525
522
  /* @__PURE__ */ jsx(
526
523
  "div",
527
524
  {
528
- className: clsx(
529
- focused && !inputDisabled && !isBlockedInput ? focusAnimation : ""
530
- ),
525
+ className: clsx(focused && !inputDisabled ? focusAnimation : ""),
531
526
  style: {
532
527
  position: "absolute",
533
528
  left: "10%",
@@ -544,7 +539,7 @@ const ChatWindow = ({
544
539
  style: {
545
540
  display: "flex",
546
541
  padding: "12px 16px",
547
- background: isBlockedInput || inputDisabled || status !== "connected" || messages.length == 0 || escalationDeflected ? "light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3))" : "#ffffff",
542
+ background: inputDisabled || escalationDeflected ? "light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3))" : "#ffffff",
548
543
  borderRadius: 12
549
544
  },
550
545
  children: [
@@ -568,7 +563,7 @@ const ChatWindow = ({
568
563
  "aria-label": "Ask anything...",
569
564
  className: clsx(inputStyle, autoResize),
570
565
  autoFocus: true,
571
- disabled: isBlockedInput || inputDisabled || status !== "connected" || messages.length == 0 || escalationDeflected,
566
+ disabled: inputDisabled || escalationDeflected,
572
567
  rows: 1
573
568
  }
574
569
  ),
@@ -582,7 +577,7 @@ const ChatWindow = ({
582
577
  type: "submit",
583
578
  title: "Send message",
584
579
  "aria-label": "Send message",
585
- disabled: isBlockedInput || inputDisabled || status !== "connected" || !input.trim() || messages.length == 0 || escalationDeflected,
580
+ disabled: inputDisabled || !input.trim() || escalationDeflected,
586
581
  children: /* @__PURE__ */ jsx(
587
582
  "svg",
588
583
  {
@@ -1,4 +1,3 @@
1
- import "./Chatbot.css.ts.vanilla.css.js";
2
1
  var chatbotAXB = "_1hpv6vm0";
3
2
  var chatbotUFB = "_1hpv6vm1";
4
3
  export {
@@ -40,17 +40,34 @@ const Chatbot = ({
40
40
  hideDisplayThankyouMessage
41
41
  } = useOpenChat();
42
42
  const isVisible = usePageVisibility();
43
- const { addMessage, addMessages, clearMessages, messages } = useMessages();
43
+ const {
44
+ addMessage,
45
+ addMessages,
46
+ clearMessages,
47
+ messages,
48
+ addPendingMessage,
49
+ resolvePendingMessage
50
+ } = useMessages();
44
51
  const clientRef = useRef(null);
45
52
  const menuRef = useRef(null);
46
53
  const isMountedRef = useRef(false);
47
54
  const chatRef = useRef(null);
48
55
  const chatLoading = useRef(false);
49
56
  const agent_virtual = useRef(null);
57
+ const messageQueueRef = useRef([]);
58
+ const sendInFlightRef = useRef(false);
59
+ const flushingRef = useRef(false);
60
+ const statusRef = useRef("idle");
50
61
  const [status, setStatus] = useState("idle");
51
62
  const [menusLoaded, setMenusLoaded] = useState(false);
52
63
  const [isTyping, setIsTyping] = useState(false);
53
64
  const [scalationStarted, setScalationStarted] = useState(false);
65
+ useEffect(() => {
66
+ statusRef.current = status;
67
+ if (status === "connected") {
68
+ flushQueuedMessages();
69
+ }
70
+ }, [status]);
54
71
  useEffect(() => {
55
72
  if (messages.length === 0) return;
56
73
  const hasScalation = messages.some(
@@ -67,6 +84,9 @@ const Chatbot = ({
67
84
  ["axos", 1],
68
85
  ["ufb", 3]
69
86
  ]);
87
+ const hasRealReply = () => useMessages.getState().messages.some(
88
+ (m) => !m.pending && m.$userType !== "end_user" && m.type !== "noti" && m.$sid !== "typing-1"
89
+ );
70
90
  const typingMessage = {
71
91
  $sid: "typing-1",
72
92
  type: "system",
@@ -107,7 +127,7 @@ const Chatbot = ({
107
127
  menuRef.current = await clientRef.current.getMenus();
108
128
  setMenusLoaded(true);
109
129
  console.log("menus:", menuRef.current);
110
- if (menuRef.current !== null) {
130
+ if (menuRef.current !== null && useOpenChat.getState().isOpen) {
111
131
  await startChat("onReady");
112
132
  }
113
133
  }
@@ -121,10 +141,11 @@ const Chatbot = ({
121
141
  const onChatMessageHandler = async (message) => {
122
142
  console.log("Received message:", message);
123
143
  const { event, $userType } = message;
124
- if (["system", "virtual_agent", "user"].includes($userType) && event === void 0) {
144
+ if (["virtual_agent", "user"].includes($userType) && event === void 0) {
125
145
  addMessage(message);
126
146
  if (!hasEscalated) {
127
147
  unblockInput?.();
148
+ flushQueuedMessages();
128
149
  }
129
150
  return;
130
151
  }
@@ -205,6 +226,7 @@ const Chatbot = ({
205
226
  startEscalation?.();
206
227
  }
207
228
  }
229
+ flushQueuedMessages();
208
230
  } catch (error) {
209
231
  console.error("Error fetching messages on chat connected:", error);
210
232
  }
@@ -226,7 +248,7 @@ const Chatbot = ({
226
248
  const deregisterEventHandlers = () => {
227
249
  clientRef.current?.off("ready", onReadyHandler);
228
250
  clientRef.current?.off("authenticated", onAuthenticatedHandler);
229
- clientRef.current?.off("chat.ongoing", onDismissedHandler);
251
+ clientRef.current?.off("chat.ongoing", onChatOngoingHandler);
230
252
  clientRef.current?.off("chat.message", onChatMessageHandler);
231
253
  clientRef.current?.off("chat.typingStarted", onChatTypingStartedHandler);
232
254
  clientRef.current?.off("chat.typingEnded", onChatTypingEndedHandler);
@@ -340,6 +362,41 @@ const Chatbot = ({
340
362
  setChatStarted();
341
363
  }
342
364
  };
365
+ const sendNow = async (msg) => {
366
+ sendInFlightRef.current = true;
367
+ let success = true;
368
+ try {
369
+ await clientRef.current?.sendTextMessage(msg);
370
+ } catch (error) {
371
+ console.log(error);
372
+ success = false;
373
+ } finally {
374
+ sendInFlightRef.current = false;
375
+ if (!hasEscalated) {
376
+ blockInput?.();
377
+ }
378
+ }
379
+ return success;
380
+ };
381
+ const flushQueuedMessages = async () => {
382
+ if (flushingRef.current) return;
383
+ if (messageQueueRef.current.length === 0) return;
384
+ if (!clientRef.current || statusRef.current !== "connected") return;
385
+ if (!hasRealReply()) return;
386
+ flushingRef.current = true;
387
+ try {
388
+ while (messageQueueRef.current.length > 0 && !useOpenChat.getState().isBlockedInput && !sendInFlightRef.current) {
389
+ const next = messageQueueRef.current[0];
390
+ const success = await sendNow(next.text);
391
+ messageQueueRef.current.shift();
392
+ if (!success) {
393
+ resolvePendingMessage(next.id);
394
+ }
395
+ }
396
+ } finally {
397
+ flushingRef.current = false;
398
+ }
399
+ };
343
400
  const onSendMessage = async (msg) => {
344
401
  const clientChatId = clientRef.current?.chat?.id;
345
402
  const refChatId = chatRef.current?.id;
@@ -351,15 +408,16 @@ const Chatbot = ({
351
408
  "| chatRef.id:",
352
409
  refChatId
353
410
  );
354
- try {
355
- await clientRef.current?.sendTextMessage(msg);
356
- } catch (error) {
357
- console.log(error);
358
- } finally {
359
- if (!hasEscalated) {
360
- blockInput?.();
361
- }
411
+ const notReady = !clientRef.current || statusRef.current !== "connected";
412
+ const isThinking = useOpenChat.getState().isBlockedInput;
413
+ const noReplyYet = !hasRealReply();
414
+ if (notReady || isThinking || sendInFlightRef.current || noReplyYet) {
415
+ const id = `pending-${Date.now()}-${Math.random().toString(36).slice(2)}`;
416
+ messageQueueRef.current.push({ id, text: msg });
417
+ addPendingMessage(id, msg);
418
+ return;
362
419
  }
420
+ await sendNow(msg);
363
421
  };
364
422
  const onEndChat = async () => {
365
423
  console.log(`Ending chat [end chat]`);
@@ -373,6 +431,9 @@ const Chatbot = ({
373
431
  clearMessages();
374
432
  chatRef.current = null;
375
433
  resetChatStarted();
434
+ messageQueueRef.current = [];
435
+ sendInFlightRef.current = false;
436
+ flushingRef.current = false;
376
437
  console.log("Chat ended");
377
438
  endEscalation?.();
378
439
  } finally {
@@ -399,6 +460,8 @@ const Chatbot = ({
399
460
  console.log("Chatbot unmounted");
400
461
  deregisterEventHandlers();
401
462
  chatLoading.current = false;
463
+ messageQueueRef.current = [];
464
+ sendInFlightRef.current = false;
402
465
  reset();
403
466
  resetChatStarted();
404
467
  setStatus("idle");
@@ -443,36 +506,6 @@ const Chatbot = ({
443
506
  }
444
507
  }
445
508
  }, [isOpen, isVisible]);
446
- useMount(() => {
447
- const hasParams = () => {
448
- return window.location.search.includes("utm_");
449
- };
450
- const tryStart = () => {
451
- if (hasParams()) {
452
- startChat("utm-ready");
453
- return true;
454
- }
455
- return false;
456
- };
457
- if (tryStart()) return;
458
- const original = history.replaceState;
459
- history.replaceState = function(...args) {
460
- original.apply(this, args);
461
- if (tryStart()) {
462
- history.replaceState = original;
463
- }
464
- };
465
- const interval = setInterval(() => {
466
- if (tryStart()) {
467
- clearInterval(interval);
468
- history.replaceState = original;
469
- }
470
- }, 100);
471
- return () => {
472
- clearInterval(interval);
473
- history.replaceState = original;
474
- };
475
- });
476
509
  return menusLoaded && /* @__PURE__ */ jsxs(
477
510
  "div",
478
511
  {
@@ -3,6 +3,7 @@ import { MessageResponse as BaseMessageResponse, HumanAgent, VirtualAgent } from
3
3
 
4
4
  interface MessageResponse extends BaseMessageResponse {
5
5
  to_agent?: HumanAgent;
6
+ pending?: boolean;
6
7
  }
7
8
  interface ChatbotMessageProps {
8
9
  msg: MessageResponse;
@@ -108,6 +108,7 @@ const ChatbotMessage = ({
108
108
  messageStyle,
109
109
  msg.$userType == "end_user" ? user_msg : agent_msg
110
110
  ),
111
+ style: msg.pending ? { opacity: 0.65 } : void 0,
111
112
  children: /* @__PURE__ */ jsxs(
112
113
  "div",
113
114
  {
@@ -253,7 +254,7 @@ const ChatbotMessage = ({
253
254
  textTransform: "capitalize",
254
255
  fontFamily: "inherit"
255
256
  },
256
- children: timeText
257
+ children: msg.pending ? "Sending..." : timeText
257
258
  }
258
259
  )
259
260
  ]
@@ -1,12 +1,17 @@
1
1
  import { MessageResponse } from '@ujet/websdk-headless';
2
2
 
3
+ export type PendingMessage = MessageResponse & {
4
+ pending?: boolean;
5
+ };
3
6
  interface MessageStore {
4
- messages: Array<MessageResponse>;
7
+ messages: Array<PendingMessage>;
5
8
  addMessage: (message: MessageResponse) => void;
6
9
  removeMessage: (id: string) => void;
7
10
  addMessages: (newMessages: MessageResponse[]) => void;
8
11
  clearMessages: () => void;
9
12
  isEscalated: boolean;
13
+ addPendingMessage: (id: string, text: string) => void;
14
+ resolvePendingMessage: (id: string) => void;
10
15
  }
11
16
  export interface Message {
12
17
  id: string;
@@ -3,18 +3,77 @@ const useMessages = create((set, get) => ({
3
3
  messages: [],
4
4
  addMessage: (message) => set((state) => {
5
5
  const cleaned = state.messages.filter((m) => m.$sid !== "typing-1");
6
- return { messages: [...cleaned, message] };
6
+ const isPending = message.pending;
7
+ if (!isPending && message.$userType === "end_user") {
8
+ const pendingIdx = cleaned.findIndex(
9
+ (m) => m.pending && m.content === message.content
10
+ );
11
+ if (pendingIdx !== -1) {
12
+ return {
13
+ messages: [
14
+ ...cleaned.slice(0, pendingIdx),
15
+ message,
16
+ ...cleaned.slice(pendingIdx + 1)
17
+ ]
18
+ };
19
+ }
20
+ return { messages: [...cleaned, message] };
21
+ }
22
+ const insertBeforeIdx = !isPending ? cleaned.findIndex((m) => m.pending) : -1;
23
+ if (insertBeforeIdx === -1) {
24
+ return { messages: [...cleaned, message] };
25
+ }
26
+ return {
27
+ messages: [
28
+ ...cleaned.slice(0, insertBeforeIdx),
29
+ message,
30
+ ...cleaned.slice(insertBeforeIdx)
31
+ ]
32
+ };
7
33
  }),
8
34
  removeMessage: (id) => set((state) => ({
9
35
  messages: state.messages.filter(
10
36
  (msg) => "$sid" in msg && msg.$sid !== id
11
37
  )
12
38
  })),
13
- addMessages: (newMessages) => set((state) => ({ messages: [...state.messages, ...newMessages] })),
39
+ addMessages: (newMessages) => set((state) => {
40
+ const pendingIdx = state.messages.findIndex(
41
+ (m) => m.pending
42
+ );
43
+ if (pendingIdx === -1) {
44
+ return { messages: [...state.messages, ...newMessages] };
45
+ }
46
+ return {
47
+ messages: [
48
+ ...state.messages.slice(0, pendingIdx),
49
+ ...newMessages,
50
+ ...state.messages.slice(pendingIdx)
51
+ ]
52
+ };
53
+ }),
14
54
  clearMessages: () => set({ messages: [] }),
15
55
  isEscalated: get()?.messages?.some(
16
56
  (msg) => ["escalationAccepted", "escalationStarted"].includes(msg.event)
17
- )
57
+ ),
58
+ addPendingMessage: (id, text) => set((state) => ({
59
+ messages: [
60
+ ...state.messages,
61
+ {
62
+ $sid: id,
63
+ type: "text",
64
+ content: text,
65
+ sender: { id: "end_user", type: "end_user" },
66
+ $timestamp: /* @__PURE__ */ new Date(),
67
+ $userType: "end_user",
68
+ $index: Date.now(),
69
+ $userId: 0,
70
+ pending: true
71
+ }
72
+ ]
73
+ })),
74
+ resolvePendingMessage: (id) => set((state) => ({
75
+ messages: state.messages.filter((msg) => msg.$sid !== id)
76
+ }))
18
77
  }));
19
78
  export {
20
79
  useMessages
@@ -22,7 +22,7 @@ import clsx from "clsx";
22
22
  import "../../Accordion/Accordion.js";
23
23
  import "../../Accordion/Accordion.css.js";
24
24
  import "../../AlertBanner/AlertBanner.css.js";
25
- import { useState, useEffect } from "react";
25
+ import { useState, useRef, useEffect, useLayoutEffect } from "react";
26
26
  import "../../Article/Article.css.js";
27
27
  import "../../ArticlesSet/ArticlesSet.css.js";
28
28
  import "../../Calculators/calculator.css.js";
@@ -134,6 +134,7 @@ import "../../StepItemSet/StepItemSet.css.js";
134
134
  /* empty css */
135
135
  /* empty css */
136
136
  /* empty css */
137
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
137
138
  const MortgageRate = ({
138
139
  icon = false,
139
140
  children,
@@ -159,6 +160,8 @@ const MortgageRate = ({
159
160
  const [_downPayment, setDownPayment] = useState(0);
160
161
  const [isTypingDownPayment, setIsTypingDownPayment] = useState(false);
161
162
  const [salesPriceVal, setSalesPriceVal] = useState(0);
163
+ const containerRef = useRef(null);
164
+ const restoreScrollOnNextRender = useRef(false);
162
165
  const cachedEmailValidator = useCachedEmailValidator(validateEmail);
163
166
  const schema = z.object({
164
167
  firstName: z.string({ message: "First Name is required." }).regex(/^[A-Za-z][^0-9_!¡?÷?¿/\\+=@#$%ˆ&*,.^(){}|~<>;:[\]]{1,}$/g, {
@@ -295,6 +298,17 @@ const MortgageRate = ({
295
298
  methods.setValue("downPayment", calculatedDownPayment);
296
299
  }
297
300
  }, [salesPrice, downPaymentPercentage]);
301
+ useIsomorphicLayoutEffect(() => {
302
+ if (!restoreScrollOnNextRender.current) return;
303
+ restoreScrollOnNextRender.current = false;
304
+ const anchorViewport = () => {
305
+ const target = showingTables && containerRef.current?.nextElementSibling || containerRef.current;
306
+ target?.scrollIntoView({ block: "start" });
307
+ };
308
+ anchorViewport();
309
+ const raf = requestAnimationFrame(anchorViewport);
310
+ return () => cancelAnimationFrame(raf);
311
+ }, [hideModal, showingTables]);
298
312
  const submitForm = async (data) => {
299
313
  const processData = {
300
314
  ...data
@@ -303,6 +317,7 @@ const MortgageRate = ({
303
317
  if (setRateWatchSubmitted) {
304
318
  setRateWatchSubmitted(false);
305
319
  }
320
+ restoreScrollOnNextRender.current = true;
306
321
  setHideModal(true);
307
322
  };
308
323
  const variant = getVariant(fullVariant);
@@ -366,7 +381,7 @@ const MortgageRate = ({
366
381
  }
367
382
  return value;
368
383
  };
369
- return /* @__PURE__ */ jsxs(Fragment, { children: [
384
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, children: [
370
385
  hideModal && !showingTables && /* @__PURE__ */ jsx(
371
386
  "section",
372
387
  {
@@ -442,9 +457,9 @@ const MortgageRate = ({
442
457
  {
443
458
  className: form,
444
459
  onSubmit: async (e) => {
460
+ e.preventDefault();
445
461
  onValidate && onValidate(e);
446
462
  await handleSubmit(submitForm)(e);
447
- e.preventDefault();
448
463
  },
449
464
  children: [
450
465
  /* @__PURE__ */ jsxs("div", { className: clsx(formWrapper({ variant })), children: [
@@ -794,12 +809,15 @@ const MortgageRate = ({
794
809
  {
795
810
  id,
796
811
  className: `${chevron({ variant })} ${!isValid ? na_cursor : ""}`,
797
- onClick: isValid ? handleSubmit(async (data) => {
798
- await onSubmit(data);
799
- toggleModal();
800
- setHideModal(false);
801
- }) : (e) => {
812
+ onClick: (e) => {
802
813
  e.preventDefault();
814
+ if (!isValid) return;
815
+ handleSubmit(async (data) => {
816
+ await onSubmit(data);
817
+ restoreScrollOnNextRender.current = true;
818
+ toggleModal();
819
+ setHideModal(false);
820
+ })(e);
803
821
  },
804
822
  children: "Sign Up For Ratewatch"
805
823
  }
@@ -36,7 +36,7 @@ const MortgageRateFilters = ({
36
36
  loanPurpose: z.number({ message: "Loan Purpose is required." }),
37
37
  propertyType: z.number({ message: "Property Type is required." }).positive({ message: "Property Type is required." }),
38
38
  occupancy: z.number({ message: "Property Usage is required." }).positive({ message: "Property Usage is required." }),
39
- salesPrice: z.number({ message: "Purchase Price is required." }).refine((val) => loanPurpose === 1 ? val != null : true, {
39
+ salesPrice: z.number({ message: "Purchase Price is required." }).optional().refine((val) => loanPurpose === 1 ? val != null : true, {
40
40
  message: "Sales Price is required."
41
41
  }).refine(
42
42
  (val) => {
@@ -47,7 +47,7 @@ const MortgageRateFilters = ({
47
47
  },
48
48
  { message: "Value must be between $50,000 and $25,000,000." }
49
49
  ),
50
- downPayment: z.number({ message: "Down Payment is required." }).refine((val) => loanPurpose === 1 ? val != null : true, {
50
+ downPayment: z.number({ message: "Down Payment is required." }).optional().refine((val) => loanPurpose === 1 ? val != null : true, {
51
51
  message: "Down Payment is required."
52
52
  }).refine(
53
53
  (val) => {
@@ -61,8 +61,8 @@ const MortgageRateFilters = ({
61
61
  }
62
62
  ),
63
63
  creditScore: z.number({ message: "Credit Score is required." }).positive({ message: "Credit Score is required." }),
64
- zipCode: z.number({ message: "Zip Code is required." }),
65
- appraisedValue: z.number({ message: "Home Price is required." }).refine(
64
+ zipCode: z.number({ message: "Zip Code is required." }).int({ message: "Zip Code must be a valid 5-digit zip code." }).gte(1e4, { message: "Zip Code must be a valid 5-digit zip code." }).lte(99999, { message: "Zip Code must be a valid 5-digit zip code." }),
65
+ appraisedValue: z.number({ message: "Home Price is required." }).optional().refine(
66
66
  (val) => {
67
67
  if (loanPurpose === 2) {
68
68
  return val != null && val >= 5e4 && val <= 25e6;
@@ -71,7 +71,7 @@ const MortgageRateFilters = ({
71
71
  },
72
72
  { message: "Value must be between $50,000 and $25,000,000." }
73
73
  ),
74
- mortgageBalance: z.number({ message: "Mortgage Balance is required." }).refine(
74
+ mortgageBalance: z.number({ message: "Mortgage Balance is required." }).optional().refine(
75
75
  (val) => {
76
76
  if (loanPurpose === 2) {
77
77
  return val != null && val >= 5e4 && val <= 25e6;
@@ -80,7 +80,7 @@ const MortgageRateFilters = ({
80
80
  },
81
81
  { message: "Value must be between $50,000 and $25,000,000." }
82
82
  ),
83
- cashOut: z.number({ message: "Cash Out is required." }).refine(
83
+ cashOut: z.number({ message: "Cash Out is required." }).optional().refine(
84
84
  (val) => {
85
85
  if (loanPurpose === 2 && val == null) {
86
86
  return false;
@@ -92,7 +92,19 @@ const MortgageRateFilters = ({
92
92
  });
93
93
  const methods = useForm({
94
94
  resolver: zodResolver(schema),
95
- mode: "all"
95
+ mode: "all",
96
+ defaultValues: {
97
+ loanPurpose: formData.loanPurpose,
98
+ propertyType: formData.propertyType,
99
+ occupancy: formData.occupancy,
100
+ salesPrice: formData.salesPrice,
101
+ downPayment: formData.downPayment,
102
+ creditScore: formData.creditScore,
103
+ zipCode: formData.zipCode,
104
+ appraisedValue: formData.appraisedValue,
105
+ mortgageBalance: formData.mortgageBalance,
106
+ cashOut: formData.cashOut
107
+ }
96
108
  });
97
109
  const {
98
110
  handleSubmit,
@@ -102,10 +114,10 @@ const MortgageRateFilters = ({
102
114
  } = methods;
103
115
  const salesPrice = watch("salesPrice");
104
116
  useEffect(() => {
105
- setSalesPriceVal(salesPrice);
117
+ setSalesPriceVal(salesPrice ?? 0);
106
118
  });
107
119
  useEffect(() => {
108
- if (salesPrice > 0 && !isTypingDownPayment) {
120
+ if (salesPrice != null && salesPrice > 0 && !isTypingDownPayment) {
109
121
  const calculatedDownPayment = Math.round(
110
122
  salesPrice * downPaymentPercentage / 100
111
123
  );
@@ -115,7 +127,16 @@ const MortgageRateFilters = ({
115
127
  }, [salesPrice, downPaymentPercentage]);
116
128
  const submitForm = async (data) => {
117
129
  const processData = {
118
- ...data
130
+ loanPurpose: data.loanPurpose,
131
+ propertyType: data.propertyType,
132
+ occupancy: data.occupancy,
133
+ creditScore: data.creditScore,
134
+ zipCode: data.zipCode,
135
+ salesPrice: data.salesPrice ?? 0,
136
+ downPayment: data.downPayment ?? 0,
137
+ appraisedValue: data.appraisedValue ?? 0,
138
+ mortgageBalance: data.mortgageBalance ?? 0,
139
+ cashOut: data.cashOut ?? 0
119
140
  };
120
141
  await onSubmit(processData);
121
142
  };
@@ -391,7 +412,8 @@ const MortgageRateFilters = ({
391
412
  inputMode: "numeric",
392
413
  ...register("zipCode", {
393
414
  required: true,
394
- pattern: /^\d{5}$/
415
+ pattern: /^\d{5}$/,
416
+ setValueAs: (value) => Number(value)
395
417
  }),
396
418
  label: "Zip Code",
397
419
  sizes: "medium",
@@ -1,6 +1,6 @@
1
1
  export type RateWatchInputs = {
2
2
  updateType: string;
3
- updateCadence: string;
3
+ updateCadence?: string;
4
4
  loanTerms?: string;
5
5
  hasSmsConsent: boolean;
6
6
  tR1DesiredRate?: string;
@@ -136,12 +136,34 @@ const RateWatchForm = ({
136
136
  toggleModal,
137
137
  setRateWatchSubmitted
138
138
  }) => {
139
+ const desiredRateFields = [
140
+ "tR1DesiredRate",
141
+ "tR2DesiredRate",
142
+ "tR3DesiredRate"
143
+ ];
144
+ const sanitizeDesiredRate = (value) => {
145
+ if (!value) return "";
146
+ const normalizedValue = value.replace(/[^\d.]/g, "");
147
+ if (!normalizedValue || normalizedValue === ".") return "";
148
+ const [wholePart, decimalPart = ""] = normalizedValue.split(".");
149
+ const trimmedDecimal = decimalPart.slice(0, 4);
150
+ const formattedValue = trimmedDecimal ? `${wholePart || "0"}.${trimmedDecimal}` : wholePart || "0";
151
+ const numericValue = Number(formattedValue);
152
+ if (!Number.isFinite(numericValue)) return "";
153
+ return Number.isInteger(numericValue) ? String(numericValue) : numericValue.toFixed(4).replace(/\.0+$|(?<=\.\d*)(0+)$/, "").replace(/\.$/, "");
154
+ };
155
+ const isDesiredRateOutOfRange = (value) => {
156
+ if (!value) return false;
157
+ const numericValue = Number(value);
158
+ if (!Number.isFinite(numericValue)) return false;
159
+ return numericValue < 2 || numericValue > 10;
160
+ };
139
161
  const [updateType, setUpdateType] = useState(
140
162
  "Target Rate"
141
163
  );
142
164
  const schema = z.object({
143
165
  updateType: z.string().min(1, { message: "Update type is required." }),
144
- updateCadence: z.string().refine((val) => !(updateType === "Frequency" && !val), {
166
+ updateCadence: z.string().optional().refine((val) => !(updateType === "Frequency" && !val), {
145
167
  message: "Frequency is required."
146
168
  }),
147
169
  loanTerms: z.string().optional().refine((val) => !(updateType === "Frequency" && !val), {
@@ -150,11 +172,13 @@ const RateWatchForm = ({
150
172
  hasSmsConsent: z.boolean().refine((val) => val === true, {
151
173
  message: "You must agree to receive SMS messages."
152
174
  }),
153
- tR1DesiredRate: z.string().optional().refine(
175
+ tR1DesiredRate: z.string().optional().refine((val) => updateType === "Target Rate" ? !!val : true, {
176
+ message: "Desired Rate is required."
177
+ }).refine(
154
178
  (val) => {
155
179
  if (val && updateType === "Target Rate") {
156
180
  const numericValue = +val;
157
- return !val || numericValue >= 2 && numericValue <= 10;
181
+ return numericValue >= 2 && numericValue <= 10;
158
182
  }
159
183
  return true;
160
184
  },
@@ -193,15 +217,21 @@ const RateWatchForm = ({
193
217
  email: z.string()
194
218
  });
195
219
  const methods = useForm({
196
- resolver: zodResolver(schema)
220
+ resolver: zodResolver(schema),
221
+ mode: "onChange"
197
222
  });
198
223
  const {
199
224
  handleSubmit,
200
225
  register,
201
226
  formState: { errors, isSubmitting, isValid },
202
227
  setValue,
203
- clearErrors
228
+ clearErrors,
229
+ watch
204
230
  } = methods;
231
+ const watchedDesiredRates = watch(desiredRateFields);
232
+ const hasDesiredRateWarning = desiredRateFields.some(
233
+ (_, index) => isDesiredRateOutOfRange(watchedDesiredRates[index])
234
+ );
205
235
  const submitForm = async (data) => {
206
236
  const processedData = {
207
237
  ...data,
@@ -240,12 +270,22 @@ const RateWatchForm = ({
240
270
  const handleTypeChange = (event) => {
241
271
  const value = event.target.value;
242
272
  setUpdateType(value);
273
+ methods.setValue("updateType", value, { shouldValidate: true });
274
+ };
275
+ const handleDesiredRateChange = (fieldName) => (event) => {
276
+ const sanitizedValue = sanitizeDesiredRate(event.target.value);
277
+ event.target.value = sanitizedValue;
278
+ setValue(fieldName, sanitizedValue, {
279
+ shouldValidate: true,
280
+ shouldDirty: true,
281
+ shouldTouch: true
282
+ });
243
283
  };
244
284
  const handleSubmitClick = () => {
245
285
  handleSubmit(submitForm)();
246
286
  };
247
287
  useEffect(() => {
248
- methods.reset();
288
+ methods.reset({ updateType });
249
289
  }, [updateType]);
250
290
  return /* @__PURE__ */ jsx("section", { className: "rate-watch-form", children: /* @__PURE__ */ jsx(FormProvider, { ...methods, children: /* @__PURE__ */ jsx(
251
291
  SuccesFormWrapper,
@@ -323,7 +363,7 @@ const RateWatchForm = ({
323
363
  /* @__PURE__ */ jsx("p", { className: mt1Rem, children: "You can establish up to three different targets with one rate watch." }),
324
364
  /* @__PURE__ */ jsxs("div", { className: threeColRow, children: [
325
365
  /* @__PURE__ */ jsxs("div", { children: [
326
- /* @__PURE__ */ jsx("label", { className: resposiveLabel, children: "Select a Loan Type*" }),
366
+ /* @__PURE__ */ jsx("label", { className: resposiveLabel, children: "Select a Loan Type" }),
327
367
  /* @__PURE__ */ jsxs(
328
368
  Dropdown,
329
369
  {
@@ -342,18 +382,24 @@ const RateWatchForm = ({
342
382
  )
343
383
  ] }),
344
384
  /* @__PURE__ */ jsxs("div", { children: [
345
- /* @__PURE__ */ jsx("label", { className: resposiveLabel, children: "Desired Rate" }),
385
+ /* @__PURE__ */ jsxs("label", { className: resposiveLabel, children: [
386
+ "Desired Rate",
387
+ /* @__PURE__ */ jsx("sup", { children: "*" })
388
+ ] }),
346
389
  /* @__PURE__ */ jsx(
347
390
  Input,
348
391
  {
349
392
  className: dynPH,
350
393
  ...register("tR1DesiredRate", {
351
- setValueAs: (value) => value ? `${value}` : ""
394
+ setValueAs: (value) => sanitizeDesiredRate(String(value ?? ""))
352
395
  }),
396
+ onChange: handleDesiredRateChange("tR1DesiredRate"),
353
397
  type: "number",
398
+ min: "2",
399
+ max: "10",
354
400
  placeholder: "Desired Rate",
355
401
  error: !!errors.tR1DesiredRate,
356
- helperText: errors.tR1DesiredRate?.message
402
+ helperText: isDesiredRateOutOfRange(watch("tR1DesiredRate")) ? "Desired Rate must be between 2 and 10." : errors.tR1DesiredRate?.message
357
403
  }
358
404
  )
359
405
  ] }),
@@ -404,9 +450,16 @@ const RateWatchForm = ({
404
450
  Input,
405
451
  {
406
452
  className: dynPH,
407
- ...register("tR2DesiredRate"),
408
- type: "string",
409
- placeholder: "Desired Rate"
453
+ ...register("tR2DesiredRate", {
454
+ setValueAs: (value) => sanitizeDesiredRate(String(value ?? ""))
455
+ }),
456
+ onChange: handleDesiredRateChange("tR2DesiredRate"),
457
+ type: "number",
458
+ min: "2",
459
+ max: "10",
460
+ placeholder: "Desired Rate",
461
+ error: isDesiredRateOutOfRange(watch("tR2DesiredRate")),
462
+ helperText: isDesiredRateOutOfRange(watch("tR2DesiredRate")) ? "Desired Rate must be between 2 and 10." : errors.tR2DesiredRate?.message
410
463
  }
411
464
  ) }),
412
465
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
@@ -452,10 +505,17 @@ const RateWatchForm = ({
452
505
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
453
506
  Input,
454
507
  {
455
- ...register("tR3DesiredRate"),
456
- type: "string",
508
+ ...register("tR3DesiredRate", {
509
+ setValueAs: (value) => sanitizeDesiredRate(String(value ?? ""))
510
+ }),
511
+ onChange: handleDesiredRateChange("tR3DesiredRate"),
512
+ type: "number",
513
+ min: "2",
514
+ max: "10",
457
515
  placeholder: "Desired Rate",
458
- className: dynPH
516
+ className: dynPH,
517
+ error: isDesiredRateOutOfRange(watch("tR3DesiredRate")),
518
+ helperText: isDesiredRateOutOfRange(watch("tR3DesiredRate")) ? "Desired Rate must be between 2 and 10." : errors.tR3DesiredRate?.message
459
519
  }
460
520
  ) }),
461
521
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
@@ -480,7 +540,20 @@ const RateWatchForm = ({
480
540
  }
481
541
  ) })
482
542
  ] }),
483
- /* @__PURE__ */ jsx("p", { className: mt1Rem, children: "*The number in Desired Rate must be at least 2% and no more than 10%" })
543
+ /* @__PURE__ */ jsxs(
544
+ "p",
545
+ {
546
+ className: mt1Rem,
547
+ style: {
548
+ color: hasDesiredRateWarning ? "#d92d20" : void 0,
549
+ fontWeight: hasDesiredRateWarning ? 600 : void 0
550
+ },
551
+ children: [
552
+ /* @__PURE__ */ jsx("sup", { children: "*" }),
553
+ " Please enter a desired rate between 2% and 10%"
554
+ ]
555
+ }
556
+ )
484
557
  ] }),
485
558
  /* @__PURE__ */ jsxs("div", { className: modalCheckboxContainer, children: [
486
559
  /* @__PURE__ */ jsx(
@@ -69,9 +69,9 @@ import "../Carousel/index.js";
69
69
  /* empty css */
70
70
  import "../Chatbot/store/chat.js";
71
71
  import "../Chatbot/Chatbot.js";
72
- import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
73
72
  import "../LoadingIndicator/LoadingIndicator.css.js";
74
73
  import "react-use";
74
+ import "../Chatbot/Chatbot.css.ts.vanilla.css.js";
75
75
  /* empty css */
76
76
  import "../Chatbot/store/messages.js";
77
77
  /* empty css */
@@ -17,6 +17,7 @@ const DownPaymentInput = forwardRef(
17
17
  disabled,
18
18
  helperText: helper,
19
19
  error = false,
20
+ defaultValue,
20
21
  ...rest
21
22
  }, ref) => {
22
23
  const [downPayment, setDownPayment] = useState(0);
@@ -293,6 +293,12 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
293
293
  .page_body_contents > div:has(._1m7m2ax._1m7m2a4) + div:has(._1m7m2ax._1m7m2a4) {
294
294
  padding-top: 0;
295
295
  }
296
+ .page_body_contents > div:has(.stacked) {
297
+ padding-bottom: 40px;
298
+ }
299
+ .page_body_contents > div:has(> ._1m7m2ax) {
300
+ padding-block: 56px;
301
+ }
296
302
  .page_body_contents > div:has(._1m7m2ax) + div:has(._1m7m2ax) {
297
303
  margin-top: 0;
298
304
  }
@@ -459,6 +465,9 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
459
465
  .stacked:first-child {
460
466
  padding-top: 42px;
461
467
  }
468
+ .page_body_contents > div:has(> ._1m7m2ax) {
469
+ padding-block: 42px;
470
+ }
462
471
  }
463
472
  @media screen and (max-width: 768px) {
464
473
  ._1m7m2av {
@@ -476,6 +485,9 @@ main > div:nth-last-child(2) > ._1m7m2a0:not(._1m7m2ax) {
476
485
  .stacked:first-child {
477
486
  padding-top: 2rem;
478
487
  }
488
+ .page_body_contents > div:has(> ._1m7m2ax) {
489
+ padding-block: 2rem;
490
+ }
479
491
  }
480
492
  @media screen and (max-width: 327px) {
481
493
  ._1m7m2av {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@axos-web-dev/shared-components",
3
3
  "description": "Axos shared components library for web.",
4
- "version": "2.2.29",
4
+ "version": "2.2.31",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
7
7
  "module": "dist/main.js",