@eohjsc/react-native-smart-city 0.3.87 → 0.3.89

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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/commons/Device/FlatListItems.js +1 -1
  3. package/src/commons/Device/HistoryChart.js +2 -2
  4. package/src/commons/Device/LinearChart.js +1 -0
  5. package/src/commons/Device/PMSensor/PMSensorIndicator.js +1 -5
  6. package/src/commons/Device/WindDirection/Compass/index.js +1 -0
  7. package/src/commons/MenuActionAddnew/__test__/MenuActionAddNew.test.js +1 -1
  8. package/src/commons/MenuActionList/__test__/MenuActionList.test.js +1 -1
  9. package/src/commons/ModalPopupCT/index.js +31 -25
  10. package/src/commons/SubUnit/OneTap/index.js +2 -1
  11. package/src/commons/UnitSummary/AirQuality/index.js +1 -1
  12. package/src/configs/API.js +1 -1
  13. package/src/configs/AccessibilityLabel.js +3 -0
  14. package/src/context/SCContext.tsx +1 -1
  15. package/src/screens/AddLocationMaps/index.js +5 -4
  16. package/src/screens/AllGateway/hooks/__test__/index.test.js +26 -2
  17. package/src/screens/AllGateway/hooks/useGateway.js +11 -9
  18. package/src/screens/Automate/MultiUnits.js +1 -0
  19. package/src/screens/Automate/index.js +1 -2
  20. package/src/screens/Device/detail.js +1 -1
  21. package/src/screens/Sharing/InfoMemberUnit.js +41 -8
  22. package/src/screens/Sharing/Styles/inforMemberUnitStyles.js +11 -0
  23. package/src/screens/Sharing/__test__/InfoMemberUnit.test.js +62 -2
  24. package/src/screens/SubUnit/AddSubUnit.js +8 -5
  25. package/src/screens/Unit/ChooseLocationStyles.js +1 -0
  26. package/src/screens/Unit/Station/index.js +2 -1
  27. package/src/screens/Unit/Summaries.js +5 -1
  28. package/src/screens/Unit/hook/__test__/useUnitConnectRemoteDevices.test.js +57 -0
  29. package/src/utils/I18n/translations/en.json +4 -1
  30. package/src/utils/I18n/translations/vi.json +3 -0
  31. package/src/utils/Utils.js +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eohjsc/react-native-smart-city",
3
3
  "title": "React Native Smart Home",
4
- "version": "0.3.87",
4
+ "version": "0.3.89",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -46,7 +46,7 @@ const FlatListItems = memo(({ data, style, title, offsetTitle }) => {
46
46
  }
47
47
  return items.map((item, index) => (
48
48
  <QualityIndicatorItem
49
- key={item?.id}
49
+ key={(item?.id || index).toString()}
50
50
  standard={item.standard}
51
51
  value={item.value}
52
52
  measure={item.unit}
@@ -162,7 +162,7 @@ const HistoryChart = memo(
162
162
  return roundedSum.toFixed();
163
163
  }, [configuration, datas, chartConfig]);
164
164
 
165
- const renderChart = useCallback(() => {
165
+ const renderChart = useMemo(() => {
166
166
  if (configuration.type === 'line_chart') {
167
167
  return (
168
168
  <View style={styles.chartContainer}>
@@ -228,7 +228,7 @@ const HistoryChart = memo(
228
228
  </View>
229
229
  </View>
230
230
  )}
231
- {renderChart()}
231
+ {renderChart}
232
232
  {configuration.config === 'power_consumption' && !!chartConfig.price && (
233
233
  <Text type="H4">
234
234
  {t('total_power_price')} <Text bold>{formatMoney(totalPrice)}</Text>
@@ -108,6 +108,7 @@ function LinearChart({ datas }) {
108
108
 
109
109
  return (
110
110
  <View style={styles.container}>
111
+ {/* // TODO : Hinh will research to fix warning Highchart*/}
111
112
  <HighchartsReactNative
112
113
  styles={styles.chartStyle}
113
114
  webviewStyles={styles.webviewStyle}
@@ -3,12 +3,8 @@ import { View } from 'react-native';
3
3
  import { FlatList } from 'react-native';
4
4
  import QualityIndicatorItem from '../WaterQualitySensor/QualityIndicatorsItem';
5
5
  import styles from './PMSensorIndicatorStyles';
6
- import { roundNumber } from '../../../utils/Utils';
6
+ import { keyExtractor, roundNumber } from '../../../utils/Utils';
7
7
 
8
- //using for PM2.5-10, CO, UV, Rainflow Sensor
9
- const keyExtractor = (item, index) => {
10
- return index;
11
- };
12
8
  const PMSensorIndicator = memo(({ data = [], style }) => {
13
9
  const renderItem = useCallback(
14
10
  ({ item }) => {
@@ -41,6 +41,7 @@ const Compass = memo(({ data = [], ...props }) => {
41
41
  toValue: value,
42
42
  duration: 200,
43
43
  easing: Easing.linear,
44
+ useNativeDriver: false,
44
45
  }).start();
45
46
  }, [currentRotation, value]);
46
47
 
@@ -17,7 +17,7 @@ describe('Test MenuActionAddNew', () => {
17
17
  let wrapper;
18
18
 
19
19
  // flat list issue
20
- it.skip('onItemClick MenuActionAddNew', async () => {
20
+ it('onItemClick MenuActionAddNew', async () => {
21
21
  const mockFunc = jest.fn();
22
22
  await act(async () => {
23
23
  wrapper = create(wrapComponent(dataActions));
@@ -9,7 +9,7 @@ describe('Test MenuActionList', () => {
9
9
  const listItem = [item(1)];
10
10
  let wrapper;
11
11
 
12
- it.skip('onItemClick MenuActionList', async () => {
12
+ it('onItemClick MenuActionList', async () => {
13
13
  const mockOnItemClick = jest.fn();
14
14
  const mockHideModal = jest.fn();
15
15
 
@@ -20,6 +20,8 @@ const ModalPopupCT = ({
20
20
  isVisible = false,
21
21
  onPressConfirm,
22
22
  onPressCancel,
23
+ textShowAlert,
24
+ footer,
23
25
  }) => {
24
26
  const [isChecked, setIsChecked] = useState(false);
25
27
  const isUnHideButton = useMemo(() => {
@@ -59,7 +61,7 @@ const ModalPopupCT = ({
59
61
  <View style={styles.viewIconAlert}>{iconAlert}</View>
60
62
  <View style={styles.viewTextAlert}>
61
63
  <Text type="H4" color={Colors.Gray9}>
62
- {t('text_alert_modal_popup_ct')}
64
+ {textShowAlert || t('text_alert_modal_popup_ct')}
63
65
  </Text>
64
66
  </View>
65
67
  </View>
@@ -94,41 +96,45 @@ const ModalPopupCT = ({
94
96
  </View>
95
97
  )}
96
98
  </View>
97
- <View style={styles.footer}>
98
- <View style={styles.buttonBottom}>
99
- <TouchableOpacity
100
- onPress={onPressCancel}
101
- style={styles.buttonCancel}
102
- >
103
- <Text
104
- type="H4"
105
- bold
106
- color={Colors.Gray9}
107
- style={styles.paddingBottom3}
108
- >
109
- {t('cancel')}
110
- </Text>
111
- </TouchableOpacity>
112
- <View style={styles.buttonConfirm}>
99
+ {footer || (
100
+ <View style={styles.footer}>
101
+ <View style={styles.buttonBottom}>
113
102
  <TouchableOpacity
114
- onPress={onPressConfirm}
115
- disabled={isUnHideButton ? !isUnHideButton : !isChecked}
103
+ onPress={onPressCancel}
104
+ style={styles.buttonCancel}
116
105
  >
117
106
  <Text
118
107
  type="H4"
119
108
  bold
120
- color={Colors.White}
109
+ color={Colors.Gray9}
121
110
  style={styles.paddingBottom3}
122
111
  >
123
- {t((type === 'reboot' && 'reboot') || 'delete')}
112
+ {t('cancel')}
124
113
  </Text>
125
114
  </TouchableOpacity>
126
- <View
127
- style={isUnHideButton ? {} : !isChecked && styles.disableButton}
128
- />
115
+ <View style={styles.buttonConfirm}>
116
+ <TouchableOpacity
117
+ onPress={onPressConfirm}
118
+ disabled={isUnHideButton ? !isUnHideButton : !isChecked}
119
+ >
120
+ <Text
121
+ type="H4"
122
+ bold
123
+ color={Colors.White}
124
+ style={styles.paddingBottom3}
125
+ >
126
+ {t((type === 'reboot' && 'reboot') || 'delete')}
127
+ </Text>
128
+ </TouchableOpacity>
129
+ <View
130
+ style={
131
+ isUnHideButton ? {} : !isChecked && styles.disableButton
132
+ }
133
+ />
134
+ </View>
129
135
  </View>
130
136
  </View>
131
- </View>
137
+ )}
132
138
  </View>
133
139
  </ModalCustom>
134
140
  );
@@ -49,6 +49,7 @@ const SubUnitAutomate = ({ isOwner, listAutomate, unit, wrapItemStyle }) => {
49
49
  >
50
50
  {listAutomate.map((item, index) => (
51
51
  <TouchableOpacity
52
+ key={(item?.id || index).toString()}
52
53
  style={
53
54
  indexAutomate === index
54
55
  ? styles.borderSelection
@@ -79,7 +80,7 @@ const SubUnitAutomate = ({ isOwner, listAutomate, unit, wrapItemStyle }) => {
79
80
  isOwner={isOwner}
80
81
  automate={item}
81
82
  unit={unit}
82
- key={item.id}
83
+ key={item.id.toString()}
83
84
  />
84
85
  ))}
85
86
  <ItemAddNew
@@ -146,7 +146,7 @@ const AirQuality = memo(({ summaryDetail }) => {
146
146
  {!!advices &&
147
147
  advices.map((item, index) => {
148
148
  return (
149
- <View key={index} style={styles.boxContentHealth}>
149
+ <View key={index.toString()} style={styles.boxContentHealth}>
150
150
  <View
151
151
  style={[styles.boxDot, { backgroundColor: outdoorColor }]}
152
152
  />
@@ -4,7 +4,7 @@ const API = {
4
4
  UNIT: {
5
5
  MY_UNITS: () => '/property_manager/units/mine/',
6
6
  SHARED_UNITS: () => '/property_manager/shared_units/',
7
- CREATE_UNIT: () => '/property_manager/units/',
7
+ CREATE_UNIT: () => '/property_manager/iot_dashboard/dev_mode/units/',
8
8
  UNIT_DETAIL: (id) => `/property_manager/units/${id}/`,
9
9
  UNITS_PUBLIC: () => '/property_manager/units/public/',
10
10
  UNIT_SUMMARY: (id) => `/property_manager/units/${id}/summary/`,
@@ -675,4 +675,7 @@ export default {
675
675
  ROW_ITEM_DEVMOD: 'ROW_ITEM_DEVMOD',
676
676
  BUTTON_UNDERSTAND: 'BUTTON_UNDERSTAND',
677
677
  MODAL_POPUP_CUSTOM: 'MODAL_POPUP_CUSTOM',
678
+
679
+ //MemberInfo
680
+ CHANGE_ROLE: 'CHANGE_ROLE',
678
681
  };
@@ -91,7 +91,7 @@ export const SCWrapper = (props) => {
91
91
  return (
92
92
  <>
93
93
  <SCProvider {...props} />
94
- <Toast config={toastConfig} ref={(ref) => Toast.setRef(ref)} />
94
+ <Toast config={toastConfig} />
95
95
  </>
96
96
  );
97
97
  };
@@ -41,10 +41,11 @@ const AddLocationMaps = memo(() => {
41
41
 
42
42
  const onDone = useCallback(() => {
43
43
  navigate(Routes.AddSubUnit, {
44
- location: input,
44
+ location: searchedLocation,
45
45
  isAddUnit: true,
46
46
  });
47
- }, [input, navigate]);
47
+ }, [searchedLocation, navigate]);
48
+
48
49
  const onBack = useCallback(() => {
49
50
  goBack();
50
51
  }, [goBack]);
@@ -95,7 +96,7 @@ const AddLocationMaps = memo(() => {
95
96
  }, []);
96
97
 
97
98
  const onPressRowLocation = useCallback(async (item) => {
98
- setInput(item.description);
99
+ setInput(item);
99
100
  setSearchData([]);
100
101
  const body = {
101
102
  params: {
@@ -111,7 +112,7 @@ const AddLocationMaps = memo(() => {
111
112
  if (success) {
112
113
  const { location } = data.result.geometry;
113
114
  setSearchedLocation({
114
- description: item.description,
115
+ description: item?.description,
115
116
  latitude: location.lat,
116
117
  longitude: location.lng,
117
118
  });
@@ -17,13 +17,15 @@ jest.mock('@react-navigation/native', () => {
17
17
  });
18
18
  describe('Test useGateway', () => {
19
19
  let hook;
20
+
20
21
  beforeEach(async () => {
22
+ mock.reset();
21
23
  await act(() => {
22
24
  hook = renderHook(() => useGateway());
23
25
  });
24
26
  });
27
+
25
28
  afterEach(() => {
26
- mock.reset();
27
29
  Toast.show.mockClear();
28
30
  });
29
31
 
@@ -46,9 +48,17 @@ describe('Test useGateway', () => {
46
48
  .reply(200, { results: [{ id: 1, name: 'chip 1' }] });
47
49
 
48
50
  await act(async () => {
49
- await hook.result.current.fetchDataGateways();
51
+ await hook.result.current.fetchDataGateways(1, 1);
50
52
  });
51
53
  expect(hook.result.current.gateways).toEqual([{ id: 1, name: 'chip 1' }]);
54
+
55
+ await act(async () => {
56
+ await hook.result.current.fetchDataGateways(2, 1);
57
+ });
58
+ expect(hook.result.current.gateways).toEqual([
59
+ { id: 1, name: 'chip 1' },
60
+ { id: 1, name: 'chip 1' },
61
+ ]);
52
62
  });
53
63
 
54
64
  it('test useGateway fetchDevicesGateway isInterval', async () => {
@@ -103,4 +113,18 @@ describe('Test useGateway', () => {
103
113
  visibilityTime: 1000,
104
114
  });
105
115
  });
116
+
117
+ it('test fetchActionConfigDevice', async () => {
118
+ mock.onDelete(API.DEV_MODE.ZIGBEE.CONFIG_MAP(1, 1)).reply(200);
119
+ await act(async () => {
120
+ await hook.result.current.fetchActionConfigDevice(1, 1, true, false);
121
+ });
122
+ expect(hook.result.current.detailDeviceZigbee).toEqual({});
123
+
124
+ mock.onDelete(API.DEV_MODE.ZIGBEE.ACTION(1, 1)).reply(200);
125
+ await act(async () => {
126
+ await hook.result.current.fetchActionConfigDevice(1, 1, true, true);
127
+ });
128
+ expect(hook.result.current.detailDeviceZigbee).toEqual({});
129
+ });
106
130
  });
@@ -68,7 +68,9 @@ export const useGateway = () => {
68
68
  const config_read = data
69
69
  ?.filter(
70
70
  (item) =>
71
- item?.config?.is_read && item?.value_type !== VIRTUAL_TYPE
71
+ !item?.config.is_write &&
72
+ item?.config?.is_read &&
73
+ item?.value_type !== VIRTUAL_TYPE
72
74
  )
73
75
  ?.map((item) => {
74
76
  return {
@@ -86,12 +88,10 @@ export const useGateway = () => {
86
88
  });
87
89
  const config_virtual = data
88
90
  ?.filter((item) => item?.value_type === VIRTUAL_TYPE)
89
- ?.map((item) => {
90
- return {
91
- ...item,
92
- pin_number: item?.pin_number.toString(),
93
- };
94
- });
91
+ ?.map((item) => ({
92
+ ...item,
93
+ pin_number: item?.pin_number.toString(),
94
+ }));
95
95
 
96
96
  setDetailDeviceInternal((prev) => ({
97
97
  ...prev,
@@ -164,8 +164,10 @@ export const useGateway = () => {
164
164
  );
165
165
  success && setGatewayDevices((prev) => ({ ...prev, modbus: [] }));
166
166
  }
167
- success && ToastBottomHelper.success(t('delete_successfully'));
168
- success && navigation.pop(numberGoBack);
167
+ if (success) {
168
+ ToastBottomHelper.success(t('delete_successfully'));
169
+ navigation.pop(numberGoBack);
170
+ }
169
171
  },
170
172
  [navigation]
171
173
  );
@@ -131,6 +131,7 @@ const MultiUnits = () => {
131
131
  <View style={styles.wrapItem}>
132
132
  {sortedListItems.map((item, index) => (
133
133
  <ItemOneTap
134
+ key={(item?.id || index).toString()}
134
135
  isOwner={isOwner}
135
136
  automate={item}
136
137
  wrapSyles={[
@@ -25,8 +25,7 @@ import {
25
25
  AccessibilityLabel,
26
26
  UNIT_TYPES,
27
27
  } from '../../configs/Constants';
28
-
29
- const keyExtractor = (item) => item.id;
28
+ import { keyExtractor } from '../../utils/Utils';
30
29
 
31
30
  const Automate = () => {
32
31
  const t = useTranslations();
@@ -706,7 +706,7 @@ const DeviceDetail = ({ route }) => {
706
706
  return (
707
707
  <SensorDisplayItem
708
708
  accessibilityLabel={AccessibilityLabel.SENSOR_DISPLAY_ITEM}
709
- key={index}
709
+ key={(item?.id || index).toString()}
710
710
  item={item}
711
711
  evaluate={evaluate}
712
712
  emergency={onEmergencyButtonPress}
@@ -9,7 +9,7 @@ import { HeaderCustom } from '../../commons/Header';
9
9
  import RowMemberInfo from '../GuestInfo/components/RowGuestInfo';
10
10
  import { useIsOwnerOfUnit } from '../../hooks/Common';
11
11
  import { axiosGet } from '../../utils/Apis/axios';
12
- import { AlertAction } from '../../commons';
12
+ import { AlertAction, ViewButtonBottom } from '../../commons';
13
13
  import { useStateAlertAction, useDataMember } from './hooks';
14
14
  import ItemChangeRole from './Components/ItemChangeRole';
15
15
  import MemberSvg from '../../Images/Common/member.svg';
@@ -18,6 +18,7 @@ import styles from './Styles/inforMemberUnitStyles';
18
18
  import { useNavigation, useIsFocused } from '@react-navigation/native';
19
19
  import Routes from '../../utils/Route';
20
20
  import { AccessibilityLabel } from '../../configs/Constants';
21
+ import ModalPopupCT from '../../commons/ModalPopupCT';
21
22
 
22
23
  const InfoMemberUnit = memo(({ route }) => {
23
24
  const t = useTranslations();
@@ -28,6 +29,7 @@ const InfoMemberUnit = memo(({ route }) => {
28
29
  };
29
30
  const { navigate } = useNavigation();
30
31
  const [isLoading, setIsLoading] = useState(true);
32
+ const [isShowWarning, setIsShowWarning] = useState(false);
31
33
  const [memberInfo, setMemberInfo] = useState({});
32
34
  const [itemSelected, setItemSelected] = useState({});
33
35
  const isFocused = useIsFocused();
@@ -56,10 +58,7 @@ const InfoMemberUnit = memo(({ route }) => {
56
58
  if (stateAlertAction?.is_change) {
57
59
  hideAlertAction();
58
60
  if (itemSelected?.role?.is_owner) {
59
- navigate(Routes.EnterPassword, {
60
- dataParams: { unit_id: unit?.id, member },
61
- type: 'infoMemberUnit',
62
- });
61
+ setIsShowWarning(true);
63
62
  }
64
63
  } else {
65
64
  removeMember(
@@ -72,14 +71,19 @@ const InfoMemberUnit = memo(({ route }) => {
72
71
  }, [
73
72
  hideAlertAction,
74
73
  itemSelected?.role?.is_owner,
75
- member,
76
- navigate,
77
74
  removeMember,
78
75
  stateAlertAction?.is_change,
79
76
  stateAlertAction?.member,
80
- unit?.id,
81
77
  ]);
82
78
 
79
+ const handleChangeOwner = useCallback(() => {
80
+ setIsShowWarning(false);
81
+ navigate(Routes.EnterPassword, {
82
+ dataParams: { unit_id: unit?.id, member },
83
+ type: 'infoMemberUnit',
84
+ });
85
+ }, [member, navigate, unit?.id]);
86
+
83
87
  const fetchMemberInfo = useCallback(async () => {
84
88
  setIsLoading(true);
85
89
  const { success, data } = await axiosGet(
@@ -158,6 +162,22 @@ const InfoMemberUnit = memo(({ route }) => {
158
162
  return <></>;
159
163
  }, [isOwner, navigate, unit?.id, memberInfo]);
160
164
 
165
+ const footerWarning = useMemo(() => {
166
+ return (
167
+ <>
168
+ <View style={styles.line} />
169
+ <ViewButtonBottom
170
+ leftTitle={t('cancel')}
171
+ rightTitle={t('change')}
172
+ styleButton={styles.styleButton}
173
+ styleButtonLeftText={styles.textButton}
174
+ onLeftClick={() => setIsShowWarning(false)}
175
+ onRightClick={handleChangeOwner}
176
+ />
177
+ </>
178
+ );
179
+ }, [handleChangeOwner, t]);
180
+
161
181
  useEffect(() => {
162
182
  if (isIdentityOwner) {
163
183
  setItemSelected(itemsRoleModal[0]);
@@ -223,6 +243,7 @@ const InfoMemberUnit = memo(({ route }) => {
223
243
  <TouchableOpacity
224
244
  onPress={onPressChangeRole}
225
245
  style={styles.role}
246
+ accessibilityLabel={AccessibilityLabel.CHANGE_ROLE}
226
247
  >
227
248
  <View style={styles.leftRole}>
228
249
  <Text type="Body" color={Colors.Gray7}>
@@ -271,6 +292,18 @@ const InfoMemberUnit = memo(({ route }) => {
271
292
  >
272
293
  <ChangeRoleContent />
273
294
  </AlertAction>
295
+
296
+ <ModalPopupCT
297
+ isVisible={isShowWarning}
298
+ title={t('transfer_ownership')}
299
+ subTitle={t('text_change_owner')}
300
+ isShowAlert
301
+ isShowUnderstand={false}
302
+ textShowAlert={t('text_alert_change_owner')}
303
+ styleWrapButton={styles.wrapButton}
304
+ styleRightButton={styles.button}
305
+ footer={footerWarning}
306
+ />
274
307
  </>
275
308
  );
276
309
  });
@@ -89,4 +89,15 @@ export default StyleSheet.create({
89
89
  removeButtonStyle: {
90
90
  color: Colors.Red,
91
91
  },
92
+ line: {
93
+ borderTopWidth: 1,
94
+ borderTopColor: Colors.Neutral.Neutral3,
95
+ },
96
+ styleButton: {
97
+ paddingTop: 24,
98
+ paddingBottom: 0,
99
+ },
100
+ textButton: {
101
+ color: Colors.Gray9,
102
+ },
92
103
  });
@@ -7,10 +7,13 @@ import InfoMemberUnit from '../InfoMemberUnit';
7
7
  import { HeaderCustom } from '../../../commons/Header';
8
8
  import { SCProvider } from '../../../context';
9
9
  import { mockSCStore } from '../../../context/mockStore';
10
- import { AlertAction } from '../../../commons';
10
+ import { AlertAction, ViewButtonBottom } from '../../../commons';
11
11
  import API from '../../../configs/API';
12
12
  import { AccessibilityLabel } from '../../../configs/Constants';
13
13
  import api from '../../../utils/Apis/axios';
14
+ import ItemChangeRole from '../Components/ItemChangeRole';
15
+ import ModalPopupCT from '../../../commons/ModalPopupCT';
16
+ import Routes from '../../../utils/Route';
14
17
 
15
18
  const mock = new MockAdapter(api.axiosInstance);
16
19
 
@@ -53,6 +56,7 @@ describe('Test InfoMemberUnit', () => {
53
56
  unit: {
54
57
  id: 1,
55
58
  name: 'unit',
59
+ user_id: 1,
56
60
  },
57
61
  member: {
58
62
  id: 1,
@@ -107,7 +111,7 @@ describe('Test InfoMemberUnit', () => {
107
111
 
108
112
  it('render InfoMemberUnit delete member', async () => {
109
113
  mock.onGet(API.SHARE.UNIT_MEMBER_INFO(1, 1)).reply(200, {
110
- id: 1,
114
+ id: 2,
111
115
  identity: 'owner',
112
116
  phone_number: '88888',
113
117
  name: 'user',
@@ -131,4 +135,60 @@ describe('Test InfoMemberUnit', () => {
131
135
  });
132
136
  expect(mockGoBack).toBeCalled();
133
137
  });
138
+
139
+ it('test change owner', async () => {
140
+ mock.onGet(API.SHARE.UNIT_MEMBER_INFO(1, 1)).reply(200, {
141
+ id: 2,
142
+ identity: 'owner',
143
+ phone_number: '88888',
144
+ name: 'user',
145
+ email: 'abc@gmail.com',
146
+ });
147
+ await act(async () => {
148
+ tree = await create(wrapComponent(route));
149
+ });
150
+ const instance = tree.root;
151
+
152
+ const touchChangeRole = instance.findByProps({
153
+ accessibilityLabel: AccessibilityLabel.CHANGE_ROLE,
154
+ });
155
+ await act(async () => {
156
+ await touchChangeRole.props.onPress();
157
+ });
158
+
159
+ const alertAction = instance.findByType(AlertAction);
160
+ expect(alertAction.props.visible).toEqual(true);
161
+
162
+ const itemChangeRole = instance.findAllByType(ItemChangeRole);
163
+ expect(itemChangeRole).toHaveLength(2);
164
+
165
+ const modalPopupCT = instance.findByType(ModalPopupCT);
166
+ await act(async () => {
167
+ await itemChangeRole[0].props.onPress();
168
+ });
169
+ await act(async () => {
170
+ await alertAction.props.rightButtonClick();
171
+ });
172
+ expect(alertAction.props.visible).toEqual(false);
173
+ expect(modalPopupCT.props.isVisible).toEqual(true);
174
+
175
+ const viewButtonBottom = instance.findAllByType(ViewButtonBottom);
176
+ expect(viewButtonBottom).toHaveLength(2);
177
+
178
+ await act(async () => {
179
+ await viewButtonBottom[1].props.onRightClick();
180
+ });
181
+ expect(modalPopupCT.props.isVisible).toEqual(false);
182
+ expect(mockedNavigate).toHaveBeenCalledWith(Routes.EnterPassword, {
183
+ dataParams: {
184
+ unit_id: 1,
185
+ member: {
186
+ id: 1,
187
+ name: 'user1',
188
+ share_id: 1,
189
+ },
190
+ },
191
+ type: 'infoMemberUnit',
192
+ });
193
+ });
134
194
  });
@@ -29,7 +29,7 @@ const AddSubUnit = ({ route }) => {
29
29
  unit,
30
30
  addType,
31
31
  isAddUnit,
32
- location = '',
32
+ location = {},
33
33
  isInsideUnit,
34
34
  } = route?.params;
35
35
  const [roomName, setRoomName] = useState('');
@@ -50,8 +50,10 @@ const AddSubUnit = ({ route }) => {
50
50
  awaitCreate.current = true;
51
51
  const dataObj = {
52
52
  name: roomName,
53
- address: location,
53
+ address: location.description,
54
54
  background: wallpaper,
55
+ lat: location.latitude,
56
+ lng: location.longitude,
55
57
  };
56
58
  const formData = createFormData(dataObj, ['background']);
57
59
  const { success, data } = await axiosPost(
@@ -161,7 +163,7 @@ const AddSubUnit = ({ route }) => {
161
163
 
162
164
  const validateData = useMemo(() => {
163
165
  if (isAddUnit) {
164
- return roomName === '' || wallpaper === '' || location === '';
166
+ return roomName === '' || wallpaper === '' || location.description === '';
165
167
  } else {
166
168
  return roomName === '' || wallpaper === '';
167
169
  }
@@ -212,9 +214,10 @@ const AddSubUnit = ({ route }) => {
212
214
  <Text style={styles.addWallpaper}>{t('geolocation')}</Text>
213
215
  <Text
214
216
  style={styles.textLocation}
215
- color={location && Colors.Primary}
217
+ color={location.description && Colors.Primary}
216
218
  >
217
- {location || t('text_explain_add_geolocation')}
219
+ {location.description ||
220
+ t('text_explain_add_geolocation')}
218
221
  </Text>
219
222
  </View>
220
223
  </TouchableWithoutFeedback>
@@ -14,6 +14,7 @@ export default StyleSheet.create({
14
14
  },
15
15
  bottomButton: {
16
16
  backgroundColor: Colors.White,
17
+ marginBottom: 100,
17
18
  },
18
19
  wrapMarker: {
19
20
  zIndex: 3,
@@ -1,6 +1,7 @@
1
1
  import React, { memo, useEffect, useRef, useState } from 'react';
2
2
  import { View, Text, TouchableOpacity, FlatList } from 'react-native';
3
3
  import { AccessibilityLabel } from '../../../configs/Constants';
4
+ import { keyExtractor } from '../../../utils/Utils';
4
5
  import styles from './StationStyles';
5
6
 
6
7
  const Station = ({ listStation = [], onSnapToItem, indexStation }) => {
@@ -63,10 +64,10 @@ const Station = ({ listStation = [], onSnapToItem, indexStation }) => {
63
64
  return (
64
65
  <View style={styles.container}>
65
66
  <FlatList
67
+ keyExtractor={keyExtractor}
66
68
  removeClippedSubviews={false}
67
69
  onScroll={onScroll}
68
70
  ref={flatListRef}
69
- keyExtractor={(item) => item.id}
70
71
  horizontal
71
72
  data={data}
72
73
  extraData={data}
@@ -107,7 +107,11 @@ const Summaries = memo(({ unit }) => {
107
107
  accessibilityLabel={AccessibilityLabel.UNIT_DETAIL_UNIT_SUMMARY_VIEW}
108
108
  >
109
109
  {unitSummaries.map((item, index) => (
110
- <SummaryItem key={index} item={item} goToSummary={goToSummary} />
110
+ <SummaryItem
111
+ key={(item?.id || index).toString()}
112
+ item={item}
113
+ goToSummary={goToSummary}
114
+ />
111
115
  ))}
112
116
  </ScrollView>
113
117
  )}
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-hooks';
3
+ import { SCProvider } from '../../../../context';
4
+ import { mockSCStore } from '../../../../context/mockStore';
5
+ import { useUnitConnectRemoteDevices } from '../useUnitConnectRemoteDevices';
6
+
7
+ const mockSetAction = jest.fn();
8
+
9
+ const wrapper = ({ children }) => <SCProvider>{children}</SCProvider>;
10
+
11
+ jest.mock('react', () => {
12
+ return {
13
+ ...jest.requireActual('react'),
14
+ useContext: () => ({
15
+ setAction: mockSetAction,
16
+ stateData: mockSCStore({
17
+ app: {
18
+ isNetworkConnected: true,
19
+ },
20
+ bluetooth: {
21
+ isEnabled: true,
22
+ },
23
+ iot: {
24
+ lgthinq: {
25
+ unitConnected: [
26
+ {
27
+ id: 1,
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ }),
33
+ }),
34
+ };
35
+ });
36
+
37
+ describe('Test useUnitConnectRemoteDevices', () => {
38
+ const unit = {
39
+ id: 1,
40
+ remote_control_options: {
41
+ lg_thinq: [
42
+ {
43
+ id: 1,
44
+ lg_devices: [{ device_id: 1, configs: [{ id: 1, name: 'name' }] }],
45
+ },
46
+ ],
47
+ bluetooth: [1],
48
+ googlehome: [1],
49
+ },
50
+ };
51
+ it('test handleLgThinqConnect', async () => {
52
+ renderHook(() => useUnitConnectRemoteDevices(unit), {
53
+ wrapper,
54
+ });
55
+ expect(mockSetAction).not.toBeCalled();
56
+ });
57
+ });
@@ -675,7 +675,7 @@
675
675
  "text_notification_content_turbility_high": "The Turbility at **unit_name** is **status**. Water is not safe to drink.",
676
676
  "text_notification_content_smoke": "Smoke appears in **unit_name**. Please check your home and call the rescue team if there is a fire.",
677
677
  "text_notification_content_fire": "There is a fire at **unit_name**, Please move out of the house immediately and call the rescue team.",
678
- "text_notification_content_active_sos": "**device_name** is activating at **unit_name** - **station_name**, Please check it NOW.",
678
+ "text_notification_content_active_sos": "**device_name** is activating at **unit_name** - **station_name**. Please check it NOW.",
679
679
  "text_notification_content_replace_water_filter": "Filter **config_name** of **device_name** in **station_name** has less than 10h remaining. Check and replace now.",
680
680
  "text_notification_content_low_battery": "Battery of **device_name** in **unit_name**: **station_name** has less than 20%. Check and replace now.",
681
681
  "text_notification_content_remove_unit_to_owner": "Unit **unit_name** has been removed successfully.",
@@ -1125,5 +1125,8 @@
1125
1125
  "data_address": "Data address",
1126
1126
  "methods": "{number} methods",
1127
1127
  "io_method": "Input/Output method",
1128
+ "transfer_ownership": "Transfer ownership",
1129
+ "text_change_owner": "Ownership permissions of this unit to the user you have selected.",
1130
+ "text_alert_change_owner": "You will be removed from this Unit after transfer ownership.",
1128
1131
  "the_system_has_been_upgraded" : "To provide a better user experience, [Era] will perform a system upgrade.The upgrade will take about 10 minutes.\n\n Some device control functions will be interrupted during the upgrade.[Era] would like to ask for your understanding for this inconvenience.\n\nBest regards."
1129
1132
  }
@@ -1124,6 +1124,9 @@
1124
1124
  "data_address": "Địa chỉ dữ liệu",
1125
1125
  "methods": "{number} phương thức",
1126
1126
  "io_method": "Giao thức Input/Output",
1127
+ "transfer_ownership": "Chuyển quyền sở hữu",
1128
+ "text_change_owner": "Quyền sở hữu đơn vị này cho người dùng bạn đã chọn.",
1129
+ "text_alert_change_owner": "Bạn sẽ bị xóa khỏi Unit này sau khi chuyển quyền sở hữu.",
1127
1130
  "the_system_has_been_upgraded" : "Để mang lại trải nghiệm tốt hơn cho người dùng, [Era] sẽ tiến hành nâng cấp hệ thống.Việc nâng cấp sẽ diễn ra trong khoảng 10 phút.\n\nMột số chức năng điều khiển thiết bị sẽ bị gián đoạn trong thời gian nâng cấp.[Era] kính mong quý khách hàng thông cảm cho sự bất tiện này.\n\nTrân trọng "
1128
1131
 
1129
1132
  }
@@ -230,6 +230,8 @@ export const PIN_MAPPING = {
230
230
  },
231
231
  };
232
232
 
233
+ export const keyExtractor = (item, index) => (item?.id || index).toString();
234
+
233
235
  export default {
234
236
  validateEmail,
235
237
  isObjectEmpty,