@widergy/utilitygo-smart-bill-mobile 3.11.0 → 3.12.1

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,18 @@
1
+ ## [3.12.1](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v3.12.0...v3.12.1) (2026-03-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * [DEV-766] ci linter skip ([#69](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/issues/69)) ([b0177a9](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/commit/b0177a9643bed6ca2b18c8b6d442ee614800fb44))
7
+
8
+ # [3.12.0](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v3.11.0...v3.12.0) (2026-03-12)
9
+
10
+
11
+ ### Features
12
+
13
+ * [EVEP-107] attachment delinquency ([#68](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/issues/68)) ([6f713e7](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/commit/6f713e7134a83ba606a35c95188d3f9d26dd2362))
14
+ * [EVEP-423] change ratecard footer ([#67](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/issues/67)) ([eded80d](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/commit/eded80d667497e5342fb0d3bdbc40499cfa1109b))
15
+
1
16
  # [3.11.0](https://github.com/widergy/UtilityGO-Smart-Bill-Mobile/compare/v3.10.0...v3.11.0) (2026-02-26)
2
17
 
3
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widergy/utilitygo-smart-bill-mobile",
3
- "version": "3.11.0",
3
+ "version": "3.12.1",
4
4
  "description": "UtilityGO SmartBill Mobile",
5
5
  "license": "MIT",
6
6
  "main": "src/lib/index.js",
@@ -12,6 +12,7 @@ import getStyles from './styles';
12
12
  import AIDrawer from './components/AIDrawer';
13
13
  import BillHeader from './tabs/Billing/components/BillHeader';
14
14
  import AlternativeHeader from './tabs/Billing/components/AlternativeHeader';
15
+ import Attachment from './tabs/Billing/components/Attachment';
15
16
 
16
17
  const SmartBillSummary = ({
17
18
  assets,
@@ -58,6 +59,7 @@ const SmartBillSummary = ({
58
59
  const [tabsKey, setTabsKey] = useState(0);
59
60
  const [aiDrawerIsOpen, setAiDrawerIsOpen] = useState(false);
60
61
  const [lastTrackedTab, setLastTrackedTab] = useState(defaultCurrentTab);
62
+ const [attachmentOpen, setAttachmentOpen] = useState(false);
61
63
 
62
64
  const changeCurrentTab = newTab => setCurrentTab(newTab?.value || newTab);
63
65
 
@@ -222,6 +224,7 @@ const SmartBillSummary = ({
222
224
  billType={smartBill?.bill_type}
223
225
  colors={colors}
224
226
  debtStatusLabel={smartBill?.debt_status_label}
227
+ delinquency={smartBill?.client?.delinquency}
225
228
  header={translations?.billingTab.header}
226
229
  smartBill={smartBill}
227
230
  />
@@ -237,6 +240,14 @@ const SmartBillSummary = ({
237
240
  clientNumberFormatter={utils?.formatters?.clientNumberFormatter}
238
241
  />
239
242
  )}
243
+ {smartBill?.attachment && (
244
+ <Attachment
245
+ attachment={smartBill?.attachment}
246
+ attachmentOpen={attachmentOpen}
247
+ attachmentText={translations.billingTab.header.attachmentText}
248
+ setAttachmentOpen={setAttachmentOpen}
249
+ />
250
+ )}
240
251
  {TAB_COMPONENT_MAPPER?.[currentTab]?.({
241
252
  assets,
242
253
  billingLayout,
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { object, string, func, bool } from 'prop-types';
3
+ import { UTActionCard, UTBottomSheet } from '@widergy/mobile-ui';
4
+
5
+ import styles from './styles';
6
+
7
+ const Attachment = ({ attachment, attachmentText, setAttachmentOpen, attachmentOpen }) => {
8
+ return (
9
+ <>
10
+ <UTActionCard
11
+ classNames={{ container: styles.container, titleAndDescription: styles.titleAndDescription }}
12
+ headerActions={[
13
+ {
14
+ colorTheme: 'accent',
15
+ Icon: 'IconChevronRight',
16
+ isPrimary: true
17
+ }
18
+ ]}
19
+ adornment={{
20
+ position: 'left',
21
+ type: 'icon',
22
+ alignment: 'center',
23
+ justifyContent: 'center',
24
+ props: { name: 'IconSearch', colorTheme: 'gray' }
25
+ }}
26
+ mainAction={() => setAttachmentOpen(!attachmentOpen)}
27
+ title={attachmentText}
28
+ titleProps={{ style: styles.titleActionCard }}
29
+ />
30
+ <UTBottomSheet
31
+ onClose={() => setAttachmentOpen(false)}
32
+ title={attachment.title}
33
+ titleProps={{ variant: 'title3', weight: 'medium', colorTheme: 'accent' }}
34
+ description={attachment.subtitle}
35
+ visible={attachmentOpen}
36
+ withBodyPadding={false}
37
+ />
38
+ </>
39
+ );
40
+ };
41
+
42
+ Attachment.propTypes = {
43
+ attachment: object,
44
+ attachmentText: string,
45
+ setAttachmentOpen: func,
46
+ attachmentOpen: bool
47
+ };
48
+
49
+ export default Attachment;
@@ -0,0 +1,13 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ container: {
5
+ marginBottom: 8,
6
+ marginHorizontal: 16,
7
+ marginTop: 24
8
+ },
9
+ titleAndDescription: {
10
+ display: 'flex',
11
+ justifyContent: 'center'
12
+ }
13
+ });
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Surface, UTLabel, UTStatus } from '@widergy/mobile-ui';
2
+ import { Surface, UTLabel, UTStatus, UTIcon } from '@widergy/mobile-ui';
3
3
  import { View, Image } from 'react-native';
4
4
  import { string, object } from 'prop-types';
5
5
 
@@ -8,14 +8,23 @@ import { getCurrentPeriod, getFormattedDate } from '../../utils';
8
8
 
9
9
  import { createStyles } from './styles';
10
10
 
11
- const BillHeader = ({ assets, colors, header, debtStatusLabel, smartBill, billNumber, billType }) => {
11
+ const BillHeader = ({
12
+ assets,
13
+ colors,
14
+ header,
15
+ debtStatusLabel,
16
+ delinquency,
17
+ smartBill,
18
+ billNumber,
19
+ billType
20
+ }) => {
12
21
  const styles = createStyles(colors);
13
22
  const { UtilityLogo } = assets.billingTab;
14
23
 
15
24
  const { billIssueDate, billTypeHelpText } = header;
16
25
  const currentPeriod = getCurrentPeriod(smartBill);
17
26
  const issuedOnDate = getFormattedDate(currentPeriod?.settlements?.current?.issued_on);
18
- const debtStatusValidation = debtStatusLabel !== null;
27
+ const debtStatusValidation = !!debtStatusLabel;
19
28
  return (
20
29
  <Surface elevation={1} style={styles.billHeader}>
21
30
  {UtilityLogo && <Image resizeMode="contain" source={UtilityLogo} height={35} />}
@@ -44,6 +53,14 @@ const BillHeader = ({ assets, colors, header, debtStatusLabel, smartBill, billNu
44
53
  {debtStatusValidation && (
45
54
  <DebtStatusLabel colors={colors} debtStatusLabel={debtStatusLabel} isWarning={smartBill?.warning} />
46
55
  )}
56
+ {!!delinquency && (
57
+ <View style={styles.delinquency}>
58
+ <UTIcon name="IconX" size={16} colorTheme="error" />
59
+ <UTLabel variant="small" colorTheme="error">
60
+ {delinquency?.title}
61
+ </UTLabel>
62
+ </View>
63
+ )}
47
64
  </View>
48
65
  </Surface>
49
66
  );
@@ -56,6 +73,7 @@ BillHeader.propTypes = {
56
73
  colors: object,
57
74
  debtStatusLabel: string,
58
75
  header: object,
76
+ delinquency: object,
59
77
  smartBill: object
60
78
  };
61
79
 
@@ -2,32 +2,42 @@ import { StyleSheet } from 'react-native';
2
2
 
3
3
  export const createStyles = colors =>
4
4
  StyleSheet.create({
5
- billHeaderLabels: {
6
- alignItems: 'center',
7
- columnGap: 32,
8
- flexDirection: 'row',
9
- justifyContent: 'space-between',
10
- paddingTop: 16,
11
- rowGap: 16,
12
- flexWrap: 'wrap'
13
- },
14
- valueAndHelpText: {
15
- flexDirection: 'column',
16
- gap: 4
17
- },
18
5
  billHeader: {
19
6
  backgroundColor: colors.light01,
20
7
  borderRadius: 8,
21
8
  flexDirection: 'column',
22
9
  gap: 12,
23
10
  justifyContent: 'space-between',
24
- padding: 16,
25
11
  marginHorizontal: 16,
26
- marginTop: 16
12
+ marginTop: 16,
13
+ padding: 16
27
14
  },
15
+ billHeaderLabels: {
16
+ alignItems: 'center',
17
+ columnGap: 32,
18
+ flexDirection: 'row',
19
+ flexWrap: 'wrap',
20
+ justifyContent: 'space-between',
21
+ paddingTop: 16,
22
+ rowGap: 16
23
+ },
24
+
28
25
  billTypeAndNumber: {
29
26
  alignItems: 'center',
30
27
  flexDirection: 'row',
31
28
  gap: 4
29
+ },
30
+ delinquency: {
31
+ alignItems: 'center',
32
+ backgroundColor: colors.semanticError01,
33
+ borderRadius: 8,
34
+ display: 'flex',
35
+ flexDirection: 'row',
36
+ gap: 8,
37
+ padding: 4
38
+ },
39
+ valueAndHelpText: {
40
+ flexDirection: 'column',
41
+ gap: 4
32
42
  }
33
43
  });
@@ -53,7 +53,7 @@ const ProgressBar = ({
53
53
  end={{ x: 1, y: 0 }}
54
54
  style={styles.progressBackground}
55
55
  >
56
- {data.asMutable({ deep: true }).map(({ range, name }) => {
56
+ {(data?.asMutable ? data.asMutable({ deep: true }) : data).map(({ range, name }) => {
57
57
  const isLast = range[1] === 'Infinity';
58
58
  const width = isLast ? '20%' : `${(range[1] / totalRange) * 100}%`;
59
59
 
@@ -1,10 +1,9 @@
1
1
  import React, { useEffect, useMemo } from 'react';
2
2
  import { Linking, View } from 'react-native';
3
- import { Surface, UTButton, UTLabel } from '@widergy/mobile-ui';
3
+ import { Surface, UTButton, UTLabel, UTDataElement } from '@widergy/mobile-ui';
4
4
  import { object, string, func, objectOf, array, bool } from 'prop-types';
5
5
  import isEmpty from 'lodash/isEmpty';
6
6
  import debounce from 'lodash/debounce';
7
- import upperFirst from 'lodash/upperFirst';
8
7
 
9
8
  import {
10
9
  getConsumptionLabels,
@@ -44,7 +43,7 @@ const RateCard = ({
44
43
  yourConsumption,
45
44
  yourRate
46
45
  } = rateCardTranslations;
47
- const { client, periods } = smartBill;
46
+ const { client, periods } = smartBill?.asMutable ? smartBill.asMutable({ deep: true }) : { ...smartBill };
48
47
  const purchasedRate = client?.rate?.purchased || '';
49
48
  const periodsToCompare = getConsumptionPeriodsToCompare(periods);
50
49
  const { totalConsumption, totalConsumptionFormatted } = getConsumptionLabels(periodsToCompare) || {};
@@ -141,16 +140,48 @@ const RateCard = ({
141
140
 
142
141
  <View style={styles.footer}>
143
142
  {hasCharge &&
144
- charges.map(({ concept, amount }) => (
145
- <View style={styles.detail} key={concept}>
146
- <UTLabel style={styles.detailTitle} weight="medium">
147
- {upperFirst(concept)}
148
- </UTLabel>
149
- <UTLabel variant="subtitle1" weight="medium">
150
- {formatAmount?.(amount) || '-'}
151
- </UTLabel>
152
- </View>
153
- ))}
143
+ charges
144
+ ?.sort((a, b) => {
145
+ const aHasSubconcepts = !isEmpty(a.lines);
146
+ const bHasSubconcepts = !isEmpty(b.lines);
147
+
148
+ if (aHasSubconcepts === bHasSubconcepts) return 0;
149
+ return aHasSubconcepts ? 1 : -1;
150
+ })
151
+ .map(({ concept, amount, lines = [] }) => {
152
+ const hasSubconcepts = !isEmpty(lines);
153
+ const lastSubconcept = lines.length - 1;
154
+
155
+ const renderSubconcept = (subconcept, i) => {
156
+ const subconceptStyles = [
157
+ styles.subconcept,
158
+ !(i === lastSubconcept) && styles.subconceptBottomBorder,
159
+ i === 0 && styles.subconceptTopBorder
160
+ ];
161
+ return (
162
+ <View style={subconceptStyles} key={i}>
163
+ <View style={styles.subconceptTitleContainer}>
164
+ <UTLabel colorTheme="gray">{subconcept?.title ?? '-'}</UTLabel>
165
+ </View>
166
+ <UTLabel weight="medium">{formatAmount?.(subconcept?.amount)}</UTLabel>
167
+ </View>
168
+ );
169
+ };
170
+
171
+ return (
172
+ <UTDataElement
173
+ Data={formatAmount?.(amount)}
174
+ dataProps={{ colorTheme: 'dark', weight: 'medium' }}
175
+ key={concept}
176
+ orientation={hasSubconcepts ? 'vertical' : undefined}
177
+ style={styles.concept}
178
+ title={concept}
179
+ titleProps={{ weight: 'medium', colorTheme: 'dark' }}
180
+ >
181
+ {hasSubconcepts && lines?.map(renderSubconcept)}
182
+ </UTDataElement>
183
+ );
184
+ })}
154
185
 
155
186
  <View style={styles.footerAction}>
156
187
  {externalLinks.map(({ link, label }, index) => (
@@ -2,68 +2,94 @@ import { StyleSheet } from 'react-native';
2
2
 
3
3
  export const createStyles = colors =>
4
4
  StyleSheet.create({
5
+ concept: {
6
+ borderTopColor: colors.light04,
7
+ borderTopWidth: 1,
8
+ paddingHorizontal: 16
9
+ },
10
+ consumptionContent: {
11
+ flex: 1
12
+ },
13
+ consumptionDataContainer: {
14
+ alignItems: 'center',
15
+ flexDirection: 'row',
16
+ gap: 24,
17
+ justifyContent: 'space-between',
18
+ paddingHorizontal: 16,
19
+ paddingTop: 16
20
+ },
5
21
  container: {
6
22
  backgroundColor: colors.light01,
7
23
  borderRadius: 8,
8
24
  flexDirection: 'column',
9
25
  gap: 24
10
26
  },
11
- headerTitles: {
12
- flexDirection: 'column',
13
- gap: 8,
14
- paddingTop: 16,
15
- paddingHorizontal: 16
16
- },
17
- consumptionDataContainer: {
27
+ detail: {
18
28
  alignItems: 'center',
29
+ borderTopColor: colors.light04,
30
+ borderTopWidth: 1,
19
31
  flexDirection: 'row',
32
+ gap: 16,
20
33
  justifyContent: 'space-between',
21
- gap: 24,
22
- paddingTop: 16,
23
- paddingHorizontal: 16
24
- },
25
- noLevels: {
26
- paddingTop: 0
34
+ padding: 16
27
35
  },
28
- consumptionContent: {
36
+ detailTitle: {
29
37
  flex: 1
30
38
  },
39
+ divider: {
40
+ backgroundColor: colors.light04,
41
+ height: 1,
42
+ width: '100%'
43
+ },
31
44
  footer: {
32
45
  width: '100%'
33
46
  },
34
47
  footerAction: {
35
48
  alignItems: 'center',
36
- backgroundColor: 'transparent',
37
- borderTopWidth: 1,
38
49
  borderTopColor: colors.light04,
50
+ borderTopWidth: 1,
39
51
  flexDirection: 'column',
40
52
  justifyContent: 'space-between'
41
53
  },
42
- detail: {
54
+ footerActionChildren: {
43
55
  alignItems: 'center',
44
- borderTopWidth: 1,
45
- borderTopColor: colors.light04,
46
- flexDirection: 'row',
47
56
  justifyContent: 'space-between',
48
- gap: 16,
49
- padding: 16
50
- },
51
- detailTitle: {
52
- flex: 1
57
+ paddingVertical: 8
53
58
  },
54
59
  footerActionRoot: {
55
- padding: 0,
56
60
  margin: 0,
61
+ padding: 0,
57
62
  width: '100%'
58
63
  },
59
- footerActionChildren: {
60
- alignItems: 'center',
64
+ headerTitles: {
65
+ flexDirection: 'column',
66
+ gap: 8,
67
+ paddingHorizontal: 16,
68
+ paddingTop: 16
69
+ },
70
+ noLevels: {
71
+ paddingTop: 0
72
+ },
73
+ subconcept: {
74
+ flexDirection: 'row',
61
75
  justifyContent: 'space-between',
62
- paddingVertical: 8
76
+ paddingTop: 16,
77
+ width: '100%'
63
78
  },
64
- divider: {
65
- backgroundColor: colors.light04,
66
- width: '100%',
67
- height: 1
79
+ subconceptBottomBorder: {
80
+ borderBottomColor: colors.light04,
81
+ borderBottomWidth: 1,
82
+ paddingBottom: 16
83
+ },
84
+ subconceptContent: {
85
+ flex: 1
86
+ },
87
+ subconceptTitleContainer: {
88
+ flex: 1,
89
+ marginRight: 8
90
+ },
91
+ subconceptTopBorder: {
92
+ borderTopColor: colors.light04,
93
+ borderTopWidth: 1
68
94
  }
69
95
  });
@@ -3,6 +3,7 @@ import { bool, object, string } from 'prop-types';
3
3
  import { Surface, UTIcon, UTLabel } from '@widergy/mobile-ui';
4
4
  import { capitalize } from 'lodash';
5
5
  import { View } from 'react-native';
6
+ import numeral from 'numeral';
6
7
 
7
8
  import { getBillingsToShow, getMonthName, getDateRange } from './utils';
8
9
  import { DEFAULT_TAG_COLOR_THEME, DEFAULT_TAG_ICON, RIGHT_UNIT_ALIGNMENT, TAG_ICON_SIZE } from './constants';
@@ -40,6 +41,7 @@ const Billing = ({
40
41
  const monthName = getMonthName(issuedOn);
41
42
  const consumptionValue = consumptions?.[0].value;
42
43
  const consumptionUnit = consumptions?.[0].unit;
44
+ const totalConsumptionFormatted = numeral(consumptionValue).format('0,0.[00]');
43
45
 
44
46
  return (
45
47
  <View key={issuedOn} style={[styles.billing, current ? styles.currentBilling : {}]}>
@@ -47,8 +49,8 @@ const Billing = ({
47
49
  <View style={styles.consumption}>
48
50
  <UTLabel variant="title3" weight="medium">
49
51
  {unitAlignment === RIGHT_UNIT_ALIGNMENT
50
- ? consumptionValue
51
- : `${unit || consumptionUnit}${consumptionValue}`}
52
+ ? totalConsumptionFormatted
53
+ : `${unit || consumptionUnit}${totalConsumptionFormatted}`}
52
54
  </UTLabel>
53
55
  {unitAlignment === RIGHT_UNIT_ALIGNMENT && (
54
56
  <UTLabel variant="subtitle1" weight="medium">
@@ -1,7 +1,9 @@
1
1
  import { VARIANT } from './constants';
2
2
 
3
3
  const getValue = (smartBill, magnitudeName) => {
4
- const item = smartBill?.periods?.[0]?.consumptions?.find(c => c.magnitude === magnitudeName);
4
+ const item = smartBill?.periods
5
+ ?.find(period => period.current)
6
+ ?.consumptions?.find(c => c.magnitude === magnitudeName);
5
7
  return item ? { value: item.value || 0, unit: item.unit || '' } : { value: 0, unit: '' };
6
8
  };
7
9