@widergy/utilitygo-smart-bill-mobile 2.5.2 → 2.6.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.6.0](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v2.5.2...v2.6.0) (2025-06-27)
2
+
3
+
4
+ ### Features
5
+
6
+ * [CX-834, CX-852] smartbill analytics ([#41](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/issues/41)) ([3b02c37](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/commit/3b02c37355c36c4251ea1fbaab185d6336d78152))
7
+
1
8
  ## [2.5.2](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v2.5.1...v2.5.2) (2025-06-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widergy/utilitygo-smart-bill-mobile",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "UtilityGO SmartBill Mobile",
5
5
  "license": "MIT",
6
6
  "main": "src/lib/index.js",
@@ -17,6 +17,7 @@ const AIDrawer = ({
17
17
  components = {},
18
18
  error,
19
19
  getAnswer,
20
+ handlers,
20
21
  isOpen,
21
22
  loading,
22
23
  notEnoughBillsErrorType,
@@ -24,17 +25,20 @@ const AIDrawer = ({
24
25
  translations
25
26
  }) => {
26
27
  const styles = createStyles(colors);
28
+ const { trackOpenAIQuestion } = handlers;
27
29
  const { CollapsibleCard, SkeletonLoader } = components;
28
30
  const { generatedByLabel, title: drawerTitle } = translations?.AIDrawer || {};
29
31
  const [openCards, setOpenCards] = useState([]);
30
32
 
31
- const toggleCard = id => {
33
+ const toggleCard = (id, question) => {
32
34
  if (!answer?.[id] && !openCards.includes(id)) {
33
35
  getAnswer(id);
34
36
  }
35
- setOpenCards(prevOpenCards =>
36
- prevOpenCards.includes(id) ? prevOpenCards.filter(openId => openId !== id) : [...prevOpenCards, id]
37
- );
37
+ setOpenCards(prevOpenCards => {
38
+ if (prevOpenCards.includes(id)) return prevOpenCards.filter(openId => openId !== id);
39
+ trackOpenAIQuestion?.(question);
40
+ return [...prevOpenCards, id];
41
+ });
38
42
  };
39
43
 
40
44
  const handleClose = () => {
@@ -115,7 +119,7 @@ const AIDrawer = ({
115
119
  CustomContent={AnswerContentComponent}
116
120
  labelProps={{ withMarkdown: true }}
117
121
  loading={isLoading}
118
- onPress={() => toggleCard(id)}
122
+ onPress={() => toggleCard(id, question)}
119
123
  preventRipple
120
124
  style={{
121
125
  content: styles.collapsibleCardContent,
@@ -161,6 +165,7 @@ AIDrawer.propTypes = {
161
165
  components: object,
162
166
  error: object,
163
167
  getAnswer: func,
168
+ handlers: object,
164
169
  isOpen: bool,
165
170
  loading: object,
166
171
  notEnoughBillsErrorType: string,
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable react/forbid-prop-types */
2
2
  /* eslint-disable react/jsx-fragments */
3
- import React, { Fragment, useEffect, useState } from 'react';
3
+ import React, { Fragment, useEffect, useState, useMemo } from 'react';
4
4
  import { UTButton, UTLoading, UTTabs } from '@widergy/mobile-ui';
5
5
  import { View } from 'react-native';
6
6
  import { arrayOf, bool, func, object, shape, string } from 'prop-types';
7
7
  import isEmpty from 'lodash/isEmpty';
8
+ import debounce from 'lodash/debounce';
8
9
 
9
10
  import { TAB_COMPONENT_MAPPER } from './constants';
10
11
  import { getDefaultCurrentTab, getTabOptions } from './utils';
@@ -35,15 +36,41 @@ const SmartBillSummary = ({
35
36
  smartBillAIAnswerError,
36
37
  smartBillAIAnswerLoading
37
38
  } = constants;
38
- const { getSmartBillAIAnswer, getSmartBillAIQuestions, resetSmartBillAIAnswers } = handlers;
39
+ const {
40
+ getSmartBillAIAnswer,
41
+ getSmartBillAIQuestions,
42
+ resetSmartBillAIAnswers,
43
+ trackAIFloatingButtonClick,
44
+ trackTabChange
45
+ } = handlers;
39
46
  const filteredTabOptions = getTabOptions(tabOptions, smartBill) || [];
40
47
  const defaultCurrentTab = getDefaultCurrentTab(filteredTabOptions);
41
48
  const [currentTab, setCurrentTab] = useState(defaultCurrentTab);
42
49
  const [tabsKey, setTabsKey] = useState(0);
43
50
  const [aiDrawerIsOpen, setAiDrawerIsOpen] = useState(false);
51
+ const [lastTrackedTab, setLastTrackedTab] = useState(defaultCurrentTab);
44
52
 
45
53
  const changeCurrentTab = newTab => setCurrentTab(newTab?.value || newTab);
54
+
55
+ const debouncedTrackTabChange = useMemo(
56
+ () =>
57
+ debounce(newTab => {
58
+ const tabValue = newTab?.value || newTab;
59
+ if (tabValue !== lastTrackedTab) {
60
+ trackTabChange(tabValue);
61
+ setLastTrackedTab(tabValue);
62
+ }
63
+ }, 300),
64
+ [trackTabChange, lastTrackedTab]
65
+ );
66
+
67
+ const onUserTabChange = newTab => {
68
+ debouncedTrackTabChange(newTab);
69
+ changeCurrentTab(newTab);
70
+ };
71
+
46
72
  const openAIDrawer = () => {
73
+ trackAIFloatingButtonClick?.();
47
74
  if (isEmpty(aiQuestionsList)) getSmartBillAIQuestions();
48
75
  setAiDrawerIsOpen(true);
49
76
  };
@@ -53,8 +80,17 @@ const SmartBillSummary = ({
53
80
  };
54
81
 
55
82
  useEffect(() => {
56
- if (defaultCurrentTab) changeCurrentTab(defaultCurrentTab);
57
- }, [defaultCurrentTab, tabOptions]);
83
+ if (defaultCurrentTab && defaultCurrentTab !== currentTab) {
84
+ changeCurrentTab(defaultCurrentTab);
85
+ }
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, [defaultCurrentTab]);
88
+
89
+ useEffect(() => {
90
+ if (defaultCurrentTab && !lastTrackedTab) {
91
+ setLastTrackedTab(defaultCurrentTab);
92
+ }
93
+ }, [defaultCurrentTab, lastTrackedTab]);
58
94
 
59
95
  useEffect(() => {
60
96
  if (glossaryError && !glossaryLoading && defaultCurrentTab) {
@@ -63,12 +99,18 @@ const SmartBillSummary = ({
63
99
  }
64
100
  }, [defaultCurrentTab, glossaryError, glossaryLoading]);
65
101
 
102
+ useEffect(() => {
103
+ return () => {
104
+ debouncedTrackTabChange.cancel();
105
+ };
106
+ }, [debouncedTrackTabChange]);
107
+
66
108
  return (
67
109
  <UTLoading loading={loading} style={styles.loading}>
68
110
  <UTTabs
69
111
  key={tabsKey}
70
112
  labelProps={{ weight: 'semibold' }}
71
- onChange={changeCurrentTab}
113
+ onChange={onUserTabChange}
72
114
  tabs={filteredTabOptions}
73
115
  value={currentTab}
74
116
  />
@@ -105,6 +147,7 @@ const SmartBillSummary = ({
105
147
  components={components}
106
148
  error={smartBillAIAnswerError}
107
149
  getAnswer={getSmartBillAIAnswer}
150
+ handlers={handlers}
108
151
  isOpen={aiDrawerIsOpen}
109
152
  loading={smartBillAIAnswerLoading}
110
153
  notEnoughBillsErrorType={notEnoughBillsErrorType}
@@ -1,9 +1,10 @@
1
1
  /* eslint-disable react/forbid-prop-types */
2
- import React from 'react';
2
+ import React, { useEffect, useMemo } from 'react';
3
3
  import { Linking, View } from 'react-native';
4
4
  import { Surface, UTButton, UTLabel } from '@widergy/mobile-ui';
5
5
  import { object, string, func, objectOf, array } from 'prop-types';
6
6
  import isEmpty from 'lodash/isEmpty';
7
+ import debounce from 'lodash/debounce';
7
8
 
8
9
  import {
9
10
  getConsumptionLabels,
@@ -24,7 +25,8 @@ const RateCard = ({
24
25
  ratesTableLink,
25
26
  smartBill = {},
26
27
  subsidy = '',
27
- subsidyLevels
28
+ subsidyLevels,
29
+ trackRedirectionToExternalLink
28
30
  }) => {
29
31
  const styles = createStyles(colors);
30
32
  const {
@@ -60,7 +62,24 @@ const RateCard = ({
60
62
  const noLevels = isEmpty(consumptionLevels);
61
63
  const showRateLabel = !isT1 || startRateValue === false || limitRateValue === false;
62
64
 
63
- const openExternalLink = () => ratesTableLink && Linking.openURL(ratesTableLink);
65
+ const debouncedTrackRedirection = useMemo(
66
+ () =>
67
+ debounce(link => {
68
+ trackRedirectionToExternalLink(link);
69
+ }, 300),
70
+ [trackRedirectionToExternalLink]
71
+ );
72
+
73
+ useEffect(() => {
74
+ return () => {
75
+ debouncedTrackRedirection.cancel();
76
+ };
77
+ }, [debouncedTrackRedirection]);
78
+
79
+ const openExternalLink = () => {
80
+ debouncedTrackRedirection(ratesTableLink);
81
+ if (ratesTableLink) Linking.openURL(ratesTableLink);
82
+ };
64
83
 
65
84
  return (
66
85
  <Surface elevation={1} style={styles.container}>
@@ -153,7 +172,8 @@ RateCard.propTypes = {
153
172
  ratesTableLink: string,
154
173
  smartBill: object,
155
174
  subsidy: string,
156
- subsidyLevels: objectOf(string)
175
+ subsidyLevels: objectOf(string),
176
+ trackRedirectionToExternalLink: func
157
177
  };
158
178
 
159
179
  export default RateCard;
@@ -68,7 +68,8 @@ const Billing = ({
68
68
  const {
69
69
  handleAutomaticDebitAdherence = () => {},
70
70
  handleDownloadBill = () => {},
71
- handleGoToDigitalBill = () => {}
71
+ handleGoToDigitalBill = () => {},
72
+ trackRedirectionToExternalLink = () => {}
72
73
  } = handlers;
73
74
  const {
74
75
  adherence_to_automatic_debit: adherenceToAutomaticDebit,
@@ -173,6 +174,7 @@ const Billing = ({
173
174
  smartBill,
174
175
  subsidy,
175
176
  subsidyLevels,
177
+ trackRedirectionToExternalLink,
176
178
  translations
177
179
  }}
178
180
  />
@@ -3,15 +3,17 @@ import React, { useEffect, useMemo, useState } from 'react';
3
3
  import { Image, ScrollView, View } from 'react-native';
4
4
  import { UTButton, UTLabel, UTLoading } from '@widergy/mobile-ui';
5
5
  import { func, object, shape, string } from 'prop-types';
6
+ import debounce from 'lodash/debounce';
6
7
 
7
8
  import styles from './styles';
8
9
 
9
10
  const Glossary = ({ constants = {}, handlers = {}, translations = {} }) => {
10
- const { getGlossaryData } = handlers;
11
+ const { getGlossaryData, trackClickGlossarySection = () => {} } = handlers;
11
12
  const { glossaryError: error, glossaryData, glossaryLoading: loading } = constants;
12
13
  const { title: glossaryTitle } = translations;
13
14
  const { pages = [], sections = [] } = glossaryData || {};
14
15
  const [selectedIndex, setSelectedIndex] = useState(0);
16
+ const [lastTrackedSection, setLastTrackedSection] = useState(null);
15
17
  const selectedSection = useMemo(() => sections?.[selectedIndex] || {}, [sections, selectedIndex]);
16
18
  const currentPage = pages.find(p => p.id === selectedSection.page_id) || {};
17
19
  const { image } = currentPage;
@@ -21,15 +23,28 @@ const Glossary = ({ constants = {}, handlers = {}, translations = {} }) => {
21
23
  const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
22
24
  const aspectRatio = imageSize.width / imageSize.height || 1;
23
25
 
26
+ const debouncedTrackSection = useMemo(
27
+ () =>
28
+ debounce(sectionTitle => {
29
+ if (sectionTitle && sectionTitle !== lastTrackedSection) {
30
+ trackClickGlossarySection(sectionTitle);
31
+ setLastTrackedSection(sectionTitle);
32
+ }
33
+ }, 300),
34
+ [trackClickGlossarySection, lastTrackedSection]
35
+ );
36
+
24
37
  const handleNext = () => {
25
38
  if (selectedIndex < sections.length - 1) {
26
39
  setSelectedIndex(prev => prev + 1);
40
+ debouncedTrackSection(sections[selectedIndex + 1]?.title);
27
41
  }
28
42
  };
29
43
 
30
44
  const handlePrev = () => {
31
45
  if (selectedIndex > 0) {
32
46
  setSelectedIndex(prev => prev - 1);
47
+ debouncedTrackSection(sections[selectedIndex - 1]?.title);
33
48
  }
34
49
  };
35
50
 
@@ -41,11 +56,23 @@ const Glossary = ({ constants = {}, handlers = {}, translations = {} }) => {
41
56
  if (image) Image.getSize(image, (width, height) => setImageSize({ width, height }));
42
57
  }, [image, selectedSection]);
43
58
 
59
+ useEffect(() => {
60
+ if (selectedSection?.title && !lastTrackedSection) {
61
+ setLastTrackedSection(selectedSection.title);
62
+ }
63
+ }, [selectedSection, lastTrackedSection]);
64
+
44
65
  useEffect(() => {
45
66
  getGlossaryData?.();
46
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
47
68
  }, []);
48
69
 
70
+ useEffect(() => {
71
+ return () => {
72
+ debouncedTrackSection.cancel();
73
+ };
74
+ }, [debouncedTrackSection]);
75
+
49
76
  return error && !loading ? null : (
50
77
  <GeneralContainer>
51
78
  <View style={styles.generalContainer}>