@eohjsc/react-native-smart-city 0.3.51 → 0.3.53

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/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.51",
4
+ "version": "0.3.53",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -116,7 +116,7 @@
116
116
  "@react-native-community/datetimepicker": "https://github.com/hinh-eoh/datepicker",
117
117
  "@react-native-community/geolocation": "^2.0.2",
118
118
  "@react-native-community/masked-view": "^0.1.10",
119
- "@react-native-community/netinfo": "^6.0.0",
119
+ "@react-native-community/netinfo": "^9.3.4",
120
120
  "@react-native-community/segmented-control": "^2.1.1",
121
121
  "@react-native-community/slider": "^3.0.3",
122
122
  "@react-native-community/toolbar-android": "^0.1.0-rc.2",
@@ -148,6 +148,62 @@ describe('Test ItemQuickAction', () => {
148
148
  assertIsActionOn();
149
149
  });
150
150
 
151
+ // eslint-disable-next-line max-len
152
+ it('trigger action with quick action with auto update and allow_config_store_value_id = quick_action.config_id and action.name includes off', async () => {
153
+ sensor.quick_action.will_auto_update_status = true;
154
+ sensor.quick_action.on_action.allow_config_store_value_id =
155
+ sensor.quick_action.config_id;
156
+ sensor.quick_action.on_action.name = 'off_action';
157
+
158
+ await act(async () => {
159
+ tree = await create(
160
+ <ItemQuickAction sensor={sensor} wrapperStyle={style} />
161
+ );
162
+ });
163
+ const instance = tree.root;
164
+ const buttonOnActionPress = instance.find(
165
+ (el) =>
166
+ el.props.accessibilityLabel ===
167
+ `${AccessibilityLabel.ITEM_QUICK_ACTION_PRESS}-${sensor?.id}` &&
168
+ el.type === TouchableOpacity
169
+ );
170
+ await act(async () => {
171
+ await buttonOnActionPress.props.onPress();
172
+ });
173
+ await act(async () => {
174
+ jest.runAllTimers();
175
+ });
176
+ assertIsActionOn();
177
+ });
178
+
179
+ // eslint-disable-next-line max-len
180
+ it('trigger action with quick action with auto update and allow_config_store_value_id = quick_action.config_id and action.name not includes off', async () => {
181
+ sensor.quick_action.will_auto_update_status = true;
182
+ sensor.quick_action.on_action.allow_config_store_value_id =
183
+ sensor.quick_action.config_id;
184
+ sensor.quick_action.on_action.name = 'on_action';
185
+
186
+ await act(async () => {
187
+ tree = await create(
188
+ <ItemQuickAction sensor={sensor} wrapperStyle={style} />
189
+ );
190
+ });
191
+ const instance = tree.root;
192
+ const buttonOnActionPress = instance.find(
193
+ (el) =>
194
+ el.props.accessibilityLabel ===
195
+ `${AccessibilityLabel.ITEM_QUICK_ACTION_PRESS}-${sensor?.id}` &&
196
+ el.type === TouchableOpacity
197
+ );
198
+ await act(async () => {
199
+ await buttonOnActionPress.props.onPress();
200
+ });
201
+ await act(async () => {
202
+ jest.runAllTimers();
203
+ });
204
+ assertIsActionOn();
205
+ });
206
+
151
207
  it('on press will toggle action', async () => {
152
208
  sensor.quick_action.on_action.id = 10; // off action
153
209
  const mockSetStatus = jest.fn();
@@ -13,8 +13,9 @@ const OnOffButtonAction = ({ title, configuration, onPress, template }) => {
13
13
  name: text_on,
14
14
  action: action_on,
15
15
  action_off: null,
16
+ template,
16
17
  });
17
- }, [onPress, configuration, text_on, action_on]);
18
+ }, [onPress, configuration, text_on, action_on, template]);
18
19
 
19
20
  const onPressActionOff = useCallback(() => {
20
21
  onPress &&
@@ -23,8 +24,9 @@ const OnOffButtonAction = ({ title, configuration, onPress, template }) => {
23
24
  name: text_off,
24
25
  action: action_off,
25
26
  action_on: null,
27
+ template,
26
28
  });
27
- }, [onPress, configuration, text_off, action_off]);
29
+ }, [onPress, configuration, text_off, action_off, template]);
28
30
 
29
31
  return (
30
32
  <>
@@ -15,6 +15,7 @@ const OnOffSimpleAction = ({ configuration, onPress, template }) => {
15
15
  name: t('text_on'),
16
16
  action: action_on,
17
17
  action_off: null,
18
+ template,
18
19
  });
19
20
  };
20
21
 
@@ -25,6 +26,7 @@ const OnOffSimpleAction = ({ configuration, onPress, template }) => {
25
26
  name: t('text_off'),
26
27
  action: action_off,
27
28
  action_on: null,
29
+ template,
28
30
  });
29
31
  };
30
32
 
@@ -13,8 +13,9 @@ const OnOffSmartLockAction = ({ configuration, onPress, template }) => {
13
13
  name: text_on,
14
14
  action: action_on,
15
15
  action_off: null,
16
+ template,
16
17
  });
17
- }, [onPress, configuration, text_on, action_on]);
18
+ }, [onPress, configuration, text_on, action_on, template]);
18
19
 
19
20
  const onPressActionClose = useCallback(() => {
20
21
  onPress &&
@@ -23,8 +24,9 @@ const OnOffSmartLockAction = ({ configuration, onPress, template }) => {
23
24
  name: text_off,
24
25
  action: action_off,
25
26
  action_on: null,
27
+ template,
26
28
  });
27
- }, [onPress, configuration, text_off, action_off]);
29
+ }, [onPress, configuration, text_off, action_off, template]);
28
30
 
29
31
  return (
30
32
  <>
@@ -117,7 +117,7 @@ const ItemDevice = memo(
117
117
  }
118
118
  // not managed by backend
119
119
  if (sensor?.device_type === DEVICE_TYPE.GOOGLE_HOME) {
120
- return !isHomeAssistantConnecting;
120
+ return isHomeAssistantConnected;
121
121
  }
122
122
  return true;
123
123
  })();
@@ -1,17 +1,21 @@
1
1
  import React from 'react';
2
- import renderer, { act } from 'react-test-renderer';
2
+ import { Platform } from 'react-native';
3
+ import { act, create } from 'react-test-renderer';
3
4
 
4
5
  import ImagePicker from '../index';
5
6
  import ButtonPopup from '../../ButtonPopup';
6
7
  import { SCProvider } from '../../../context';
7
8
  import { mockSCStore } from '../../../context/mockStore';
8
9
 
9
- const wrapComponent = (options) => (
10
+ const mockSetImageUrl = jest.fn();
11
+ const mockSetShowImagePicker = jest.fn();
12
+
13
+ const wrapComponent = (options, showImagePicker) => (
10
14
  <SCProvider initState={mockSCStore({})}>
11
15
  <ImagePicker
12
- showImagePicker={true}
13
- setShowImagePicker={''}
14
- setImageUrl={'setImageUrl'}
16
+ showImagePicker={showImagePicker}
17
+ setShowImagePicker={mockSetShowImagePicker}
18
+ setImageUrl={mockSetImageUrl}
15
19
  optionsCapture={options}
16
20
  optionsSelect={{
17
21
  mediaType: 'photo',
@@ -23,14 +27,18 @@ const wrapComponent = (options) => (
23
27
 
24
28
  describe('Test ImagePicker', () => {
25
29
  let tree;
26
- let Platform;
27
- beforeEach(() => {
28
- Platform = require('react-native').Platform;
29
- });
30
+
31
+ const options = {
32
+ mediaType: 'photo',
33
+ compressImageMaxWidth: 1280,
34
+ compressImageMaxHeight: 720,
35
+ compressImageQuality: 0.8,
36
+ };
37
+
30
38
  it('create ImagePicker', async () => {
31
39
  Platform.OS = 'android';
32
40
  await act(async () => {
33
- tree = renderer.create(wrapComponent());
41
+ tree = await create(wrapComponent());
34
42
  });
35
43
  const instance = tree.root;
36
44
  const textInputs = instance.findAllByType(ButtonPopup);
@@ -39,14 +47,8 @@ describe('Test ImagePicker', () => {
39
47
 
40
48
  it('create ImagePicker optionsCapture', async () => {
41
49
  Platform.OS = 'android';
42
- const options = {
43
- mediaType: 'photo',
44
- compressImageMaxWidth: 1280,
45
- compressImageMaxHeight: 720,
46
- compressImageQuality: 0.8,
47
- };
48
50
  await act(async () => {
49
- tree = renderer.create(wrapComponent(options));
51
+ tree = await create(wrapComponent(options));
50
52
  });
51
53
  const instance = tree.root;
52
54
  const textInputs = instance.findAllByType(ButtonPopup);
@@ -54,23 +56,34 @@ describe('Test ImagePicker', () => {
54
56
  await act(async () => {
55
57
  textInputs[0].props.onPressMain();
56
58
  });
59
+ expect(mockSetImageUrl).toBeCalledWith({ path: 'path_from_camera' });
57
60
  });
61
+
58
62
  it('create ImagePicker onChooseFile', async () => {
59
63
  Platform.OS = 'android';
60
- const options = {
61
- mediaType: 'photo',
62
- compressImageMaxWidth: 1280,
63
- compressImageMaxHeight: 720,
64
- compressImageQuality: 0.8,
65
- };
66
64
  await act(async () => {
67
- tree = renderer.create(wrapComponent(options));
65
+ tree = await create(wrapComponent(options));
68
66
  });
69
67
  const instance = tree.root;
70
- const textInputs = instance.findAllByType(ButtonPopup);
71
- expect(textInputs.length).toBe(1);
68
+ const buttonPopup = instance.findByType(ButtonPopup);
69
+ await act(async () => {
70
+ buttonPopup.props.onPressSecondary();
71
+ });
72
+ expect(mockSetImageUrl).toBeCalledWith({
73
+ path: 'file:///data/user/0/com.eohjsc.eohmobile/cache/Camera/80fbbd4b-926d-425f-a081-e21b13f2f7d0.jpg',
74
+ });
75
+ });
76
+
77
+ it('Test onClose', async () => {
78
+ Platform.OS = 'ios';
79
+ await act(async () => {
80
+ tree = await create(wrapComponent(options, true));
81
+ });
82
+ const instance = tree.root;
83
+ const buttonPopup = instance.findByType(ButtonPopup);
72
84
  await act(async () => {
73
- textInputs[0].props.onPressSecondary();
85
+ await buttonPopup.props.onClose();
74
86
  });
87
+ expect(mockSetShowImagePicker).toBeCalledWith(false);
75
88
  });
76
89
  });
@@ -1,10 +1,14 @@
1
- import React, { useCallback } from 'react';
2
- import { Platform, PermissionsAndroid } from 'react-native';
1
+ import React, { memo, useCallback } from 'react';
3
2
  import ImagePickerCrop from 'react-native-image-crop-picker';
3
+ import { RESULTS } from 'react-native-permissions';
4
4
 
5
- import { useTranslations } from '../../hooks/Common/useTranslations';
6
-
5
+ import {
6
+ keyPermission,
7
+ OpenSetting,
8
+ permitPermissionFunction,
9
+ } from '../../utils/Permission/common';
7
10
  import ButtonPopup from '../ButtonPopup';
11
+ import { useTranslations } from '../../hooks/Common/useTranslations';
8
12
 
9
13
  const ImagePicker = ({
10
14
  showImagePicker,
@@ -14,47 +18,6 @@ const ImagePicker = ({
14
18
  optionsSelect,
15
19
  }) => {
16
20
  const t = useTranslations();
17
-
18
- const requestCameraPermission = useCallback(async () => {
19
- if (Platform.OS === 'android') {
20
- try {
21
- const granted = await PermissionsAndroid.request(
22
- PermissionsAndroid.PERMISSIONS.CAMERA,
23
- {
24
- title: 'Camera Permission',
25
- message: 'App needs camera permission',
26
- }
27
- );
28
- // If CAMERA Permission is granted
29
- return granted === PermissionsAndroid.RESULTS.GRANTED;
30
- } catch (err) {
31
- return false;
32
- }
33
- } else {
34
- return true;
35
- }
36
- }, []);
37
-
38
- const requestExternalWritePermission = useCallback(async () => {
39
- if (Platform.OS === 'android') {
40
- try {
41
- const granted = await PermissionsAndroid.request(
42
- PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
43
- {
44
- title: 'External Storage Write Permission',
45
- message: 'App needs write permission',
46
- }
47
- );
48
- // If WRITE_EXTERNAL_STORAGE Permission is granted
49
- return granted === PermissionsAndroid.RESULTS.GRANTED;
50
- } catch (err) {
51
- return false;
52
- }
53
- } else {
54
- return true;
55
- }
56
- }, []);
57
-
58
21
  const captureImage = useCallback(
59
22
  async (type) => {
60
23
  let options = optionsCapture
@@ -65,28 +28,27 @@ const ImagePicker = ({
65
28
  compressImageMaxHeight: 720,
66
29
  compressImageQuality: 0.8,
67
30
  };
68
-
69
- let isCameraPermitted = await requestCameraPermission();
70
- let isStoragePermitted = await requestExternalWritePermission();
71
- if (isCameraPermitted && isStoragePermitted) {
72
- await ImagePickerCrop.openCamera(options)
73
- .then((response) => {
74
- setImageUrl(response);
75
- setShowImagePicker(false);
76
- })
77
- .catch((e) => {
78
- /* eslint-disable no-console */
79
- console.log('ERROR ' + e);
80
- });
81
- }
31
+ permitPermissionFunction(keyPermission.CAMERA, async (result) => {
32
+ if (result === RESULTS.GRANTED) {
33
+ await ImagePickerCrop.openCamera(options)
34
+ .then((response) => {
35
+ setImageUrl(response);
36
+ setShowImagePicker(false);
37
+ })
38
+ .catch((e) => {
39
+ /* eslint-disable no-console */
40
+ console.log('ERROR ' + e);
41
+ });
42
+ }
43
+ if (result === RESULTS.BLOCKED) {
44
+ OpenSetting(
45
+ t('camera_request_permission'),
46
+ t('camera_request_permission_des')
47
+ );
48
+ }
49
+ });
82
50
  },
83
- [
84
- setImageUrl,
85
- setShowImagePicker,
86
- requestCameraPermission,
87
- requestExternalWritePermission,
88
- optionsCapture,
89
- ]
51
+ [optionsCapture, setImageUrl, setShowImagePicker, t]
90
52
  );
91
53
 
92
54
  const chooseFile = useCallback(
@@ -99,17 +61,27 @@ const ImagePicker = ({
99
61
  compressImageMaxHeight: 720,
100
62
  compressImageQuality: 0.8,
101
63
  };
102
- await ImagePickerCrop.openPicker(options)
103
- .then((response) => {
104
- setImageUrl(response);
105
- setShowImagePicker(false);
106
- })
107
- .catch((e) => {
108
- /* eslint-disable no-console */
109
- console.log('ERROR ' + e);
110
- });
64
+ permitPermissionFunction(keyPermission.SELECT_PHOTO, async (result) => {
65
+ if (result === RESULTS.GRANTED || result === RESULTS.LIMITED) {
66
+ await ImagePickerCrop.openPicker(options)
67
+ .then((response) => {
68
+ setImageUrl(response);
69
+ setShowImagePicker(false);
70
+ })
71
+ .catch((e) => {
72
+ /* eslint-disable no-console */
73
+ console.log('ERROR ' + e);
74
+ });
75
+ }
76
+ if (result === RESULTS.BLOCKED) {
77
+ OpenSetting(
78
+ t('photo_request_permission'),
79
+ t('photo_request_permission_des')
80
+ );
81
+ }
82
+ });
111
83
  },
112
- [setImageUrl, setShowImagePicker, optionsSelect]
84
+ [optionsSelect, setImageUrl, setShowImagePicker, t]
113
85
  );
114
86
 
115
87
  const onCaptureImage = useCallback(() => {
@@ -124,25 +96,18 @@ const ImagePicker = ({
124
96
  setShowImagePicker(false);
125
97
  }, [setShowImagePicker]);
126
98
 
127
- const buttonEvent = {
128
- main_title: t('take_photo'),
129
- secondary_title: t('choose_from_library'),
130
- on_press_main: onCaptureImage,
131
- on_press_secondary: onChooseFile,
132
- };
133
-
134
99
  return (
135
100
  <ButtonPopup
136
101
  rowButton={false}
137
102
  visible={showImagePicker}
138
103
  onClose={onClose}
139
- mainTitle={buttonEvent.main_title}
140
- onPressMain={buttonEvent.on_press_main}
141
- secondaryTitle={buttonEvent.secondary_title}
142
- onPressSecondary={buttonEvent.on_press_secondary}
104
+ mainTitle={t('take_photo')}
105
+ onPressMain={onCaptureImage}
106
+ secondaryTitle={t('choose_from_library')}
107
+ onPressSecondary={onChooseFile}
143
108
  typeSecondary={'primary'}
144
109
  />
145
110
  );
146
111
  };
147
112
 
148
- export default ImagePicker;
113
+ export default memo(ImagePicker);
@@ -55,7 +55,7 @@ const SelectGateway = ({
55
55
  const t = useTranslations();
56
56
  const { goBack } = useNavigation();
57
57
  const isFocused = useIsFocused();
58
- const [selectedIndex, setSelectedIndex] = useState(0);
58
+ const [selectedIndex, setSelectedIndex] = useState(-1);
59
59
  const [gateways, setGateways] = useState([]);
60
60
  const [showPopupGuide, setShowPopupGuide, setHidePopupGuide] =
61
61
  useBoolean(false);
@@ -77,15 +77,19 @@ const SelectGateway = ({
77
77
  }
78
78
  }, [setShowPopupGuide, type, unitId]);
79
79
 
80
- const handleOk = () => {
80
+ const handleButtonModalOk = useCallback(() => {
81
81
  setHidePopupGuide();
82
- onPressOk();
83
- };
82
+ onPressOk && onPressOk();
83
+ }, [onPressOk, setHidePopupGuide]);
84
84
 
85
85
  useEffect(() => {
86
86
  isFocused && fetchDetails();
87
87
  }, [fetchDetails, isFocused]);
88
88
 
89
+ const onRightClickNext = useCallback(() => {
90
+ onPressNext && onPressNext(gateways[selectedIndex]);
91
+ }, [gateways, onPressNext, selectedIndex]);
92
+
89
93
  return (
90
94
  <View style={styles.container}>
91
95
  <HeaderCustom title={title} isShowSeparator />
@@ -115,7 +119,7 @@ const SelectGateway = ({
115
119
  onLeftClick={goBack}
116
120
  rightTitle={t('next')}
117
121
  rightDisabled={selectedIndex === -1}
118
- onRightClick={() => onPressNext(gateways[selectedIndex])}
122
+ onRightClick={onRightClickNext}
119
123
  accessibilityLabelPrefix={AccessibilityLabel.PREFIX.SELECT_UNIT}
120
124
  />
121
125
  <ModalCustom // todo Huy - make reused component
@@ -134,7 +138,7 @@ const SelectGateway = ({
134
138
  </View>
135
139
  <ViewButtonBottom
136
140
  rightTitle={t('ok')}
137
- onRightClick={handleOk}
141
+ onRightClick={handleButtonModalOk}
138
142
  styleButton={styles.bottomButton}
139
143
  />
140
144
  </View>
@@ -1,9 +1,12 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
3
 
4
4
  import { Colors } from '../../configs';
5
5
  import Text from '../../commons/Text';
6
6
  import { AccessibilityLabel } from '../../configs/Constants';
7
+ import withPreventDoubleClick from '../WithPreventDoubleClick';
8
+
9
+ const PreventDoubleTouch = withPreventDoubleClick(TouchableOpacity);
7
10
 
8
11
  // this view support either 1 or 2 button
9
12
  const ViewButtonBottom = ({
@@ -19,9 +22,14 @@ const ViewButtonBottom = ({
19
22
  styleButtonLeft,
20
23
  styleButtonRight,
21
24
  accessibilityLabelPrefix = '',
25
+ isPreventDoubleTouch = false,
22
26
  }) => {
23
27
  const useTwoButton = leftTitle && rightTitle;
24
28
 
29
+ const RightButtonView = useMemo(() => {
30
+ return isPreventDoubleTouch ? PreventDoubleTouch : TouchableOpacity;
31
+ }, [isPreventDoubleTouch]);
32
+
25
33
  return (
26
34
  <View style={styles.container}>
27
35
  {leftTitle && (
@@ -47,7 +55,7 @@ const ViewButtonBottom = ({
47
55
  </TouchableOpacity>
48
56
  )}
49
57
  {rightTitle && (
50
- <TouchableOpacity
58
+ <RightButtonView
51
59
  style={[
52
60
  styles.button,
53
61
  styleButton,
@@ -66,7 +74,7 @@ const ViewButtonBottom = ({
66
74
  >
67
75
  {rightTitle}
68
76
  </Text>
69
- </TouchableOpacity>
77
+ </RightButtonView>
70
78
  )}
71
79
  </View>
72
80
  );
@@ -97,6 +97,7 @@ const SCDefaultConfig = {
97
97
  pusherAppKey: '8557fcc63959f564f1aa',
98
98
  pusherAppCluster: 'ap1',
99
99
  intervalWatchConfigTime: 30000,
100
+ setCurrentSensorDisplay: () => {},
100
101
  };
101
102
 
102
103
  export class SCConfig {
@@ -112,6 +113,7 @@ export class SCConfig {
112
113
  static pusherAppCluste = SCDefaultConfig.pusherAppCluster;
113
114
  static language = 'en';
114
115
  static intervalWatchConfigTime = SCDefaultConfig.intervalWatchConfigTime;
116
+ static setCurrentSensorDisplay = SCDefaultConfig.setCurrentSensorDisplay;
115
117
  }
116
118
 
117
119
  export const initSCConfig = (config) => {
@@ -135,4 +137,6 @@ export const initSCConfig = (config) => {
135
137
  config.pusherAppCluster ?? SCDefaultConfig.pusherAppCluster;
136
138
  SCConfig.intervalWatchConfigTime =
137
139
  config.intervalWatchConfigTime ?? SCDefaultConfig.intervalWatchConfigTime;
140
+ SCConfig.setCurrentSensorDisplay =
141
+ config.setCurrentSensorDisplay ?? SCDefaultConfig.setCurrentSensorDisplay;
138
142
  };
@@ -13,7 +13,8 @@ const permissions = [
13
13
  'android.permission.BLUETOOTH_ADVERTISE',
14
14
  ];
15
15
 
16
- const useBluetoothConnection = () => {
16
+ // NOTE: fnCallback is used for Lavida when found device
17
+ const useBluetoothConnection = (fnCallback) => {
17
18
  const t = useTranslations();
18
19
  const appState = useRef(AppState.currentState);
19
20
 
@@ -29,6 +30,7 @@ const useBluetoothConnection = () => {
29
30
  useSCContextSelector((state) => state.bluetooth.permissionsGranted);
30
31
 
31
32
  const onDeviceFound = useCallback(async (name, device) => {
33
+ fnCallback && fnCallback({ name, device });
32
34
  setAction(Action.SET_BLUETOOTH_CONNECTED_DEVICE, { name, device });
33
35
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
36
  }, []);
@@ -78,6 +78,7 @@ const ConnectingWifiGuide = ({ route }) => {
78
78
  'Eoh@2021',
79
79
  false
80
80
  );
81
+ setCurrentState(1);
81
82
  } catch (e) {
82
83
  Alert.alert(
83
84
  t('cannot_connect_to_device_wifi'),
@@ -6,18 +6,19 @@ import Routes from '../../utils/Route';
6
6
 
7
7
  const SelectZigbeeGateway = ({ route }) => {
8
8
  const t = useTranslations();
9
- const navigation = useNavigation();
9
+ const { navigate } = useNavigation();
10
10
  const { unitId, subUnit } = route?.params || {};
11
11
 
12
12
  const onPressNext = useCallback(
13
13
  (gateway) => {
14
- navigation.navigate(Routes.ZigbeeDeviceConnectGuide, {
15
- unitId,
16
- subUnit,
17
- chipId: gateway.id,
18
- });
14
+ gateway &&
15
+ navigate(Routes.ZigbeeDeviceConnectGuide, {
16
+ unitId,
17
+ subUnit,
18
+ chipId: gateway.id,
19
+ });
19
20
  },
20
- [navigation, unitId, subUnit]
21
+ [unitId, subUnit, navigate]
21
22
  );
22
23
 
23
24
  return (
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { Alert, ScrollView } from 'react-native';
3
3
  import { act, create } from 'react-test-renderer';
4
4
  import MockAdapter from 'axios-mock-adapter';
5
+ import { NavigationContext } from '@react-navigation/native';
5
6
 
6
7
  import DeviceDetail from '../detail';
7
8
  import { API } from '../../../configs';
@@ -29,7 +30,6 @@ jest.mock('@react-navigation/native', () => {
29
30
  useNavigation: () => ({
30
31
  navigate: mockedNavigate,
31
32
  }),
32
- useFocusEffect: jest.fn(),
33
33
  };
34
34
  });
35
35
 
@@ -73,9 +73,20 @@ const mockAxios = (
73
73
 
74
74
  let store = mockSCStore({});
75
75
 
76
+ const navContextValue = {
77
+ isFocused: () => false,
78
+ addListener: jest.fn(() => jest.fn()),
79
+ };
76
80
  const wrapComponent = (state, account, route) => (
77
81
  <SCProvider initState={state}>
78
- <DeviceDetail account={account} route={route} />
82
+ <NavigationContext.Provider
83
+ value={{
84
+ ...navContextValue,
85
+ isFocused: () => true,
86
+ }}
87
+ >
88
+ <DeviceDetail account={account} route={route} />
89
+ </NavigationContext.Provider>
79
90
  </SCProvider>
80
91
  );
81
92
 
@@ -7,7 +7,7 @@ import React, {
7
7
  useContext,
8
8
  } from 'react';
9
9
  import { View, TouchableOpacity, Platform } from 'react-native';
10
- import { useNavigation } from '@react-navigation/native';
10
+ import { useNavigation, useFocusEffect } from '@react-navigation/native';
11
11
  import { useSelector } from 'react-redux';
12
12
  import { IconFill, IconOutline } from '@ant-design/icons-react-native';
13
13
  import { Icon } from '@ant-design/react-native';
@@ -55,7 +55,7 @@ import PreventAccess from '../../commons/PreventAccess';
55
55
  import styles from './styles';
56
56
  import Routes from '../../utils/Route';
57
57
  import { getData as getLocalData } from '../../utils/Storage';
58
- import { API, Colors } from '../../configs';
58
+ import { API, Colors, SCConfig } from '../../configs';
59
59
  import { DEVICE_TYPE, AccessibilityLabel } from '../../configs/Constants';
60
60
  import { axiosGet } from '../../utils/Apis/axios';
61
61
  import { notImplemented } from '../../utils/Utils';
@@ -81,7 +81,8 @@ const DeviceDetail = ({ route }) => {
81
81
  // eslint-disable-next-line no-unused-vars
82
82
  const [configValues, setConfigValues] = useConfigGlobalState('configValues');
83
83
 
84
- const { unitData, unitId, sensorData, sensorId } = route?.params || {};
84
+ const { unitData, unitId, sensorData, sensorId, isMyUnitDeviceScreen } =
85
+ route?.params || {};
85
86
  const [unit, setUnit] = useState(unitData || { id: unitId });
86
87
  const [sensor, setSensor] = useState(sensorData || { id: sensorId });
87
88
  const [station, setStation] = useState(sensor?.station);
@@ -116,6 +117,9 @@ const DeviceDetail = ({ route }) => {
116
117
  useBluetoothDeviceConnected(sensor);
117
118
 
118
119
  const isDeviceHasBle = useMemo(() => {
120
+ if (display.items.length === 0) {
121
+ return false;
122
+ }
119
123
  const action = display.items.filter((item) => item.type === 'action');
120
124
  if (action.length === 0) {
121
125
  return false;
@@ -348,7 +352,7 @@ const DeviceDetail = ({ route }) => {
348
352
  menuItems.push({
349
353
  text: t('move_to_another_sub_unit'),
350
354
  route: Routes.MoveToAnotherSubUnit,
351
- data: { unit, sensor, station },
355
+ data: { unit, sensor, station, isMyUnitDeviceScreen },
352
356
  });
353
357
  }
354
358
  if (isShowSetupEmergencyContact) {
@@ -428,14 +432,15 @@ const DeviceDetail = ({ route }) => {
428
432
  display.items,
429
433
  isOwner,
430
434
  isShowSetupEmergencyContact,
431
- isShowSetUpSmartLock,
435
+ sideMenu,
432
436
  t,
437
+ isShowSetUpSmartLock,
433
438
  isFavorite,
434
439
  sensor,
435
440
  unit,
436
441
  sensorName,
437
- sideMenu,
438
442
  station,
443
+ isMyUnitDeviceScreen,
439
444
  emergencyDeviceId,
440
445
  addToFavorites,
441
446
  removeFromFavorites,
@@ -496,77 +501,79 @@ const DeviceDetail = ({ route }) => {
496
501
  [configValues, displayValuesData, evaluateValue]
497
502
  );
498
503
 
499
- useEffect(() => {
500
- let params = new URLSearchParams();
501
- const configIds = [];
502
-
503
- display.items.map((item) => {
504
- if (item.type !== 'value') {
505
- return;
506
- }
504
+ useFocusEffect(
505
+ useCallback(() => {
506
+ let params = new URLSearchParams();
507
+ const configIds = [];
507
508
 
508
- if (!item.configuration) {
509
- return;
510
- }
509
+ display.items.map((item) => {
510
+ if (item.type !== 'value') {
511
+ return;
512
+ }
511
513
 
512
- item.configuration.configs.map((config) => {
513
- if (!configIds.includes(config.id)) {
514
- configIds.push(config.id);
514
+ if (!item.configuration) {
515
+ return;
515
516
  }
517
+
518
+ item.configuration.configs.map((config) => {
519
+ if (!configIds.includes(config.id)) {
520
+ configIds.push(config.id);
521
+ }
522
+ });
516
523
  });
517
- });
518
524
 
519
- configIds.map((id) => {
520
- params.append('config', id);
521
- });
525
+ configIds.map((id) => {
526
+ params.append('config', id);
527
+ });
522
528
 
523
- const fetchValues = async () => {
524
- const { success, data, resp_status } = await axiosGet(
525
- API.DEVICE.DISPLAY_VALUES_V2(sensor?.id),
526
- {
527
- params: params,
529
+ const fetchValues = async () => {
530
+ const { success, data, resp_status } = await axiosGet(
531
+ API.DEVICE.DISPLAY_VALUES_V2(sensor?.id),
532
+ {
533
+ params: params,
534
+ }
535
+ );
536
+ if (success) {
537
+ data.isConnected = data.is_connected;
538
+ data.lastUpdated = data.last_updated
539
+ ? moment(data.last_updated)
540
+ : data.last_updated;
541
+ setDisplayValuesData((prevState) => {
542
+ if (prevState.isConnected !== data.isConnected) {
543
+ setAction(Action.SET_DEVICES_STATUS, [
544
+ { id: sensor?.id, is_connected: data.is_connected },
545
+ ]);
546
+ }
547
+ return data;
548
+ });
549
+ } else if (resp_status >= 500) {
550
+ setServerDown(true);
528
551
  }
529
- );
530
- if (success) {
531
- data.isConnected = data.is_connected;
532
- data.lastUpdated = data.last_updated
533
- ? moment(data.last_updated)
534
- : data.last_updated;
535
- setDisplayValuesData((prevState) => {
536
- if (prevState.isConnected !== data.isConnected) {
537
- setAction(Action.SET_DEVICES_STATUS, [
538
- { id: sensor?.id, is_connected: data.is_connected },
539
- ]);
552
+ setLoading((preState) => {
553
+ if (preState.isConnected) {
554
+ return {
555
+ ...preState,
556
+ isConnected: false,
557
+ };
540
558
  }
541
- return data;
559
+ return preState;
542
560
  });
543
- } else if (resp_status >= 500) {
544
- setServerDown(true);
561
+ };
562
+ if (
563
+ isNetworkConnected &&
564
+ sensor?.is_managed_by_backend &&
565
+ sensor?.device_type !== DEVICE_TYPE.LG_THINQ
566
+ ) {
567
+ const updateInterval = setInterval(() => fetchValues(), 5000);
568
+ fetchValues();
569
+ return () => clearInterval(updateInterval);
570
+ } else {
571
+ Object.keys(sensor).length > 1 &&
572
+ setLoading((preState) => ({ ...preState, isConnected: false }));
545
573
  }
546
- setLoading((preState) => {
547
- if (preState.isConnected) {
548
- return {
549
- ...preState,
550
- isConnected: false,
551
- };
552
- }
553
- return preState;
554
- });
555
- };
556
- if (
557
- isNetworkConnected &&
558
- sensor?.is_managed_by_backend &&
559
- sensor?.device_type !== DEVICE_TYPE.LG_THINQ
560
- ) {
561
- const updateInterval = setInterval(() => fetchValues(), 5000);
562
- fetchValues();
563
- return () => clearInterval(updateInterval);
564
- } else {
565
- Object.keys(sensor).length > 1 &&
566
- setLoading((preState) => ({ ...preState, isConnected: false }));
567
- }
568
- // eslint-disable-next-line react-hooks/exhaustive-deps
569
- }, [sensor, display, isNetworkConnected]);
574
+ // eslint-disable-next-line react-hooks/exhaustive-deps
575
+ }, [sensor, display, isNetworkConnected])
576
+ );
570
577
 
571
578
  const isShowEmergencyResolve =
572
579
  display.items.filter(
@@ -751,6 +758,10 @@ const DeviceDetail = ({ route }) => {
751
758
  ]
752
759
  );
753
760
 
761
+ useEffect(() => {
762
+ SCConfig.setCurrentSensorDisplay(sensor);
763
+ }, [sensor]);
764
+
754
765
  return (
755
766
  <View style={styles.wrap}>
756
767
  <WrapHeaderScrollable
@@ -33,15 +33,17 @@ const RowSubUnit = ({ subUnit, isSelected, onSelect }) => {
33
33
  const MoveToAnotherSubUnit = memo(({ route }) => {
34
34
  const t = useTranslations();
35
35
  const { params = {} } = route;
36
- const { unit, sensor, station } = params;
36
+ const { unit, sensor, station, isMyUnitDeviceScreen } = params;
37
37
  const { navigate } = useNavigation();
38
38
  const [selectedSubUnit, setSelectedSubUnit] = useState(
39
39
  unit?.stations?.find((subUnit) => subUnit.id === station.id)
40
40
  );
41
41
 
42
42
  const listStationUnit = useMemo(() => {
43
- return unit?.stations.slice(2) || [];
44
- }, [unit?.stations]);
43
+ return isMyUnitDeviceScreen
44
+ ? unit?.stations || []
45
+ : unit?.stations.slice(2) || [];
46
+ }, [isMyUnitDeviceScreen, unit?.stations]);
45
47
 
46
48
  const handleOnSelect = useCallback((item) => {
47
49
  setSelectedSubUnit(item);
@@ -55,7 +57,12 @@ const MoveToAnotherSubUnit = memo(({ route }) => {
55
57
  }
56
58
  );
57
59
  if (success) {
58
- navigate(Routes.UnitDetail);
60
+ navigate(Routes.UnitStack, {
61
+ screen: Routes.UnitDetail,
62
+ params: {
63
+ unitId: unit.id,
64
+ },
65
+ });
59
66
  }
60
67
  }, [navigate, selectedSubUnit?.id, sensor?.id, station?.id, unit?.id]);
61
68
 
@@ -72,7 +72,10 @@ const PlayBackCamera = () => {
72
72
  (hour) => {
73
73
  const hourWithTimezone =
74
74
  parseInt(hour, 10) + parseInt(item?.configuration?.time_zone || 0, 10);
75
- return hourWithTimezone < 10 ? '0' + hourWithTimezone : hourWithTimezone;
75
+ if (hourWithTimezone < 0 || hourWithTimezone > 9) {
76
+ return hourWithTimezone;
77
+ }
78
+ return '0' + hourWithTimezone;
76
79
  },
77
80
  [item?.configuration?.time_zone]
78
81
  );
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { act, renderHook } from '@testing-library/react-hooks';
3
+ import { SCProvider } from '../../../context';
4
+ import { mockSCStore } from '../../../context/mockStore';
5
+ import { useStarredScript } from '../hooks/useStarredScript';
6
+ import MockAdapter from 'axios-mock-adapter';
7
+ import api from '../../../utils/Apis/axios';
8
+ import { API } from '../../../configs';
9
+ import { Action } from '../../../context/actionType';
10
+
11
+ const mockedSetAction = jest.fn();
12
+ const mock = new MockAdapter(api.axiosInstance);
13
+
14
+ const wrapper = ({ children }) => <SCProvider>{children}</SCProvider>;
15
+
16
+ const mockUseContext = jest.fn().mockImplementation(() => ({
17
+ stateData: mockSCStore({}),
18
+ setAction: mockedSetAction,
19
+ }));
20
+
21
+ React.useContext = mockUseContext;
22
+
23
+ jest.mock('react-native-deep-linking');
24
+
25
+ describe('Test useStarredScript', async () => {
26
+ let props;
27
+ beforeEach(() => {
28
+ mockedSetAction.mockClear();
29
+ props = {
30
+ name: 'AutomateScript',
31
+ id: 1,
32
+ script: { id: 2, name: 'ScriptName' },
33
+ };
34
+ });
35
+
36
+ it('test send remote command action null', async () => {
37
+ mock.onPost(API.AUTOMATE.UNSTAR_SCRIPT(props.id)).reply(200);
38
+ const { result } = renderHook(() => useStarredScript(props), {
39
+ wrapper,
40
+ });
41
+ await act(async () => {
42
+ await result.current.unstarScript();
43
+ });
44
+ expect(mockedSetAction).toBeCalledWith(Action.UNSTAR_SCRIPTS, [2]);
45
+ });
46
+ });
@@ -267,8 +267,8 @@ const SelectPermission = ({ route }) => {
267
267
  arrIdReadTemp.length &&
268
268
  read_permissions.push({ id: item.id, values: arrIdReadTemp });
269
269
 
270
- !arrIdControlTemp &&
271
- !arrIdReadTemp &&
270
+ !arrIdControlTemp.length &&
271
+ !arrIdReadTemp.length &&
272
272
  item.isChecked &&
273
273
  read_permissions.push({ id: item.id, values: [] });
274
274
  }
@@ -239,6 +239,7 @@ const AddSubUnit = ({ route }) => {
239
239
  rightTitle={t('done')}
240
240
  rightDisabled={validateData}
241
241
  onRightClick={goDone}
242
+ isPreventDoubleTouch
242
243
  />
243
244
  </View>
244
245
  </TouchableWithoutFeedback>
@@ -22,6 +22,7 @@ const MyUnitDevice = ({ device, unit }) => {
22
22
  params: {
23
23
  unitData: unit,
24
24
  sensorData: device,
25
+ isMyUnitDeviceScreen: true,
25
26
  },
26
27
  });
27
28
  }, [navigate, device, unit]);
@@ -19,6 +19,7 @@ import { useReceiveNotifications } from '../../../hooks';
19
19
  import { SCProvider } from '../../../context';
20
20
  import { mockSCStore } from '../../../context/mockStore';
21
21
  import api from '../../../utils/Apis/axios';
22
+ import { WrapHeaderScrollable } from '../../../commons';
22
23
 
23
24
  const mock = new MockAdapter(api.axiosInstance);
24
25
 
@@ -57,8 +58,7 @@ describe('Test UnitSummary', () => {
57
58
  let route;
58
59
 
59
60
  beforeEach(() => {
60
- mock.resetHistory();
61
-
61
+ mock.reset();
62
62
  Date.now = jest.fn(() => new Date('2021-01-24T12:00:00.000Z'));
63
63
  route = {
64
64
  params: {
@@ -144,6 +144,22 @@ describe('Test UnitSummary', () => {
144
144
  expect(componentName.props.unit).toEqual({ id: 1 });
145
145
  });
146
146
 
147
+ it('render fetchUnitSummary failure', async () => {
148
+ route.params.summaryData = null;
149
+ jest.useFakeTimers();
150
+ mock.onGet(API.UNIT.UNIT_SUMMARY_DETAIL(1, 1)).reply(200, { data: {} });
151
+ mock.onGet(API.UNIT.UNIT_SUMMARY(1)).reply(400);
152
+ await act(async () => {
153
+ tree = await create(wrapComponent(route));
154
+ });
155
+ await act(async () => {
156
+ jest.runOnlyPendingTimers();
157
+ });
158
+ const instance = tree.root;
159
+ const wapHeaderScrollable = instance.findByType(WrapHeaderScrollable);
160
+ expect(wapHeaderScrollable.props.children).toBe(null);
161
+ });
162
+
147
163
  it('render fetchUnitDetail success', async () => {
148
164
  route = {
149
165
  params: {
@@ -275,4 +291,17 @@ describe('Test UnitSummary', () => {
275
291
  }
276
292
  });
277
293
  });
294
+
295
+ it('Test render without params', async () => {
296
+ useReceiveNotifications.mockImplementationOnce(() => ({
297
+ dataNotification: {},
298
+ }));
299
+ await act(async () => {
300
+ tree = await create(wrapComponent(route));
301
+ });
302
+ const instance = tree.root;
303
+ const wapHeaderScrollable = instance.findByType(WrapHeaderScrollable);
304
+ expect(wapHeaderScrollable.props.title).toBe('');
305
+ expect(wapHeaderScrollable.props.subTitle).toBe(null);
306
+ });
278
307
  });
@@ -1096,5 +1096,7 @@
1096
1096
  "read_config_permission_error": "You don't have permission to read this config",
1097
1097
  "continue_to_wait": "Continue to wait?",
1098
1098
  "it_has_been_5_minutes": "It has been 5 minutes...",
1099
- "click_here_to_setup_device": "Click here to setup device"
1099
+ "click_here_to_setup_device": "Click here to setup device",
1100
+ "photo_request_permission": "Photo request permission",
1101
+ "photo_request_permission_des": "To select photo from gallery, please unlock access photo permission"
1100
1102
  }
@@ -1094,5 +1094,7 @@
1094
1094
  "read_config_permission_error": "Bạn không có quyền đọc cấu hình này",
1095
1095
  "continue_to_wait": "Bạn có muốn đợi tiếp?",
1096
1096
  "it_has_been_5_minutes": "Đã 5 phút trôi qua...",
1097
- "click_here_to_setup_device": "Chọn thiết bị thêm vào phòng"
1097
+ "click_here_to_setup_device": "Chọn thiết bị thêm vào phòng",
1098
+ "photo_request_permission": "Quyền yêu cầu ảnh",
1099
+ "photo_request_permission_des": "Để chọn ảnh từ thư viện, vui lòng mở khóa quyền truy cập ảnh"
1098
1100
  }