@eohjsc/react-native-smart-city 0.2.92 → 0.2.93

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 (73) hide show
  1. package/assets/images/Device/current-state.svg +3 -0
  2. package/assets/images/Device/door-state.svg +3 -0
  3. package/assets/images/Device/wind-strength.svg +12 -0
  4. package/package.json +1 -1
  5. package/src/commons/Action/ItemQuickAction.js +1 -0
  6. package/src/commons/Action/__test__/ItemQuickAction.test.js +49 -2
  7. package/src/commons/ActionGroup/OnOffTemplate/OnOffSimpleTemplateStyle.js +2 -1
  8. package/src/commons/ActionGroup/OptionsDropdownActionTemplate.js +31 -11
  9. package/src/commons/ActionGroup/OptionsDropdownActionTemplateStyle.js +5 -2
  10. package/src/commons/ActionGroup/TimerActionTemplate.js +14 -10
  11. package/src/commons/ActionGroup/TimerActionTemplateStyles.js +12 -0
  12. package/src/commons/ActionGroup/TwoButtonTemplate/TwoButtonTemplateStyles.js +55 -0
  13. package/src/commons/ActionGroup/TwoButtonTemplate/index.js +170 -0
  14. package/src/commons/ActionGroup/__test__/TimerActionTemplate.test.js +1 -1
  15. package/src/commons/ActionGroup/__test__/TwoButtonTemplate.test.js +112 -0
  16. package/src/commons/ActionGroup/index.js +3 -0
  17. package/src/commons/CameraDevice/index.js +6 -1
  18. package/src/commons/Device/HistoryChart.js +2 -2
  19. package/src/commons/Device/ItemDevice.js +3 -13
  20. package/src/commons/IconComponent/index.js +32 -26
  21. package/src/commons/MediaPlayerDetail/index.js +16 -4
  22. package/src/commons/SubUnit/Favorites/index.js +8 -7
  23. package/src/commons/SubUnit/__test__/Favorites.test.js +33 -35
  24. package/src/configs/API.js +4 -0
  25. package/src/configs/Constants.js +21 -0
  26. package/src/context/actionType.ts +17 -0
  27. package/src/context/mockStore.ts +18 -0
  28. package/src/context/reducer.ts +102 -0
  29. package/src/iot/RemoteControl/Bluetooth.js +2 -0
  30. package/src/iot/RemoteControl/GoogleHome.js +1 -0
  31. package/src/navigations/AutomateStack.js +16 -1
  32. package/src/navigations/UnitStack.js +27 -0
  33. package/src/screens/AddNewAction/Device/__test__/index.test.js +1 -1
  34. package/src/screens/AddNewAction/SelectAction.js +13 -15
  35. package/src/screens/AddNewAction/__test__/SelectAction.test.js +0 -7
  36. package/src/screens/AddNewGateway/PlugAndPlay/ConnectWifiWarning.js +2 -0
  37. package/src/screens/AddNewGateway/PlugAndPlay/GatewayWifiList.js +2 -0
  38. package/src/screens/AddNewGateway/PlugAndPlay/__test__/ConnectWifiWarning.test.js +9 -0
  39. package/src/screens/AddNewGateway/PlugAndPlay/__test__/GatewayWifiList.test.js +15 -0
  40. package/src/screens/AddNewGateway/SetupGatewayWifi.js +6 -1
  41. package/src/screens/AddNewGateway/__test__/SetupGateway.test.js +34 -0
  42. package/src/screens/AllCamera/index.js +1 -0
  43. package/src/screens/Automate/MultiUnits.js +9 -9
  44. package/src/screens/Automate/index.js +21 -20
  45. package/src/screens/Device/__test__/detail.test.js +119 -86
  46. package/src/screens/Device/detail.js +38 -51
  47. package/src/screens/Device/hooks/useFavoriteDevice.js +38 -0
  48. package/src/screens/EmergencyContacts/EmergencyContactsList.js +1 -1
  49. package/src/screens/EmergencyContacts/EmergencyContactsSelectContacts.js +41 -44
  50. package/src/screens/EmergencyContacts/__test__/EmergencyContactList.test.js +1 -0
  51. package/src/screens/EmergencyContacts/__test__/EmergencyContactsSelectContacts.test.js +18 -19
  52. package/src/screens/Notification/__test__/NotificationItem.test.js +64 -53
  53. package/src/screens/Notification/components/NotificationItem.js +13 -4
  54. package/src/screens/ScriptDetail/__test__/index.test.js +15 -4
  55. package/src/screens/ScriptDetail/hooks/useStarredScript.js +32 -0
  56. package/src/screens/ScriptDetail/index.js +11 -20
  57. package/src/screens/SharedUnit/__test__/TabHeader.test.js +5 -0
  58. package/src/screens/Sharing/SelectUser.js +3 -23
  59. package/src/screens/Sharing/__test__/SelectUser.test.js +12 -80
  60. package/src/screens/SmartIr/__test__/GroupButtonByType.test.js +33 -0
  61. package/src/screens/SmartIr/components/GroupButtonByType/GroupButtonByType.js +2 -0
  62. package/src/screens/Unit/ChooseLocation.js +5 -0
  63. package/src/screens/Unit/Detail.js +33 -37
  64. package/src/screens/Unit/ManageUnit.js +21 -20
  65. package/src/screens/Unit/ManageUnitStyles.js +1 -0
  66. package/src/screens/Unit/SelectAddress.js +8 -2
  67. package/src/screens/Unit/Summaries.js +12 -15
  68. package/src/screens/Unit/__test__/Detail.test.js +25 -0
  69. package/src/screens/Unit/components/__test__/Header.test.js +32 -0
  70. package/src/screens/Unit/hook/useFavorites.js +28 -0
  71. package/src/utils/Apis/axios.js +7 -2
  72. package/src/utils/I18n/translations/en.json +1 -4
  73. package/src/utils/I18n/translations/vi.json +1 -4
@@ -11,7 +11,6 @@ import { API } from '../../../configs';
11
11
  import { getTranslate } from '../../../utils/I18n';
12
12
  import { SCProvider } from '../../../context';
13
13
  import { mockSCStore } from '../../../context/mockStore';
14
- import { ToastBottomHelper } from '../../../utils/Utils';
15
14
 
16
15
  const wrapComponent = (route) => (
17
16
  <SCProvider initState={mockSCStore({})}>
@@ -53,6 +52,7 @@ describe('test SelectUser container', () => {
53
52
  let route;
54
53
 
55
54
  beforeEach(() => {
55
+ axios.post.mockClear();
56
56
  route = {
57
57
  params: {
58
58
  unit: {
@@ -66,10 +66,6 @@ describe('test SelectUser container', () => {
66
66
  };
67
67
  });
68
68
 
69
- afterEach(() => {
70
- axios.post.mockClear();
71
- });
72
-
73
69
  const findByTestId = (instance, id) => {
74
70
  return instance.find((el) => el.props.testID === id);
75
71
  };
@@ -158,8 +154,16 @@ describe('test SelectUser container', () => {
158
154
  expect(axios.post).not.toHaveBeenCalled();
159
155
  });
160
156
 
161
- test('_TextInput onChange phone, validate and call api sharedPermission fail', async () => {
162
- const response = {};
157
+ test('_TextInput onChange phone, validated and call api sharedPermission', async () => {
158
+ const response = {
159
+ status: 200,
160
+ data: {
161
+ user: {
162
+ id: 2,
163
+ name: 'user add',
164
+ },
165
+ },
166
+ };
163
167
  mockAxiosPost(response);
164
168
 
165
169
  await act(async () => {
@@ -190,78 +194,6 @@ describe('test SelectUser container', () => {
190
194
  });
191
195
 
192
196
  accountList = instance.findAllByType(AccountList);
193
- expect(accountList).toHaveLength(0);
194
- });
195
-
196
- test('_TextInput onChange unitOwner phone, validate and call api sharedPermission', async () => {
197
- const response = {
198
- status: 400,
199
- data: { non_field_errors: 'You cannot invite yourself' },
200
- };
201
- mockAxiosPost(response);
202
-
203
- const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
204
-
205
- await act(async () => {
206
- tree = create(wrapComponent(route));
207
- });
208
- const instance = tree.root;
209
-
210
- const textInput = instance.findByType(_TextInput);
211
- const button = instance.findByType(Button);
212
-
213
- await act(async () => {
214
- await textInput.props.onChange('0909123456');
215
- });
216
-
217
- await act(async () => {
218
- await button.props.onPress();
219
- });
220
-
221
- expect(textInput.props.errorText).toEqual('');
222
- expect(axios.post).toHaveBeenCalledWith(API.SHARE.SHARE(), {
223
- phone: '0909123456',
224
- email: '',
225
- unit: 1,
226
- permissions: { controlPermissions: {}, readPermissions: {} },
227
- });
228
-
229
- expect(spyToastError).toBeCalled();
230
- });
231
-
232
- test('_TextInput onChange phone, validate and call api not sharedPermission', async () => {
233
- const response = {
234
- status: 400,
235
- data: { phone: '0909123456' },
236
- };
237
- mockAxiosPost(response);
238
-
239
- const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
240
-
241
- await act(async () => {
242
- tree = create(wrapComponent(route));
243
- });
244
- const instance = tree.root;
245
-
246
- const textInput = instance.findByType(_TextInput);
247
- const button = instance.findByType(Button);
248
-
249
- await act(async () => {
250
- await textInput.props.onChange('0909123456');
251
- });
252
-
253
- await act(async () => {
254
- await button.props.onPress();
255
- });
256
-
257
- expect(textInput.props.errorText).toEqual('');
258
- expect(axios.post).toHaveBeenCalledWith(API.SHARE.SHARE(), {
259
- phone: '0909123456',
260
- email: '',
261
- unit: 1,
262
- permissions: { controlPermissions: {}, readPermissions: {} },
263
- });
264
-
265
- expect(spyToastError).toBeCalled();
197
+ expect(accountList).toHaveLength(1);
266
198
  });
267
199
  });
@@ -5,6 +5,8 @@ import { SCProvider } from '../../../context';
5
5
  import { mockSCStore } from '../../../context/mockStore';
6
6
  import WrapHeaderScrollable from '../../../commons/Sharing/WrapHeaderScrollable';
7
7
  import { ButtonsBottom } from '../components/GroupButtonByType/ButtonsBottom';
8
+ import { TESTID } from '../../../configs/Constants';
9
+ import TextInput from '../../../commons/Form/TextInput';
8
10
 
9
11
  const wrapComponent = (route) => (
10
12
  <SCProvider initState={mockSCStore({})}>
@@ -42,6 +44,18 @@ describe('Test GroupButtonByType', () => {
42
44
  const instance = tree.root;
43
45
  const wrapHeaderScrollable = instance.findAllByType(WrapHeaderScrollable);
44
46
  expect(wrapHeaderScrollable).toHaveLength(1);
47
+
48
+ const ButtonBottom = instance.find(
49
+ (el) => el.props.testID === TESTID.GROUP_BUTTON_TYPE.BUTTON_BOTTOM
50
+ );
51
+
52
+ act(() => {
53
+ ButtonBottom.props.onPressLeft();
54
+ });
55
+ act(() => {
56
+ ButtonBottom.props.onPressRight();
57
+ });
58
+ expect(ButtonBottom).toBeDefined();
45
59
  });
46
60
  test('onPressLeft ButtonsBottom', () => {
47
61
  const route = {
@@ -77,4 +91,23 @@ describe('Test GroupButtonByType', () => {
77
91
  buttonsBottom[0].props.onPressRight();
78
92
  });
79
93
  });
94
+
95
+ test('onChange TextInputPassword', async () => {
96
+ const route = {
97
+ params: {
98
+ device_type: { id: 1, name: '', icon: '' },
99
+ brand: { id: 1, name: '' },
100
+ },
101
+ };
102
+ act(() => {
103
+ tree = renderer.create(wrapComponent(route));
104
+ });
105
+
106
+ const instance = tree.root;
107
+ const textInput = instance.findAllByType(TextInput);
108
+ await act(async () => {
109
+ await textInput[0].props.onChange();
110
+ });
111
+ expect(textInput).toHaveLength(1);
112
+ });
80
113
  });
@@ -13,6 +13,7 @@ import { ButtonsBottom } from './ButtonsBottom';
13
13
  import { Remote, SmartIr, Union } from '../../../../Images/SmartIr';
14
14
 
15
15
  import styles from './GroupButtonByTypeStyles';
16
+ import { TESTID } from '../../../../configs/Constants';
16
17
 
17
18
  const GroupButtonByType = memo(({ route }) => {
18
19
  const t = useTranslations();
@@ -141,6 +142,7 @@ const GroupButtonByType = memo(({ route }) => {
141
142
  selectionColor={Colors.Primary}
142
143
  />
143
144
  <ButtonsBottom
145
+ testID={TESTID.GROUP_BUTTON_TYPE.BUTTON_BOTTOM}
144
146
  onPressLeft={cancelButton}
145
147
  onPressRight={ApplyButton}
146
148
  textLeft={t('cancel')}
@@ -5,6 +5,7 @@ import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
5
5
  import { useNavigation } from '@react-navigation/native';
6
6
  import MarkerGeolocation from '../../../assets/images/Map/MarkerGeolocation.svg';
7
7
  import BottomButtonView from '../../commons/BottomButtonView';
8
+ import { FullLoading } from '../../commons';
8
9
  import { useTranslations } from '../../hooks/Common/useTranslations';
9
10
 
10
11
  navigator.geolocation = require('@react-native-community/geolocation');
@@ -27,6 +28,7 @@ const ChooseLocation = memo(({ route }) => {
27
28
  const { location, setAddress, setLocation } = route.params;
28
29
  const { goBack } = useNavigation();
29
30
  const [currentLocation, setCurrentLocation] = useState(location);
31
+ const [loading, setLoading] = useState(false);
30
32
  const mapRef = useRef(null);
31
33
 
32
34
  const onDone = useCallback(async () => {
@@ -34,6 +36,7 @@ const ChooseLocation = memo(({ route }) => {
34
36
  return;
35
37
  }
36
38
  const { latitude, longitude } = currentLocation;
39
+ setLoading(true);
37
40
  const { success, data } = await axiosGet(
38
41
  API.EXTERNAL.GOOGLE_MAP.GET_LOCATION_FROM_LAT_LNG,
39
42
  {
@@ -53,6 +56,7 @@ const ChooseLocation = memo(({ route }) => {
53
56
  });
54
57
  goBack();
55
58
  }
59
+ setLoading(false);
56
60
  }, [currentLocation, setAddress, setLocation, goBack]);
57
61
 
58
62
  const onRegionChange = useCallback(
@@ -89,6 +93,7 @@ const ChooseLocation = memo(({ route }) => {
89
93
  onPressMain={onDone}
90
94
  typeMain="primaryText"
91
95
  />
96
+ {loading && <FullLoading />}
92
97
  </View>
93
98
  );
94
99
  });
@@ -1,4 +1,10 @@
1
- import React, { useCallback, useContext, useEffect, useState } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ useRef,
7
+ } from 'react';
2
8
  import { AppState, RefreshControl, View } from 'react-native';
3
9
  import { useIsFocused } from '@react-navigation/native';
4
10
  import { useTranslations } from '../../hooks/Common/useTranslations';
@@ -16,6 +22,7 @@ import {
16
22
  useIsOwnerOfUnit,
17
23
  usePopover,
18
24
  } from '../../hooks/Common';
25
+ import { useFavorites } from './hook/useFavorites';
19
26
  import { scanBluetoothDevices } from '../../iot/RemoteControl/Bluetooth';
20
27
  import { googleHomeConnect } from '../../iot/RemoteControl/GoogleHome';
21
28
  import { axiosPost, fetchWithCache } from '../../utils/Apis/axios';
@@ -31,7 +38,11 @@ import { useNavigation } from '@react-navigation/native';
31
38
  import Routes from '../../utils/Route';
32
39
  import SubUnitAutomate from '../../commons/SubUnit/OneTap';
33
40
  import SubUnitFavorites from '../../commons/SubUnit/Favorites';
34
- import { AUTOMATE_TYPE, NOTIFICATION_TYPES } from '../../configs/Constants';
41
+ import {
42
+ AUTOMATE_TYPE,
43
+ NOTIFICATION_TYPES,
44
+ TESTID,
45
+ } from '../../configs/Constants';
35
46
  import {
36
47
  watchNotificationData,
37
48
  unwatchNotificationData,
@@ -66,26 +77,27 @@ const UnitDetail = ({ route }) => {
66
77
  );
67
78
 
68
79
  const [unit, setUnit] = useState(unitData || { id: unitId });
69
- const [appState, setAppState] = useState(AppState.currentState);
70
80
  const [listMenuItem, setListMenuItem] = useState([]);
71
81
  const [listStation, setListStation] = useState([]);
72
82
  const [listAutomate, setListAutomate] = useState([]);
73
- const [favorites, setFavorites] = useState({
74
- devices: [],
75
- automates: [],
76
- });
77
83
  const [isGGHomeConnected, setIsGGHomeConnected] = useState(false);
78
84
  const [station, setStation] = useState({});
79
85
  const [indexStation, setIndexStation] = useState(0);
80
86
  const [showAdd, setShowAdd, setHideAdd] = useBoolean();
81
87
  const [isFullScreen, setIsFullScreen] = useState(false);
82
88
  const [dataFullScreen, setDataFullScreen] = useState();
89
+ const appState = useRef(AppState.currentState);
83
90
 
84
91
  const { childRef, showingPopover, showPopoverWithRef, hidePopover } =
85
92
  usePopover();
86
93
 
87
94
  const { isOwner } = useIsOwnerOfUnit(unit.user_id);
88
95
 
96
+ const { favoriteDevices, favoriteAutomates } = useFavorites(
97
+ unit.stations || [],
98
+ listAutomate
99
+ );
100
+
89
101
  const handleFullScreen = (data) => {
90
102
  setIsFullScreen(!isFullScreen);
91
103
  setDataFullScreen(data);
@@ -105,18 +117,6 @@ const UnitDetail = ({ route }) => {
105
117
  isFavorites: true,
106
118
  name: t('favorites'),
107
119
  });
108
- let favoriteDevices = [];
109
- rawUnitData.stations.forEach((stationItem) => {
110
- if (stationItem.sensors) {
111
- favoriteDevices = favoriteDevices.concat(
112
- stationItem.sensors.filter((sensorItem) => sensorItem.is_favourite)
113
- );
114
- }
115
- });
116
- setFavorites((prevData) => ({
117
- ...prevData,
118
- devices: favoriteDevices,
119
- }));
120
120
  },
121
121
  [t]
122
122
  );
@@ -153,10 +153,6 @@ const UnitDetail = ({ route }) => {
153
153
  type: AUTOMATE_TYPE.AUTOMATION,
154
154
  },
155
155
  ]);
156
- setFavorites((prevData) => ({
157
- ...prevData,
158
- automates: data.filter((item) => item?.script?.is_star),
159
- }));
160
156
  }
161
157
  }
162
158
  );
@@ -170,23 +166,21 @@ const UnitDetail = ({ route }) => {
170
166
  [fetchDetails]
171
167
  );
172
168
 
173
- const handleAppStateChange = useCallback(
174
- (nextAppState) => {
175
- if (appState.match(/inactive|background/) && nextAppState === 'active') {
169
+ useEffect(() => {
170
+ const subscription = AppState.addEventListener('change', (nextAppState) => {
171
+ if (
172
+ appState.current.match(/inactive|background/) &&
173
+ nextAppState === 'active'
174
+ ) {
176
175
  fetchDetails();
177
176
  }
178
- setAppState(nextAppState);
179
- },
180
- [appState, fetchDetails]
181
- );
182
-
183
- useEffect(() => {
184
- AppState.addEventListener('change', handleAppStateChange);
177
+ appState.current = nextAppState;
178
+ });
185
179
 
186
180
  return () => {
187
- AppState.removeEventListener('change', handleAppStateChange);
181
+ subscription.remove();
188
182
  };
189
- }, [handleAppStateChange]);
183
+ }, [fetchDetails]);
190
184
 
191
185
  const handleGoogleHomeConnect = useCallback(
192
186
  async (options) => {
@@ -299,9 +293,10 @@ const UnitDetail = ({ route }) => {
299
293
  if (station?.isFavorites) {
300
294
  return (
301
295
  <SubUnitFavorites
302
- unit={unit}
303
296
  isOwner={isOwner}
304
- favorites={favorites}
297
+ unit={unit}
298
+ favoriteDevices={favoriteDevices}
299
+ favoriteAutomates={favoriteAutomates}
305
300
  wrapItemStyle={styles.wrapItemStyle}
306
301
  isGGHomeConnected={isGGHomeConnected}
307
302
  />
@@ -365,6 +360,7 @@ const UnitDetail = ({ route }) => {
365
360
  <View style={styles.container}>
366
361
  <Summaries unit={unit} />
367
362
  <NavBar
363
+ testID={TESTID.NAVBAR_ON_SNAP_ITEM}
368
364
  listStation={listStation}
369
365
  listMenuItem={listMenuItem}
370
366
  onSnapToItem={onSnapToItem}
@@ -39,25 +39,25 @@ const ButtonWrapper = ({
39
39
  style={styles.buttonWrapper}
40
40
  >
41
41
  <View style={styles.buttonInfo}>
42
- {!icon ? (
43
- <Text type="H4" semibold>
44
- {title}
45
- </Text>
46
- ) : (
47
- (icon && <Image source={{ uri: icon }} style={styles.avatar} />) || (
48
- <View style={styles.avatar}>
49
- <Icon name={'user'} size={27} />
50
- </View>
51
- )
52
- )}
42
+ <Text type="H4" semibold>
43
+ {title}
44
+ </Text>
53
45
  <View style={styles.buttonValue}>
54
- <Text
55
- type="Body"
56
- color={valueColor || Colors.Gray7}
57
- style={styles.value}
58
- >
59
- {value}
60
- </Text>
46
+ {!icon ? (
47
+ <Text
48
+ type="Body"
49
+ color={valueColor || Colors.Gray7}
50
+ style={styles.value}
51
+ >
52
+ {value}
53
+ </Text>
54
+ ) : (
55
+ <Image source={{ uri: value }} style={styles.avatar} /> || (
56
+ <View style={styles.avatar}>
57
+ <Icon name={'user'} size={27} />
58
+ </View>
59
+ )
60
+ )}
61
61
  <IconOutline name="right" size={20} color={Colors.Gray7} />
62
62
  </View>
63
63
  </View>
@@ -218,8 +218,9 @@ const ManageUnit = ({ route }) => {
218
218
  <>
219
219
  <ButtonWrapper
220
220
  onPress={() => handleChoosePhoto('avatar')}
221
- value={t('icon_unit')}
222
- icon={unitData.icon}
221
+ value={unitData.icon}
222
+ icon
223
+ title={t('icon_unit')}
223
224
  testID={TESTID.MANAGE_UNIT_CHANGE_PHOTO}
224
225
  />
225
226
  <ButtonWrapper
@@ -20,6 +20,7 @@ export default StyleSheet.create({
20
20
  flex: 1,
21
21
  flexDirection: 'row',
22
22
  justifyContent: 'space-between',
23
+ alignItems: 'center',
23
24
  },
24
25
  buttonValue: {
25
26
  flex: 1,
@@ -9,6 +9,7 @@ import BottomButtonView from '../../commons/BottomButtonView';
9
9
  import SearchBarLocation from '../../commons/SearchLocation';
10
10
  import RowLocation from '../../commons/SearchLocation/RowLocation';
11
11
  import Text from '../../commons/Text';
12
+ import { FullLoading } from '../../commons';
12
13
  import { useTranslations } from '../../hooks/Common/useTranslations';
13
14
 
14
15
  navigator.geolocation = require('@react-native-community/geolocation');
@@ -34,6 +35,7 @@ const SelectAddress = memo(({ route }) => {
34
35
  const [input, setInput] = useState('');
35
36
  const [searchData, setSearchData] = useState([]);
36
37
  const [searchedLocation, setSearchedLocation] = useState(null);
38
+ const [loading, setLoading] = useState(false);
37
39
  const mapRef = useRef(null);
38
40
 
39
41
  const onDone = useCallback(() => {
@@ -116,6 +118,7 @@ const SelectAddress = memo(({ route }) => {
116
118
  async (position) => {
117
119
  const currentLatitude = JSON.stringify(position.coords.latitude);
118
120
  const currentLongitude = JSON.stringify(position.coords.longitude);
121
+ setLoading(true);
119
122
  const { success, data } = await axiosGet(
120
123
  API.EXTERNAL.GOOGLE_MAP.GET_LOCATION_FROM_LAT_LNG,
121
124
  {
@@ -134,9 +137,11 @@ const SelectAddress = memo(({ route }) => {
134
137
  longitude: result.geometry.location.lng,
135
138
  });
136
139
  }
140
+ setLoading(false);
137
141
  },
138
- (error) => {},
139
- { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
142
+ // eslint-disable-next-line promise/prefer-await-to-callbacks
143
+ (error) => {}
144
+ // { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 } enable on emulator
140
145
  );
141
146
  }, []);
142
147
 
@@ -233,6 +238,7 @@ const SelectAddress = memo(({ route }) => {
233
238
  typeMain="primaryText"
234
239
  typeSecondary="primaryText"
235
240
  />
241
+ {loading && <FullLoading />}
236
242
  </View>
237
243
  );
238
244
  });
@@ -1,4 +1,4 @@
1
- import React, { memo, useCallback, useEffect, useState } from 'react';
1
+ import React, { memo, useCallback, useEffect, useState, useRef } from 'react';
2
2
  import { AppState, ScrollView } from 'react-native';
3
3
  import SummaryItem from '../../commons/SummaryItem';
4
4
  import Routes from '../../utils/Route';
@@ -8,12 +8,11 @@ import { API } from '../../configs';
8
8
 
9
9
  const Summaries = memo(({ unit }) => {
10
10
  const [unitSummaries, setUnitSummaries] = useState([]);
11
- const [appState, setAppState] = useState(AppState.currentState);
12
11
  // eslint-disable-next-line no-unused-vars
13
12
  const [localState, _] = useState({});
14
13
  const isFocused = useIsFocused();
15
-
16
14
  const navigation = useNavigation();
15
+ const appState = useRef(AppState.currentState);
17
16
 
18
17
  const fetchUnitSummary = useCallback(async () => {
19
18
  if (!unit.id) {
@@ -55,23 +54,21 @@ const Summaries = memo(({ unit }) => {
55
54
  }
56
55
  }, [localState, fetchUnitSummary]);
57
56
 
58
- const handleAppStateChange = useCallback(
59
- (nextAppState) => {
60
- if (appState.match(/inactive|background/) && nextAppState === 'active') {
57
+ useEffect(() => {
58
+ const subscription = AppState.addEventListener('change', (nextAppState) => {
59
+ if (
60
+ appState.current.match(/inactive|background/) &&
61
+ nextAppState === 'active'
62
+ ) {
61
63
  fetchUnitSummary();
62
64
  }
63
- setAppState(nextAppState);
64
- },
65
- [appState, fetchUnitSummary]
66
- );
67
-
68
- useEffect(() => {
69
- AppState.addEventListener('change', handleAppStateChange);
65
+ appState.current = nextAppState;
66
+ });
70
67
 
71
68
  return () => {
72
- AppState.removeEventListener('change', handleAppStateChange);
69
+ subscription.remove();
73
70
  };
74
- }, [handleAppStateChange]);
71
+ }, [fetchUnitSummary]);
75
72
 
76
73
  useEffect(() => {
77
74
  if (!isFocused) {
@@ -514,4 +514,29 @@ describe('Test UnitDetail', () => {
514
514
  const favorites = instance.findAllByType(SubUnitFavorites);
515
515
  expect(favorites).toHaveLength(1);
516
516
  });
517
+ test('render navbar', async () => {
518
+ const unitData = {
519
+ stations: [
520
+ {
521
+ isFavorites: true,
522
+ name: 'Favorites',
523
+ },
524
+ ],
525
+ };
526
+ await act(async () => {
527
+ tree = await renderer.create(
528
+ wrapComponent({ params: { ...route.params, unitData } }, account)
529
+ );
530
+ });
531
+ const instance = tree.root;
532
+ const navBar = instance.find(
533
+ (el) => el.props.testID === TESTID.NAVBAR_ON_SNAP_ITEM
534
+ );
535
+
536
+ await act(() => {
537
+ navBar.props.onSnapToItem();
538
+ });
539
+
540
+ expect(navBar).toBeDefined();
541
+ });
517
542
  });
@@ -8,6 +8,7 @@ import { mockSCStore } from '../../../../context/mockStore';
8
8
  import Header from '../Header';
9
9
  import ImageButton from '../../../../commons/ImageButton';
10
10
  import Routes from '../../../../utils/Route';
11
+ import { TouchableOpacity } from 'react-native';
11
12
 
12
13
  const wrapComponent = (title, goBack, dark, hideRight) => (
13
14
  <SCProvider initState={mockSCStore({})}>
@@ -15,6 +16,19 @@ const wrapComponent = (title, goBack, dark, hideRight) => (
15
16
  </SCProvider>
16
17
  );
17
18
 
19
+ const mockedNavigate = jest.fn();
20
+ const mockedGoBack = jest.fn();
21
+ jest.mock('@react-navigation/native', () => {
22
+ return {
23
+ ...jest.requireActual('@react-navigation/native'),
24
+ useNavigation: () => ({
25
+ navigate: mockedNavigate,
26
+ goBack: mockedGoBack,
27
+ }),
28
+ useIsFocused: () => true,
29
+ };
30
+ });
31
+
18
32
  describe('Test Header', () => {
19
33
  let tree;
20
34
  test('test hideModal', () => {
@@ -53,4 +67,22 @@ describe('Test Header', () => {
53
67
  });
54
68
  expect(popover.props.isVisible).toBe(false);
55
69
  });
70
+
71
+ test('test onpress touchableOpacity', () => {
72
+ act(() => {
73
+ tree = renderer.create(wrapComponent());
74
+ });
75
+ const instance = tree.root;
76
+ const touchableOpacity = instance.findAllByType(TouchableOpacity);
77
+
78
+ act(() => {
79
+ touchableOpacity[0].props.onPress();
80
+ touchableOpacity[1].props.onPress();
81
+ touchableOpacity[2].props.onPress();
82
+ touchableOpacity[3].props.onPress();
83
+ });
84
+ expect(touchableOpacity).toHaveLength(4);
85
+ expect(touchableOpacity).toBeDefined();
86
+ expect(mockedGoBack).toHaveBeenCalled();
87
+ });
56
88
  });
@@ -0,0 +1,28 @@
1
+ import { useMemo } from 'react';
2
+ import { useSCContextSelector } from '../../../context';
3
+
4
+ export const useFavorites = (stations, automatesData) => {
5
+ const favoriteDeviceIds = useSCContextSelector(
6
+ (state) => state.unit.favoriteDeviceIds
7
+ );
8
+ const starredScriptIds = useSCContextSelector(
9
+ (state) => state.automate.starredScriptIds
10
+ );
11
+
12
+ const favoriteDevices = useMemo(() => {
13
+ return []
14
+ .concat(...stations.map((station) => station.sensors || []))
15
+ .filter((device) => favoriteDeviceIds.includes(device.id));
16
+ }, [stations, favoriteDeviceIds]);
17
+
18
+ const favoriteAutomates = useMemo(() => {
19
+ return []
20
+ .concat(...automatesData.map((automateData) => automateData.data || []))
21
+ .filter((automate) => starredScriptIds.includes(automate?.script?.id));
22
+ }, [automatesData, starredScriptIds]);
23
+
24
+ return {
25
+ favoriteDevices,
26
+ favoriteAutomates,
27
+ };
28
+ };
@@ -50,10 +50,11 @@ const parseErrorResponse = async (error) => {
50
50
  error,
51
51
  message,
52
52
  data,
53
+ resp_status: error?.response?.status,
53
54
  };
54
55
  };
55
56
 
56
- export async function axiosCache(URL) {
57
+ export async function axiosCache(URL, resp_status) {
57
58
  const cacheKey = `@CACHE_REQUEST_${URL}`;
58
59
  const cachedData = await getData(cacheKey);
59
60
  if (!cachedData) {
@@ -64,6 +65,7 @@ export async function axiosCache(URL) {
64
65
  success: true,
65
66
  data: JSON.parse(cachedData),
66
67
  cache: true,
68
+ resp_status,
67
69
  };
68
70
  }
69
71
 
@@ -89,7 +91,10 @@ export async function axiosGet(URL, config = {}, cache = false) {
89
91
  if (cache) {
90
92
  // only network error or server error
91
93
  if (!error.response || error.response.status >= 500) {
92
- return (await axiosCache(URL)) || (await parseErrorResponse(error));
94
+ return (
95
+ (await axiosCache(URL, error.response.status)) ||
96
+ (await parseErrorResponse(error))
97
+ );
93
98
  } else {
94
99
  await deleteData(cacheKey);
95
100
  }