@heycater/qualification-funnel 1.3.21 → 1.3.22

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/index.esm.js CHANGED
@@ -7783,6 +7783,39 @@ function answerInitialQuestions(state, answers) {
7783
7783
  }
7784
7784
  return stateWithAnswers;
7785
7785
  }
7786
+ function generateSessionId() {
7787
+ if (typeof window === "undefined") {
7788
+ return "";
7789
+ }
7790
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
7791
+ return crypto.randomUUID();
7792
+ }
7793
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
7794
+ }
7795
+ function cleanupOldTrackingSessions() {
7796
+ if (typeof window === "undefined" || typeof sessionStorage === "undefined") {
7797
+ return;
7798
+ }
7799
+ try {
7800
+ const keysToRemove = [];
7801
+ const maxAge = 24 * 60 * 60 * 1e3;
7802
+ const now2 = Date.now();
7803
+ for (let i2 = 0; i2 < sessionStorage.length; i2++) {
7804
+ const key = sessionStorage.key(i2);
7805
+ if (key && key.startsWith("funnel_started_")) {
7806
+ const timestamp = key.split("-")[1];
7807
+ if (timestamp && !isNaN(Number(timestamp))) {
7808
+ const age = now2 - Number(timestamp);
7809
+ if (age > maxAge) {
7810
+ keysToRemove.push(key);
7811
+ }
7812
+ }
7813
+ }
7814
+ }
7815
+ keysToRemove.forEach((key) => sessionStorage.removeItem(key));
7816
+ } catch (e2) {
7817
+ }
7818
+ }
7786
7819
  function initialize(state, {
7787
7820
  answers,
7788
7821
  disabledSteps,
@@ -7792,7 +7825,7 @@ function initialize(state, {
7792
7825
  selectedMenu,
7793
7826
  stepOrder
7794
7827
  } = {}) {
7795
- var _a2;
7828
+ var _a2, _b;
7796
7829
  const currentPageHref = typeof window === "undefined" ? void 0 : `${window.location.origin}${window.location.pathname}`;
7797
7830
  leadSource = leadSource || currentPageHref;
7798
7831
  const isUserLoggedIn = typeof window !== "undefined" && window.localStorage.getItem("token");
@@ -7801,6 +7834,7 @@ function initialize(state, {
7801
7834
  stepOrder
7802
7835
  });
7803
7836
  const firstStepId = ((_a2 = initialSteps[0]) == null ? void 0 : _a2.id) || DEFAULT_STEP_ORDER[0];
7837
+ const existingState = "qualification" in state ? state : null;
7804
7838
  const initialState = {
7805
7839
  ...state,
7806
7840
  initialOptions: {
@@ -7809,6 +7843,11 @@ function initialize(state, {
7809
7843
  status: QUALIFICATION_STATUS.answering,
7810
7844
  qualification: {
7811
7845
  leadSource,
7846
+ sessionInfo: ((_b = existingState == null ? void 0 : existingState.qualification) == null ? void 0 : _b.sessionInfo) || {
7847
+ sessionId: generateSessionId(),
7848
+ startedAt: Date.now(),
7849
+ lastActiveAt: Date.now()
7850
+ },
7812
7851
  answers: {},
7813
7852
  errors: {},
7814
7853
  steps: initialSteps.map((q2) => ({ id: q2.id })),
@@ -11188,6 +11227,20 @@ function FullServiceIcon(props) {
11188
11227
  }
11189
11228
  );
11190
11229
  }
11230
+ function getContextualProperties(state) {
11231
+ if (!("qualification" in state)) return {};
11232
+ const { sessionInfo, answers } = state.qualification;
11233
+ const props = {
11234
+ session_id: (sessionInfo == null ? void 0 : sessionInfo.sessionId) || ""
11235
+ };
11236
+ if (answers.service_type) {
11237
+ props.service_type = answers.service_type;
11238
+ }
11239
+ if (answers.city) {
11240
+ props.city = answers.city;
11241
+ }
11242
+ return props;
11243
+ }
11191
11244
  function trackFunnelEvent(action2, properties) {
11192
11245
  const eventName = `funnel_${action2}`;
11193
11246
  if (typeof window === "undefined") return;
@@ -11201,34 +11254,38 @@ function trackFunnelEvent(action2, properties) {
11201
11254
  });
11202
11255
  }
11203
11256
  }
11204
- function trackFunnelStarted(mode, totalSteps) {
11257
+ function trackFunnelStarted(mode, totalSteps, contextProps) {
11205
11258
  trackFunnelEvent("started", {
11206
11259
  funnel_mode: mode,
11207
- total_steps: totalSteps
11260
+ total_steps: totalSteps,
11261
+ ...contextProps
11208
11262
  });
11209
11263
  }
11210
- function trackStepViewed(mode, stepId, stepIndex, totalSteps) {
11264
+ function trackStepViewed(mode, stepId, stepIndex, totalSteps, contextProps) {
11211
11265
  trackFunnelEvent("step_viewed", {
11212
11266
  funnel_mode: mode,
11213
11267
  step_id: stepId,
11214
11268
  step_index: stepIndex,
11215
- total_steps: totalSteps
11269
+ total_steps: totalSteps,
11270
+ ...contextProps
11216
11271
  });
11217
11272
  }
11218
- function trackAnswerSelected(mode, stepId, questionId, value) {
11273
+ function trackAnswerSelected(mode, stepId, questionId, value, contextProps) {
11219
11274
  trackFunnelEvent("answer_selected", {
11220
11275
  funnel_mode: mode,
11221
11276
  step_id: stepId,
11222
11277
  question_id: questionId,
11223
- answer_value: value
11278
+ answer_value: value,
11279
+ ...contextProps
11224
11280
  });
11225
11281
  }
11226
- function trackStepCompleted(mode, stepId, stepIndex, totalSteps) {
11282
+ function trackStepCompleted(mode, stepId, stepIndex, totalSteps, contextProps) {
11227
11283
  trackFunnelEvent("step_completed", {
11228
11284
  funnel_mode: mode,
11229
11285
  step_id: stepId,
11230
11286
  step_index: stepIndex,
11231
- total_steps: totalSteps
11287
+ total_steps: totalSteps,
11288
+ ...contextProps
11232
11289
  });
11233
11290
  }
11234
11291
  function trackFormSubmitted(mode, properties) {
@@ -11237,9 +11294,10 @@ function trackFormSubmitted(mode, properties) {
11237
11294
  ...properties
11238
11295
  });
11239
11296
  }
11240
- function trackFunnelCompleted(mode) {
11297
+ function trackFunnelCompleted(mode, contextProps) {
11241
11298
  trackFunnelEvent("completed", {
11242
- funnel_mode: mode
11299
+ funnel_mode: mode,
11300
+ ...contextProps
11243
11301
  });
11244
11302
  }
11245
11303
  const TrackingContext = createContext(null);
@@ -26375,13 +26433,17 @@ function PhoneField({ label, name, formik, helperText }) {
26375
26433
  const StyledInput = styled(Input)`
26376
26434
  border-color: ${(props) => props.$hasError ? props.theme.palette.error.main : "transparent"};
26377
26435
  `;
26378
- function Success() {
26436
+ function Success({ isUkLead = false }) {
26379
26437
  const { actions } = useQualification();
26380
26438
  const router = useRouter();
26381
26439
  const { t: t2 } = useTranslation("qualification");
26382
26440
  const handleClose = () => {
26383
26441
  actions.initialize();
26384
- router.push("/en/catering-london");
26442
+ if (isUkLead) {
26443
+ router.push("/en/catering-london");
26444
+ } else {
26445
+ router.push(`/${router.locale}`);
26446
+ }
26385
26447
  };
26386
26448
  return /* @__PURE__ */ jsxs(Fragment, { children: [
26387
26449
  /* @__PURE__ */ jsx(Box, { display: "flex", justifyContent: "end", children: /* @__PURE__ */ jsx(Box, { px: 2, children: /* @__PURE__ */ jsx(IconButton$2, { onClick: handleClose, "aria-label": "back", component: "span", children: /* @__PURE__ */ jsx(CloseIcon$1, {}) }) }) }),
@@ -26661,7 +26723,7 @@ function useRequestForm() {
26661
26723
  });
26662
26724
  return {
26663
26725
  success,
26664
- isUkLead: true
26726
+ isUkLead
26665
26727
  };
26666
26728
  } else {
26667
26729
  const {
@@ -26709,6 +26771,7 @@ const getValidationSchema = ({
26709
26771
  function RequestForm({ header = null }) {
26710
26772
  var _a2, _b, _c;
26711
26773
  const [showLeadSuccess, setShowLeadSuccess] = useState(false);
26774
+ const [isActualUkLead, setIsActualUkLead] = useState(false);
26712
26775
  const { t: t2 } = useTranslation("qualification");
26713
26776
  const { submitAsNewCustomer, submitAsExistingCustomer } = useRequestForm();
26714
26777
  const pillValues = useAnswerPills({ display: "all-answers" });
@@ -26772,13 +26835,16 @@ function RequestForm({ header = null }) {
26772
26835
  isUkLead,
26773
26836
  success
26774
26837
  } = await submitAsNewCustomer(values2);
26775
- if (success && isUkLead) {
26838
+ if (success) {
26839
+ setIsActualUkLead(isUkLead);
26776
26840
  setShowLeadSuccess(true);
26841
+ const contextProps2 = getContextualProperties(state);
26777
26842
  trackFormSubmitted(mode, {
26778
- is_uk_lead: true,
26779
- has_bestseller: !!(qualification == null ? void 0 : qualification.selectedMenu)
26843
+ is_uk_lead: isUkLead,
26844
+ has_bestseller: !!(qualification == null ? void 0 : qualification.selectedMenu),
26845
+ ...contextProps2
26780
26846
  });
26781
- trackFunnelCompleted(mode);
26847
+ trackFunnelCompleted(mode, contextProps2);
26782
26848
  return;
26783
26849
  }
26784
26850
  if (signInToken) {
@@ -26787,11 +26853,13 @@ function RequestForm({ header = null }) {
26787
26853
  redirectRoute = `/${router.locale}/account/external/requests/${opportunity.id}`;
26788
26854
  }
26789
26855
  }
26856
+ const contextProps = getContextualProperties(state);
26790
26857
  trackFormSubmitted(mode, {
26791
26858
  is_logged_in: !!currentUserAccount,
26792
- has_bestseller: !!(qualification == null ? void 0 : qualification.selectedMenu)
26859
+ has_bestseller: !!(qualification == null ? void 0 : qualification.selectedMenu),
26860
+ ...contextProps
26793
26861
  });
26794
- trackFunnelCompleted(mode);
26862
+ trackFunnelCompleted(mode, contextProps);
26795
26863
  actions.setRequest(values2);
26796
26864
  redirectToCustomerAccountRequest(redirectRoute);
26797
26865
  };
@@ -26850,7 +26918,7 @@ function RequestForm({ header = null }) {
26850
26918
  phone: ((_c = (_b = currentUserAccount == null ? void 0 : currentUserAccount.owner) == null ? void 0 : _b.salesforceCustomer) == null ? void 0 : _c.phoneNumber) || ""
26851
26919
  };
26852
26920
  const isLoggedIn = !!currentUserAccount;
26853
- if (showLeadSuccess) return /* @__PURE__ */ jsx(Success, {});
26921
+ if (showLeadSuccess) return /* @__PURE__ */ jsx(Success, { isUkLead: isActualUkLead });
26854
26922
  return /* @__PURE__ */ jsxs(FeedbackMessageProvider, { children: [
26855
26923
  header,
26856
26924
  /* @__PURE__ */ jsx(
@@ -29873,6 +29941,55 @@ const LoadingIndicator = ({ variant }) => {
29873
29941
  const WrapperBox = styled(Box)`
29874
29942
  color: var(--embedded-text-color, inherit);
29875
29943
  `;
29944
+ function useAbandonmentTracking(mode, state) {
29945
+ var _a2, _b;
29946
+ const hasTrackedAbandonmentRef = useRef(false);
29947
+ const hasCompletedRef = useRef(false);
29948
+ useEffect(() => {
29949
+ if (state.status === QUALIFICATION_STATUS.answered || state.status === QUALIFICATION_STATUS.qualifiedForRequest || state.status === QUALIFICATION_STATUS.qualifiedForMarketplace || state.status === QUALIFICATION_STATUS.qualifiedForCate) {
29950
+ hasCompletedRef.current = true;
29951
+ return;
29952
+ }
29953
+ if (state.status !== QUALIFICATION_STATUS.answering) {
29954
+ hasTrackedAbandonmentRef.current = false;
29955
+ return;
29956
+ }
29957
+ if (!("qualification" in state)) return;
29958
+ const stateSnapshot = {
29959
+ stepId: state.qualification.step.id,
29960
+ stepIndex: state.qualification.stepIndex,
29961
+ totalSteps: state.qualification.steps.filter((s3) => !s3.disabled).length,
29962
+ contextProps: getContextualProperties(state)
29963
+ };
29964
+ const handleAbandonment = () => {
29965
+ if (hasTrackedAbandonmentRef.current) return;
29966
+ if (hasCompletedRef.current) return;
29967
+ trackFunnelEvent("abandoned", {
29968
+ funnel_mode: mode,
29969
+ step_id: stateSnapshot.stepId,
29970
+ step_index: stateSnapshot.stepIndex,
29971
+ total_steps: stateSnapshot.totalSteps,
29972
+ ...stateSnapshot.contextProps
29973
+ });
29974
+ hasTrackedAbandonmentRef.current = true;
29975
+ };
29976
+ window.addEventListener("beforeunload", handleAbandonment);
29977
+ let invisibilityTimeout;
29978
+ const handleVisibilityChange2 = () => {
29979
+ if (document.hidden) {
29980
+ invisibilityTimeout = setTimeout(handleAbandonment, 3e4);
29981
+ } else {
29982
+ clearTimeout(invisibilityTimeout);
29983
+ }
29984
+ };
29985
+ document.addEventListener("visibilitychange", handleVisibilityChange2);
29986
+ return () => {
29987
+ window.removeEventListener("beforeunload", handleAbandonment);
29988
+ document.removeEventListener("visibilitychange", handleVisibilityChange2);
29989
+ clearTimeout(invisibilityTimeout);
29990
+ };
29991
+ }, [state.status, (_b = (_a2 = state.qualification) == null ? void 0 : _a2.step) == null ? void 0 : _b.id, mode]);
29992
+ }
29876
29993
  const EmbeddedFunnel = React__default.forwardRef(
29877
29994
  ({
29878
29995
  onBackground,
@@ -29885,44 +30002,62 @@ const EmbeddedFunnel = React__default.forwardRef(
29885
30002
  mode = "embedded",
29886
30003
  onCancel
29887
30004
  }, ref2) => {
29888
- var _a2, _b, _c, _d, _e, _f, _g;
30005
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
29889
30006
  const scrollHelperRef = useRef(null);
29890
- const hasTrackedStart = useRef(false);
29891
30007
  const previousStepRef = useRef(null);
29892
- const onQuestionChange = useCallback(
29893
- (step2, stepIndex, totalSteps) => {
29894
- if (scrollHelperRef && (scrollHelperRef == null ? void 0 : scrollHelperRef.current)) {
29895
- scrollHelperRef.current.scrollIntoView({
29896
- inline: "nearest",
29897
- block: "nearest",
29898
- behavior: "smooth"
29899
- });
29900
- }
29901
- if (step2 && typeof totalSteps === "number") {
29902
- if (previousStepRef.current && previousStepRef.current !== step2.id && typeof stepIndex === "number" && stepIndex > 0) {
29903
- trackStepCompleted(
29904
- mode,
29905
- previousStepRef.current,
29906
- stepIndex - 1,
29907
- totalSteps
29908
- );
29909
- }
29910
- trackStepViewed(mode, step2.id, stepIndex ?? 0, totalSteps);
29911
- previousStepRef.current = step2.id;
30008
+ const { state, actions, canContinue } = useQualification({});
30009
+ useEffect(() => {
30010
+ cleanupOldTrackingSessions();
30011
+ }, []);
30012
+ useEffect(() => {
30013
+ if (state.status !== QUALIFICATION_STATUS.answering) {
30014
+ return;
30015
+ }
30016
+ const step2 = state.qualification.step;
30017
+ const stepIndex = state.qualification.stepIndex;
30018
+ const totalSteps = state.qualification.steps.filter((s3) => !s3.disabled).length;
30019
+ if (scrollHelperRef && (scrollHelperRef == null ? void 0 : scrollHelperRef.current)) {
30020
+ scrollHelperRef.current.scrollIntoView({
30021
+ inline: "nearest",
30022
+ block: "nearest",
30023
+ behavior: "smooth"
30024
+ });
30025
+ }
30026
+ if (step2 && previousStepRef.current !== step2.id) {
30027
+ const contextProps = getContextualProperties(state);
30028
+ if (previousStepRef.current && stepIndex > 0) {
30029
+ trackStepCompleted(
30030
+ mode,
30031
+ previousStepRef.current,
30032
+ stepIndex - 1,
30033
+ totalSteps,
30034
+ contextProps
30035
+ );
29912
30036
  }
29913
- },
29914
- [mode]
29915
- );
29916
- const { state, actions, canContinue } = useQualification({
29917
- onQuestionChange
29918
- });
30037
+ trackStepViewed(mode, step2.id, stepIndex, totalSteps, contextProps);
30038
+ previousStepRef.current = step2.id;
30039
+ }
30040
+ }, [state.status, (_b = (_a2 = state.qualification) == null ? void 0 : _a2.step) == null ? void 0 : _b.id, mode]);
29919
30041
  useEffect(() => {
29920
- if (!hasTrackedStart.current && state.status === QUALIFICATION_STATUS.answering) {
30042
+ var _a3, _b2;
30043
+ const sessionId = "qualification" in state ? (_b2 = (_a3 = state.qualification) == null ? void 0 : _a3.sessionInfo) == null ? void 0 : _b2.sessionId : null;
30044
+ if (!sessionId) return;
30045
+ const trackingKey = `funnel_started_${sessionId}`;
30046
+ let alreadyTracked = false;
30047
+ try {
30048
+ alreadyTracked = sessionStorage.getItem(trackingKey) === "true";
30049
+ } catch (e2) {
30050
+ }
30051
+ if (!alreadyTracked && "qualification" in state) {
29921
30052
  const enabledSteps2 = state.qualification.steps.filter((s3) => !s3.disabled);
29922
- trackFunnelStarted(mode, enabledSteps2.length);
29923
- hasTrackedStart.current = true;
30053
+ const contextProps = getContextualProperties(state);
30054
+ trackFunnelStarted(mode, enabledSteps2.length, contextProps);
30055
+ try {
30056
+ sessionStorage.setItem(trackingKey, "true");
30057
+ } catch (e2) {
30058
+ }
29924
30059
  }
29925
- }, [state.status, mode]);
30060
+ }, [(_d = (_c = state.qualification) == null ? void 0 : _c.sessionInfo) == null ? void 0 : _d.sessionId, mode]);
29926
30061
  useEffect(() => {
29927
30062
  if (scrollHelperRef && (scrollHelperRef == null ? void 0 : scrollHelperRef.current)) {
29928
30063
  scrollHelperRef.current.scrollIntoView({
@@ -29932,6 +30067,31 @@ const EmbeddedFunnel = React__default.forwardRef(
29932
30067
  });
29933
30068
  }
29934
30069
  }, [state.status]);
30070
+ const previousStatusRef = useRef(state.status);
30071
+ const lastAnsweringStateRef = useRef(null);
30072
+ useEffect(() => {
30073
+ if (state.status === QUALIFICATION_STATUS.answering && "qualification" in state) {
30074
+ lastAnsweringStateRef.current = {
30075
+ stepId: state.qualification.step.id,
30076
+ stepIndex: state.qualification.stepIndex
30077
+ };
30078
+ }
30079
+ }, [state.status, (_f = (_e = state.qualification) == null ? void 0 : _e.step) == null ? void 0 : _f.id, (_g = state.qualification) == null ? void 0 : _g.stepIndex]);
30080
+ useEffect(() => {
30081
+ if (previousStatusRef.current === QUALIFICATION_STATUS.answering && state.status === QUALIFICATION_STATUS.answered && lastAnsweringStateRef.current && "qualification" in state) {
30082
+ const totalSteps = state.qualification.steps.filter((s3) => !s3.disabled).length;
30083
+ const contextProps = getContextualProperties(state);
30084
+ trackStepCompleted(
30085
+ mode,
30086
+ lastAnsweringStateRef.current.stepId,
30087
+ lastAnsweringStateRef.current.stepIndex,
30088
+ totalSteps,
30089
+ contextProps
30090
+ );
30091
+ }
30092
+ previousStatusRef.current = state.status;
30093
+ }, [state.status, mode]);
30094
+ useAbandonmentTracking(mode, state);
29935
30095
  const toNextStep2 = useCallback(() => {
29936
30096
  actions.nextStep();
29937
30097
  }, [actions]);
@@ -29948,7 +30108,7 @@ const EmbeddedFunnel = React__default.forwardRef(
29948
30108
  if (state.status !== QUALIFICATION_STATUS.answered && state.status !== QUALIFICATION_STATUS.answering && state.status !== QUALIFICATION_STATUS.qualifiedForRequest) {
29949
30109
  return null;
29950
30110
  }
29951
- const stepId = state.status === "answering" && ((_a2 = state.qualification.step) == null ? void 0 : _a2.id) || "";
30111
+ const stepId = state.status === "answering" && ((_h = state.qualification.step) == null ? void 0 : _h.id) || "";
29952
30112
  const onKeyPress = (event) => {
29953
30113
  if (event.key === "Enter" && canContinue) {
29954
30114
  event.preventDefault();
@@ -29959,22 +30119,22 @@ const EmbeddedFunnel = React__default.forwardRef(
29959
30119
  return /* @__PURE__ */ jsx(EmbeddedRequestPage, { ratings });
29960
30120
  }
29961
30121
  let textColor = onBackground === "light" ? "dark" : "light";
29962
- if (stepId.includes("customer-") && ((_b = slides == null ? void 0 : slides.customerTier) == null ? void 0 : _b.whiteWrapper)) {
30122
+ if (stepId.includes("customer-") && ((_i = slides == null ? void 0 : slides.customerTier) == null ? void 0 : _i.whiteWrapper)) {
29963
30123
  textColor = "dark";
29964
30124
  }
29965
- if (stepId === "city" && ((_c = slides == null ? void 0 : slides.city) == null ? void 0 : _c.whiteWrapper)) {
30125
+ if (stepId === "city" && ((_j = slides == null ? void 0 : slides.city) == null ? void 0 : _j.whiteWrapper)) {
29966
30126
  textColor = "dark";
29967
30127
  }
29968
- if (stepId === "catering_categories" && ((_d = slides == null ? void 0 : slides.category) == null ? void 0 : _d.whiteWrapper)) {
30128
+ if (stepId === "catering_categories" && ((_k = slides == null ? void 0 : slides.category) == null ? void 0 : _k.whiteWrapper)) {
29969
30129
  textColor = "dark";
29970
30130
  }
29971
- if (stepId === "people_count_and_budget" && ((_e = slides == null ? void 0 : slides.budgetAndPeople) == null ? void 0 : _e.whiteWrapper)) {
30131
+ if (stepId === "people_count_and_budget" && ((_l = slides == null ? void 0 : slides.budgetAndPeople) == null ? void 0 : _l.whiteWrapper)) {
29972
30132
  textColor = "dark";
29973
30133
  }
29974
- if (stepId === "dietary_restrictions" && ((_f = slides == null ? void 0 : slides.dietaryRestriction) == null ? void 0 : _f.whiteWrapper)) {
30134
+ if (stepId === "dietary_restrictions" && ((_m = slides == null ? void 0 : slides.dietaryRestriction) == null ? void 0 : _m.whiteWrapper)) {
29975
30135
  textColor = "dark";
29976
30136
  }
29977
- if (stepId === "event_date" && ((_g = slides == null ? void 0 : slides.dateAndLead) == null ? void 0 : _g.whiteWrapper)) {
30137
+ if (stepId === "event_date" && ((_n = slides == null ? void 0 : slides.dateAndLead) == null ? void 0 : _n.whiteWrapper)) {
29978
30138
  textColor = "dark";
29979
30139
  }
29980
30140
  const isFullscreen = mode === "fullscreen";