@eohjsc/react-native-smart-city 0.3.3 → 0.3.4

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 (35) hide show
  1. package/package.json +3 -1
  2. package/src/commons/ActionGroup/ColorPickerTemplate.js +36 -23
  3. package/src/commons/ActionGroup/OnOffTemplate/OnOffButtonTemplate.js +8 -2
  4. package/src/commons/ActionGroup/SliderRangeTemplate.js +5 -1
  5. package/src/commons/ConnectingProcess/index.js +3 -0
  6. package/src/commons/Device/ItemDevice.js +4 -1
  7. package/src/commons/Device/WaterQualitySensor/QualityIndicatorsItem.js +7 -2
  8. package/src/commons/Form/CurrencyInput.js +15 -1
  9. package/src/commons/Form/TextInputPassword.js +1 -1
  10. package/src/commons/HeaderAni/index.js +6 -1
  11. package/src/commons/MediaPlayerDetail/index.js +11 -2
  12. package/src/commons/Sharing/MemberList.js +10 -2
  13. package/src/commons/Sharing/WrapHeaderScrollable.js +2 -0
  14. package/src/commons/SubUnit/__test__/ShortDetail.test.js +1 -1
  15. package/src/configs/Constants.js +19 -0
  16. package/src/configs/SCConfig.js +2 -0
  17. package/src/navigations/UnitStack.js +3 -20
  18. package/src/navigations/UnitStackStyles.js +21 -0
  19. package/src/screens/AddNewDevice/__test__/AddNewDevice.test.js +8 -1
  20. package/src/screens/AddNewDevice/__test__/ConnectingDevices.test.js +1 -1
  21. package/src/screens/Device/EditDevice/index.js +15 -13
  22. package/src/screens/SubUnit/AddSubUnit.js +23 -17
  23. package/src/screens/SubUnit/EditSubUnit.js +15 -13
  24. package/src/screens/SubUnit/__test__/AddSubUnit.test.js +9 -23
  25. package/src/screens/SyncLGDevice/__test__/AddLGDevice.test.js +8 -1
  26. package/src/screens/Unit/SelectAddress.js +7 -1
  27. package/src/screens/Unit/Station/index.js +3 -0
  28. package/src/screens/Unit/__test__/SelectAddress.test.js +80 -3
  29. package/src/screens/Unit/components/MyUnitDevice/index.js +4 -4
  30. package/src/screens/Unit/components/__test__/MyUnitDevice.test.js +2 -2
  31. package/src/utils/Apis/axios.js +9 -1
  32. package/src/utils/I18n/translations/en.json +3 -1
  33. package/src/utils/I18n/translations/vi.json +3 -1
  34. package/src/utils/Setting/Location.js +30 -0
  35. package/src/utils/__test__/Utils.test.js +12 -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.03",
4
+ "version": "0.3.04",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -146,6 +146,8 @@
146
146
  "react-hooks-global-state": "^1.0.1",
147
147
  "react-i18next": "^11.8.12",
148
148
  "react-native-alert-async": "^1.0.5",
149
+ "react-native-android-keyboard-adjust": "^1.2.0",
150
+ "react-native-android-location-enabler": "^1.2.2",
149
151
  "react-native-android-wifi": "^0.0.41",
150
152
  "react-native-appearance": "^0.3.4",
151
153
  "react-native-base64": "^0.1.0",
@@ -1,38 +1,51 @@
1
- import React, { memo, useCallback, useState, useEffect, useMemo } from 'react';
1
+ import React, { memo, useState, useEffect } from 'react';
2
2
  import { View } from 'react-native';
3
3
  import styles from './ColorPickerTemplateStyles';
4
4
  import ColorPicker from 'react-native-wheel-color-picker';
5
5
  import { watchMultiConfigs } from '../../iot/Monitor';
6
6
  import { useConfigGlobalState } from '../../iot/states';
7
7
 
8
+ let isFirstTime = true;
9
+
10
+ const WheelColorPicker = ({ valueColor, onChangeColor }) => {
11
+ return (
12
+ <ColorPicker
13
+ style={styles.colorPicker}
14
+ sliderHidden={true}
15
+ swatches={false}
16
+ color={valueColor}
17
+ onColorChangeComplete={onChangeColor}
18
+ thumbSize={16}
19
+ />
20
+ );
21
+ };
22
+
8
23
  const ColorPickerTemplate = memo(({ actionGroup, doAction, sensor }) => {
9
24
  const { configuration } = actionGroup;
10
25
  const [valueColorComplete, setValueColorComplete] = useState('');
11
- const [isFirstColor, setIsFirstColor] = useState(false);
12
26
  const [configValues] = useConfigGlobalState('configValues');
13
27
 
14
- const onChangeColor = useCallback(
15
- (color) => {
16
- if (!isFirstColor) {
17
- setIsFirstColor(true);
18
- return;
19
- }
28
+ const onChangeColor = (color) => {
29
+ const to = setTimeout(() => {
30
+ isFirstTime = false;
31
+ clearTimeout(to);
32
+ }, 1000);
33
+
34
+ !isFirstTime &&
20
35
  doAction(
21
36
  configuration?.action_color_data,
22
37
  JSON.stringify({ value: color })
23
38
  );
24
- },
25
- [configuration?.action_color_data, doAction, isFirstColor]
26
- );
27
- const valueColor = useMemo(() => {
28
- return valueColorComplete || '';
29
- }, [valueColorComplete]);
39
+ };
30
40
 
31
41
  useEffect(() => {
32
42
  const { config } = configuration;
33
43
  const configValue = configValues[config];
34
- setValueColorComplete(configValue);
35
- }, [configuration.config, configValues, configuration]);
44
+ if (configValue && isFirstTime) {
45
+ setValueColorComplete(`#${configValue?.toString(16)}`);
46
+ }
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [configValues]);
36
49
 
37
50
  useEffect(() => {
38
51
  if (sensor?.is_managed_by_backend && sensor.device_type !== 'GOOGLE_HOME') {
@@ -40,15 +53,15 @@ const ColorPickerTemplate = memo(({ actionGroup, doAction, sensor }) => {
40
53
  }
41
54
  }, [sensor, configuration.config]);
42
55
 
56
+ useEffect(() => {
57
+ return () => (isFirstTime = true);
58
+ }, []);
59
+
43
60
  return (
44
61
  <View style={styles.viewPickColor}>
45
- <ColorPicker
46
- style={styles.colorPicker}
47
- sliderHidden={true}
48
- swatches={false}
49
- color={valueColor}
50
- onColorChangeComplete={onChangeColor}
51
- thumbSize={16}
62
+ <WheelColorPicker
63
+ valueColor={valueColorComplete}
64
+ onChangeColor={onChangeColor}
52
65
  />
53
66
  </View>
54
67
  );
@@ -5,15 +5,20 @@ import React, { memo } from 'react';
5
5
  import { TouchableOpacity, View } from 'react-native';
6
6
  import { Colors } from '../../../configs';
7
7
  import styles from './OnOffButtonTemplateStyle';
8
+ import { TESTID } from '../../../configs/Constants';
8
9
 
9
10
  const OnOffButtonTemplate = memo(
10
11
  ({ isOn, triggerAction, actionGroup, isLight = false }) => {
11
- const { configuration } = actionGroup;
12
+ const { configuration, id } = actionGroup;
12
13
 
13
14
  return (
14
15
  <>
15
16
  <View style={styles.barrierControlContainer}>
16
- <TouchableOpacity style={styles.bigCircle} onPress={triggerAction}>
17
+ <TouchableOpacity
18
+ style={styles.bigCircle}
19
+ onPress={triggerAction}
20
+ testID={`${TESTID.ON_OFF_BUTTON}-${id}`}
21
+ >
17
22
  <View style={styles.smallCircle}>
18
23
  <Icon
19
24
  name={isOn ? configuration.icon_on : configuration.icon_off}
@@ -25,6 +30,7 @@ const OnOffButtonTemplate = memo(
25
30
  styles.textBig,
26
31
  { color: isOn ? Colors.Gray8 : Colors.Gray6 },
27
32
  ]}
33
+ testID={`${TESTID.SENSOR_STATUS}-${id}`}
28
34
  >
29
35
  {isOn ? configuration.text_on : configuration.text_off}
30
36
  </Text>
@@ -33,7 +33,11 @@ const SliderRangeTemplate = memo(({ actionGroup, doAction, sensor }) => {
33
33
  useEffect(() => {
34
34
  const { config } = configuration;
35
35
  const configValue = configValues[config];
36
- setValueBrightness(configValue);
36
+ let valueTemp = configValue;
37
+ if (configValue > 0) {
38
+ valueTemp = Math.round((configValue / 254) * 100);
39
+ }
40
+ setValueBrightness(valueTemp);
37
41
  }, [configuration.config, configValues, configuration]);
38
42
 
39
43
  useEffect(() => {
@@ -45,6 +45,7 @@ const ConnectingProcess = ({ route }) => {
45
45
  );
46
46
  if (success) {
47
47
  setSensor(data);
48
+ setNewName(data?.name);
48
49
  } else {
49
50
  ToastBottomHelper.error(JSON.stringify(data));
50
51
  goBack();
@@ -64,6 +65,7 @@ const ConnectingProcess = ({ route }) => {
64
65
  });
65
66
  if (success) {
66
67
  setSensor(data);
68
+ setNewName(data?.name);
67
69
  } else {
68
70
  ToastBottomHelper.error(JSON.stringify(data));
69
71
  goBack();
@@ -80,6 +82,7 @@ const ConnectingProcess = ({ route }) => {
80
82
  );
81
83
  if (success) {
82
84
  setSensor({ name: gateway?.model });
85
+ setNewName(gateway?.model);
83
86
  } else {
84
87
  ToastBottomHelper.error(JSON.stringify(data));
85
88
  goBack();
@@ -72,7 +72,10 @@ const ItemDevice = memo(
72
72
  const textConnected = isConnected ? t('connected') : t('disconnected');
73
73
 
74
74
  return (
75
- <TouchableWithoutFeedback onPress={goToSensorDisplay}>
75
+ <TouchableWithoutFeedback
76
+ onPress={goToSensorDisplay}
77
+ testID={`${TESTID.SENSOR_NAME}-${sensor?.id}`}
78
+ >
76
79
  <View
77
80
  style={[styles.container, wrapStyle, { borderColor }]}
78
81
  testID={TESTID.SUB_UNIT_DEVICES}
@@ -37,7 +37,7 @@ const QualityIndicatorItem = memo(
37
37
  >
38
38
  <IconOutline
39
39
  name={'info-circle'}
40
- size={16}
40
+ size={18}
41
41
  color={Colors.Gray8}
42
42
  />
43
43
  </TouchableOpacity>
@@ -92,6 +92,11 @@ const styles = StyleSheet.create({
92
92
  marginTop: 8,
93
93
  },
94
94
  iconInfo: {
95
- top: 10,
95
+ width: 40,
96
+ height: 40,
97
+ justifyContent: 'center',
98
+ alignItems: 'center',
99
+ marginRight: -10,
100
+ marginTop: -2,
96
101
  },
97
102
  });
@@ -1,4 +1,10 @@
1
- import React, { useState, useRef, useCallback, useMemo } from 'react';
1
+ import React, {
2
+ useState,
3
+ useRef,
4
+ useCallback,
5
+ useMemo,
6
+ useEffect,
7
+ } from 'react';
2
8
  import {
3
9
  View,
4
10
  TextInput,
@@ -6,6 +12,7 @@ import {
6
12
  TouchableWithoutFeedback,
7
13
  Platform,
8
14
  } from 'react-native';
15
+ import AndroidKeyboardAdjust from 'react-native-android-keyboard-adjust';
9
16
  import Text from '../Text';
10
17
  import { Colors } from '../../configs';
11
18
 
@@ -125,6 +132,13 @@ const CurrencyInput = ({
125
132
  return textInputValue.toString().length < 2 ? -12 : 0;
126
133
  }, [textInputValue]);
127
134
 
135
+ useEffect(() => {
136
+ const isAndroid = Platform.OS === 'android';
137
+ isAndroid && AndroidKeyboardAdjust.setAdjustResize();
138
+
139
+ return () => isAndroid && AndroidKeyboardAdjust.setAdjustPan();
140
+ }, []);
141
+
128
142
  return (
129
143
  <TouchableWithoutFeedback onPress={focusInput}>
130
144
  <View style={styles.wrap}>
@@ -90,7 +90,7 @@ const styles = StyleSheet.create({
90
90
  borderRadius: 2,
91
91
  paddingTop: 12,
92
92
  paddingBottom: 12,
93
- paddingHorizontal: 20,
93
+ paddingRight: 20,
94
94
  backgroundColor: Colors.White,
95
95
  // ...Theme.shadow,
96
96
  shadowColor: Colors.Gray13,
@@ -6,6 +6,7 @@ import { getStatusBarHeight } from 'react-native-iphone-x-helper';
6
6
 
7
7
  import Text from '../../commons/Text';
8
8
  import { Colors, Constants } from '../../configs';
9
+ import { TESTID } from '../../configs/Constants';
9
10
 
10
11
  const screenHeight = Constants.height;
11
12
  const default_height = 44;
@@ -89,7 +90,11 @@ const HeaderAni = memo(
89
90
  },
90
91
  ]}
91
92
  >
92
- <TouchableOpacity style={styles.btnBack} onPress={onPressLeft}>
93
+ <TouchableOpacity
94
+ style={styles.btnBack}
95
+ onPress={onPressLeft}
96
+ testID={TESTID.ICON_BACK}
97
+ >
93
98
  <Icon name={'left'} size={27} color={Colors.Gray9} />
94
99
  </TouchableOpacity>
95
100
  <View styles={styles.wrapRightComponent}>{rightComponent}</View>
@@ -6,7 +6,14 @@ import React, {
6
6
  useContext,
7
7
  useMemo,
8
8
  } from 'react';
9
- import { Image, View, StyleSheet, Text, TouchableOpacity } from 'react-native';
9
+ import {
10
+ Image,
11
+ View,
12
+ StyleSheet,
13
+ Text,
14
+ TouchableOpacity,
15
+ Platform,
16
+ } from 'react-native';
10
17
  import { VLCPlayer } from 'react-native-vlc-media-player';
11
18
  import { useTranslations } from '../../hooks/Common/useTranslations';
12
19
 
@@ -68,7 +75,9 @@ const MediaPlayerDetail = ({
68
75
  newHeight = 224;
69
76
  break;
70
77
  case 4:
71
- newWidth = Constants.width / 1.5;
78
+ newWidth =
79
+ Constants.width /
80
+ (Platform.OS === 'ios' && Constants.width < 400 ? 1.5 : 2);
72
81
  newHeight = 112;
73
82
  break;
74
83
  case 6:
@@ -1,7 +1,7 @@
1
1
  import React, { memo } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
3
  import { useTranslations } from '../../hooks/Common/useTranslations';
4
- import { Colors } from '../../configs';
4
+ import { Colors, Constants } from '../../configs';
5
5
  import Text from '../../commons/Text';
6
6
  import RowMember from './RowMember';
7
7
 
@@ -26,7 +26,9 @@ const MemberList = ({
26
26
  />
27
27
  ))}
28
28
  {!dataMember.length && (
29
- <Text style={styles.textCenter}>{t('no_member')}</Text>
29
+ <View style={styles.viewEmpty}>
30
+ <Text style={styles.textCenter}>{t('no_member')}</Text>
31
+ </View>
30
32
  )}
31
33
  </View>
32
34
  );
@@ -37,4 +39,10 @@ const styles = StyleSheet.create({
37
39
  box: {
38
40
  backgroundColor: Colors.White,
39
41
  },
42
+ viewEmpty: {
43
+ justifyContent: 'center',
44
+ alignItems: 'center',
45
+ height: Constants.height - 200,
46
+ backgroundColor: Colors.White,
47
+ },
40
48
  });
@@ -13,6 +13,7 @@ import { isIphoneX } from 'react-native-iphone-x-helper';
13
13
  import { Colors, Theme } from '../../configs';
14
14
  import HeaderAni, { heightHeader } from '../../commons/HeaderAni';
15
15
  import Text from '../../commons/Text';
16
+ import { TESTID } from '../../configs/Constants';
16
17
 
17
18
  const WrapHeaderScrollable = ({
18
19
  children,
@@ -59,6 +60,7 @@ const WrapHeaderScrollable = ({
59
60
  onLeft={onGoBack}
60
61
  />
61
62
  <Animated.ScrollView
63
+ testID={TESTID.ANIMATED_SCROLL}
62
64
  scrollEventThrottle={16}
63
65
  onScroll={Animated.event(
64
66
  [{ nativeEvent: { contentOffset: { y: animatedScrollYValue } } }],
@@ -151,7 +151,7 @@ describe('test ShortDetail Subunit', () => {
151
151
  (item) =>
152
152
  item.props.testID === TESTID.SUB_UNIT_DEVICES && item.type === View
153
153
  );
154
- expect(itemDevice.length).toBe(1);
154
+ expect(itemDevice.length).toBe(0);
155
155
  });
156
156
 
157
157
  test('render ShortDetail add new device', async () => {
@@ -288,6 +288,8 @@ export const TESTID = {
288
288
  SUB_UNIT_TEXT_DROPDOWN: 'SUB_UNIT_TEXT_DROPDOWN',
289
289
  SUB_UNIT_GO_DETAIL: 'SUB_UNIT_GO_DETAIL',
290
290
  VIEW_SUB_UNIT_AUTOMATE: 'VIEW_SUB_UNIT_AUTOMATE',
291
+ SUB_UNIT_NAME: 'SUB_UNIT_NAME',
292
+ ANIMATED_SCROLL: 'ANIMATED_SCROLL',
291
293
  ADD_SUB_UNIT: 'ADD_SUB_UNIT',
292
294
 
293
295
  // NavBar
@@ -392,6 +394,7 @@ export const TESTID = {
392
394
  ITEM_TEXT_ERROR: 'ITEM_TEXT_ERROR',
393
395
  SEARCH_BAR_INPUT: 'SEARCH_BAR_INPUT',
394
396
  GO_DETAIL: 'GO_DETAIL',
397
+ NAV_LIST: 'NAV_LIST',
395
398
 
396
399
  // Automate
397
400
  AUTOMATE_SCRIPT_ACTION: 'AUTOMATE_SCRIPT_ACTION',
@@ -447,8 +450,13 @@ export const TESTID = {
447
450
  ITEM_QUICK_ACTION_PRESS: 'ITEM_QUICK_ACTION_PRESS',
448
451
  TIME_COUNT_DOWN_TEXT: 'TIME_COUNT_DOWN_TEXT',
449
452
  ACTION_ITEM: 'ACTION_ITEM',
453
+
450
454
  // Sensor Item
451
455
  TEXT_SENSOR_ITEM: 'TEXT_SENSOR_ITEM',
456
+ SENSOR_NAME: 'SENSOR_NAME',
457
+ ON_OFF_BUTTON: 'ON_OFF_BUTTON',
458
+ SENSOR_STATUS: 'SENSOR_STATUS',
459
+ ICON_BACK: 'ICON_BACK',
452
460
 
453
461
  // DeviceInfo
454
462
  DEVICE_INFO_BATTERY: 'DEVICE_INFO_BATTERY',
@@ -564,6 +572,7 @@ export const TESTID = {
564
572
  ICON_ADD_STAR_SHARED_UNIT: 'ICON_ADD_STAR_SHARED_UNIT',
565
573
  TOUCH_SHARED_UNIT: 'TOUCH_SHARED_UNIT',
566
574
  SHARED_UNIT_OWN_BY: 'SHARED_UNIT_OWN_BY',
575
+ ICON_UNIT: 'ICON_UNIT',
567
576
 
568
577
  // Select subunit
569
578
 
@@ -809,3 +818,13 @@ export const SENSOR_TYPE = {
809
818
  SOS: 'sos',
810
819
  FILTER_WATER: 'filter_water',
811
820
  };
821
+
822
+ export const PROBLEM_CODE = {
823
+ CLIENT_ERROR: 'CLIENT_ERROR',
824
+ SERVER_ERROR: 'SERVER_ERROR',
825
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
826
+ CONNECTION_ERROR: 'CONNECTION_ERROR',
827
+ NETWORK_ERROR: 'NETWORK_ERROR',
828
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
829
+ CANCEL_ERROR: 'CANCEL_ERROR',
830
+ };
@@ -107,11 +107,13 @@ export class SCConfig {
107
107
  static VCONNEX_REDIRECT_URI_APP = SCDefaultConfig.VCONNEX_REDIRECT_URI_APP;
108
108
  static pusherAppKey = SCDefaultConfig.pusherAppKey;
109
109
  static pusherAppCluste = SCDefaultConfig.pusherAppCluster;
110
+ static language = 'en';
110
111
  }
111
112
 
112
113
  export const initSCConfig = (config) => {
113
114
  api.setBaseURL(config.apiRoot ?? SCDefaultConfig.apiRoot);
114
115
  LocaleConfig.defaultLocale = config.language;
116
+ SCConfig.language = config.language ?? SCConfig.language;
115
117
  SCConfig.apiRoot = config.apiRoot ?? SCDefaultConfig.apiRoot;
116
118
  SCConfig.GOOGLE_MAP_API_KEY =
117
119
  config.GOOGLE_MAP_API_KEY ?? SCDefaultConfig.GOOGLE_MAP_API_KEY;
@@ -1,5 +1,5 @@
1
1
  import React, { memo, useContext, useEffect } from 'react';
2
- import { View, StyleSheet } from 'react-native';
2
+ import { View } from 'react-native';
3
3
  import { IconOutline } from '@ant-design/icons-react-native';
4
4
  import { createStackNavigator } from '@react-navigation/stack';
5
5
  import { BleManager } from 'react-native-ble-plx';
@@ -52,10 +52,10 @@ import ConfirmUnitDeletion from '../screens/ConfirmUnitDeletion';
52
52
  import InfoMemberUnit from '../screens/Sharing/InfoMemberUnit';
53
53
  import EnterPassword from '../screens/EnterPassword';
54
54
  import { HanetCameraStack } from './HanetCameraStack';
55
-
56
55
  import { axiosGet } from '../utils/Apis/axios';
57
56
  import { API } from '../configs';
58
57
  import SideMenuDetail from '../screens/SideMenuDetail';
58
+ import { styles } from './UnitStackStyles';
59
59
 
60
60
  const Stack = createStackNavigator();
61
61
 
@@ -173,12 +173,9 @@ export const UnitStack = memo((props) => {
173
173
  component={ChooseLocation}
174
174
  options={{
175
175
  headerShown: true,
176
- headerTitleStyle: {
177
- ...styles.headerLocation,
178
- },
179
176
  headerTitle: () => (
180
177
  <View style={styles.headerLocation}>
181
- <Text type="H3" color={Colors.Gray9} bold>
178
+ <Text color={Colors.Gray9} style={styles.headerTitle} bold>
182
179
  {t('choose_on_map')}
183
180
  </Text>
184
181
 
@@ -406,17 +403,3 @@ export const UnitStack = memo((props) => {
406
403
  </Stack.Navigator>
407
404
  );
408
405
  });
409
-
410
- const styles = StyleSheet.create({
411
- icLeft: {
412
- marginLeft: Device.isIOS ? 8 : 0,
413
- },
414
- headerLocation: {
415
- alignItems: 'center',
416
- justifyContent: 'center',
417
- },
418
- headerContent: {
419
- textAlign: 'center',
420
- paddingBottom: 8,
421
- },
422
- });
@@ -0,0 +1,21 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Device } from '../configs';
3
+
4
+ export const styles = StyleSheet.create({
5
+ icLeft: {
6
+ marginLeft: Device.isIOS ? 8 : 0,
7
+ },
8
+ headerLocation: {
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ width: 290,
12
+ },
13
+ headerTitle: {
14
+ fontSize: 20,
15
+ lineHeight: 28,
16
+ },
17
+ headerContent: {
18
+ textAlign: 'center',
19
+ paddingBottom: 4,
20
+ },
21
+ });
@@ -53,6 +53,7 @@ describe('Test AddNewDevice', () => {
53
53
  unit_id: 1,
54
54
  },
55
55
  };
56
+ mock.resetHistory();
56
57
  });
57
58
 
58
59
  const getText = (instance, id) => {
@@ -103,7 +104,13 @@ describe('Test AddNewDevice', () => {
103
104
  });
104
105
  const instance = tree.root;
105
106
  const groupCheckBox = instance.findByType(GroupCheckBox);
106
- expect(groupCheckBox.props.data).toEqual([]);
107
+ expect(groupCheckBox.props.data).toEqual([
108
+ {
109
+ id: 2,
110
+ name: 'Station name',
111
+ title: 'Station name',
112
+ },
113
+ ]);
107
114
  });
108
115
 
109
116
  test('ViewButtonBottom', async () => {
@@ -103,7 +103,7 @@ describe('Test ConnectingDevices', () => {
103
103
  await jest.runOnlyPendingTimers();
104
104
  });
105
105
  expect(setInterval).toHaveBeenCalled();
106
- expect(mockedNavigate).not.toHaveBeenCalledWith(Routes.ConnectDevices, {
106
+ expect(mockedNavigate).toHaveBeenCalledWith(Routes.ConnectDevices, {
107
107
  new_sensor: { id: 1 },
108
108
  });
109
109
  });
@@ -104,20 +104,22 @@ const EditDevice = memo(() => {
104
104
  />
105
105
  </View>
106
106
  </TouchableOpacity>
107
- <TouchableOpacity
108
- style={styles.removeButton}
109
- onPress={onShowDelete(sensor?.name)}
110
- testID={TESTID.DEVICE_SHOW_REMOVE}
111
- >
112
- <Text
113
- type={'H4'}
114
- semibold
115
- color={Colors.Red}
116
- style={styles.removeBorderBottom}
107
+ {!stateAlertAction.visible && (
108
+ <TouchableOpacity
109
+ style={styles.removeButton}
110
+ onPress={onShowDelete(sensor?.name)}
111
+ testID={TESTID.DEVICE_SHOW_REMOVE}
117
112
  >
118
- {t('remove_device')}
119
- </Text>
120
- </TouchableOpacity>
113
+ <Text
114
+ type={'H4'}
115
+ semibold
116
+ color={Colors.Red}
117
+ style={styles.removeBorderBottom}
118
+ >
119
+ {t('remove_device')}
120
+ </Text>
121
+ </TouchableOpacity>
122
+ )}
121
123
  </View>
122
124
  <AlertAction
123
125
  visible={stateAlertAction.visible}
@@ -25,11 +25,12 @@ import { ToastBottomHelper } from '../../utils/Utils';
25
25
  import { TESTID } from '../../configs/Constants';
26
26
  import styles from './AddSubUnitStyles';
27
27
  import useKeyboardShow from '../../hooks/Common/useKeyboardShow';
28
+ import { replace } from '../../navigations/utils';
28
29
 
29
30
  const AddSubUnit = ({ route }) => {
30
31
  const t = useTranslations();
31
32
  const { dismissKeyboard } = useKeyboardShow();
32
- const { navigate, goBack } = useNavigation();
33
+ const { navigate, goBack, dispatch } = useNavigation();
33
34
  const { unit, addType, isAddUnit, location = '' } = route?.params;
34
35
  const [roomName, setRoomName] = useState('');
35
36
  const [wallpaper, setWallpaper] = useState('');
@@ -62,13 +63,15 @@ const AddSubUnit = ({ route }) => {
62
63
  }
63
64
  );
64
65
  if (success) {
65
- navigate(Routes.UnitStack, {
66
- screen: Routes.UnitDetail,
67
- params: {
68
- unitId: data?.id,
69
- routeName: Routes.DashboardStack,
70
- },
71
- });
66
+ dispatch(
67
+ replace(Routes.UnitStack, {
68
+ screen: Routes.UnitDetail,
69
+ params: {
70
+ unitId: data?.id,
71
+ routeName: Routes.DashboardStack,
72
+ },
73
+ })
74
+ );
72
75
  cleanData();
73
76
  } else {
74
77
  awaitCreate.current = false;
@@ -98,15 +101,17 @@ const AddSubUnit = ({ route }) => {
98
101
  });
99
102
  return;
100
103
  }
101
- navigate(Routes.UnitStack, {
102
- screen: Routes.UnitDetail,
103
- params: {
104
- unitId: unit.id,
105
- unitData: unit,
106
- isAddSubUnit: true,
107
- routeName: Routes.DashboardStack,
108
- },
109
- });
104
+ dispatch(
105
+ replace(Routes.UnitStack, {
106
+ screen: Routes.UnitDetail,
107
+ params: {
108
+ unitId: unit.id,
109
+ unitData: unit,
110
+ isAddSubUnit: true,
111
+ routeName: Routes.DashboardStack,
112
+ },
113
+ })
114
+ );
110
115
  cleanData();
111
116
  } else {
112
117
  awaitCreate.current = false;
@@ -123,6 +128,7 @@ const AddSubUnit = ({ route }) => {
123
128
  t,
124
129
  unit,
125
130
  addType,
131
+ dispatch,
126
132
  goBack,
127
133
  route.params,
128
134
  ]);
@@ -228,20 +228,22 @@ const EditSubUnit = ({ route }) => {
228
228
  setImageUrl={setImageUrl}
229
229
  optionsCapture={options}
230
230
  />
231
- <TouchableOpacity
232
- testID={TESTID.MANAGE_SUB_UNIT_REMOVE_BUTTON}
233
- onPress={onPressRemove}
234
- style={styles.removeButton}
235
- >
236
- <Text
237
- type={'H4'}
238
- semibold
239
- color={Colors.Red}
240
- style={styles.removeText}
231
+ {!showEdit && (
232
+ <TouchableOpacity
233
+ testID={TESTID.MANAGE_SUB_UNIT_REMOVE_BUTTON}
234
+ onPress={onPressRemove}
235
+ style={styles.removeButton}
241
236
  >
242
- {t('remove_sub_unit')}
243
- </Text>
244
- </TouchableOpacity>
237
+ <Text
238
+ type={'H4'}
239
+ semibold
240
+ color={Colors.Red}
241
+ style={styles.removeText}
242
+ >
243
+ {t('remove_sub_unit')}
244
+ </Text>
245
+ </TouchableOpacity>
246
+ )}
245
247
  </View>
246
248
  </View>
247
249
  <ModalCustom
@@ -36,13 +36,20 @@ jest.mock('react-redux', () => {
36
36
  };
37
37
  });
38
38
 
39
+ const mockNavigationDispatch = jest.fn();
40
+ const mockReplace = jest.fn();
41
+
39
42
  jest.mock('@react-navigation/native', () => {
40
43
  return {
41
44
  ...jest.requireActual('@react-navigation/native'),
42
45
  useNavigation: () => ({
43
46
  navigate: mockedNavigate,
44
47
  goBack: mockedGoBack,
48
+ dispatch: mockNavigationDispatch,
45
49
  }),
50
+ StackActions: {
51
+ replace: () => mockReplace,
52
+ },
46
53
  };
47
54
  });
48
55
 
@@ -63,6 +70,7 @@ describe('Test AddSubUnit', () => {
63
70
  afterEach(() => {
64
71
  mockedDispatch.mockClear();
65
72
  mockedNavigate.mockClear();
73
+ mockNavigationDispatch.mockClear();
66
74
  Toast.show.mockClear();
67
75
  });
68
76
  let tree;
@@ -150,15 +158,6 @@ describe('Test AddSubUnit', () => {
150
158
  await act(async () => {
151
159
  await viewButtonBottom.props.onRightClick();
152
160
  });
153
- expect(mockedNavigate).not.toHaveBeenCalledWith(Routes.UnitStack, {
154
- screen: Routes.UnitDetail,
155
- params: {
156
- unitId: route.params.unit.id,
157
- unitData: route.params.unit,
158
- routeName: 'DashboardStack',
159
- isAddSubUnit: true,
160
- },
161
- });
162
161
  expect(Toast.show).toHaveBeenCalledWith({
163
162
  type: 'error',
164
163
  position: 'bottom',
@@ -186,13 +185,6 @@ describe('Test AddSubUnit', () => {
186
185
  });
187
186
 
188
187
  test('test create Unit', async () => {
189
- const response = {
190
- success: true,
191
- status: 200,
192
- data: {
193
- id: 2,
194
- },
195
- };
196
188
  route.params = {
197
189
  ...route.params,
198
190
  location: 'Unit address',
@@ -211,13 +203,7 @@ describe('Test AddSubUnit', () => {
211
203
  await act(async () => {
212
204
  await viewButtonBottom.props.onRightClick();
213
205
  });
214
- expect(mockedNavigate).toHaveBeenCalledWith(Routes.UnitStack, {
215
- screen: Routes.UnitDetail,
216
- params: {
217
- unitId: response.data.id,
218
- routeName: Routes.DashboardStack,
219
- },
220
- });
206
+ expect(mockNavigationDispatch).toBeCalledWith(mockReplace);
221
207
  });
222
208
 
223
209
  test('test choose Location', async () => {
@@ -53,6 +53,7 @@ describe('Test Add LG Device', () => {
53
53
  backend_url: 'https://doamin.com',
54
54
  },
55
55
  };
56
+ mock.resetHistory();
56
57
  });
57
58
 
58
59
  const getText = (instance, id) => {
@@ -105,7 +106,13 @@ describe('Test Add LG Device', () => {
105
106
  });
106
107
  const instance = tree.root;
107
108
  const groupCheckBox = instance.findByType(GroupCheckBox);
108
- expect(groupCheckBox.props.data).toEqual([]);
109
+ expect(groupCheckBox.props.data).toEqual([
110
+ {
111
+ id: 2,
112
+ name: 'Station name',
113
+ title: 'Station name',
114
+ },
115
+ ]);
109
116
  });
110
117
 
111
118
  test('ViewButtonBottom', async () => {
@@ -4,6 +4,7 @@ import MapView, { Marker, Circle, PROVIDER_GOOGLE } from 'react-native-maps';
4
4
  import { useNavigation } from '@react-navigation/native';
5
5
  import { IconOutline, IconFill } from '@ant-design/icons-react-native';
6
6
  import { check, RESULTS } from 'react-native-permissions';
7
+ import { openPromptEnableLocation } from '../../utils/Setting/Location';
7
8
 
8
9
  import BottomButtonView from '../../commons/BottomButtonView';
9
10
  import SearchBarLocation from '../../commons/SearchLocation';
@@ -151,9 +152,14 @@ const SelectAddress = memo(({ route }) => {
151
152
  t('location_rationale_title'),
152
153
  t('location_require_message')
153
154
  );
155
+ } else if (error.code === GEOLOCATION_ERROR.POSITION_UNAVAILABLE) {
156
+ const locationEnabaled = await openPromptEnableLocation();
157
+ process.env.NODE_ENV !== 'test' &&
158
+ locationEnabaled &&
159
+ getCurrentPosition();
154
160
  }
155
161
  }
156
- // { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 } enable on emulator
162
+ // { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 } // enable on emulator
157
163
  );
158
164
  }, [t]);
159
165
 
@@ -1,5 +1,6 @@
1
1
  import React, { memo, useEffect, useRef, useState } from 'react';
2
2
  import { View, Text, TouchableOpacity, FlatList } from 'react-native';
3
+ import { TESTID } from '../../../configs/Constants';
3
4
  import styles from './StationStyles';
4
5
 
5
6
  const Station = ({ listStation = [], onSnapToItem, indexStation }) => {
@@ -23,6 +24,7 @@ const Station = ({ listStation = [], onSnapToItem, indexStation }) => {
23
24
  key={index}
24
25
  style={styles.wrapTitle}
25
26
  onPress={handleOnSnapToItem(item, index)}
27
+ testID={`${TESTID.SUB_UNIT_NAME}-${item?.station?.id}`}
26
28
  >
27
29
  <Text
28
30
  style={[styles.title, index === indexStation && styles.titleActive]}
@@ -71,6 +73,7 @@ const Station = ({ listStation = [], onSnapToItem, indexStation }) => {
71
73
  renderItem={renderItem}
72
74
  showsHorizontalScrollIndicator={false}
73
75
  scrollIndicatorInsets={{ right: 1 }}
76
+ testID={TESTID.NAV_LIST}
74
77
  />
75
78
  </View>
76
79
  );
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
+ import { Alert } from 'react-native';
2
3
  import { act, create } from 'react-test-renderer';
3
4
  import MockAdapter from 'axios-mock-adapter';
4
5
  import RNP from 'react-native-permissions';
6
+ import RNAndroidLocationEnabler from 'react-native-android-location-enabler';
5
7
 
6
8
  import { SCProvider } from '../../../context';
7
9
  import { mockSCStore } from '../../../context/mockStore';
@@ -62,6 +64,13 @@ jest.mock('react-native-maps', () => {
62
64
  };
63
65
  });
64
66
 
67
+ jest.mock('react-native-android-location-enabler', () => {
68
+ return {
69
+ ...jest.requireActual('react-native-android-location-enabler'),
70
+ promptForEnableLocationIfNeeded: jest.fn(),
71
+ };
72
+ });
73
+
65
74
  jest.mock('../../../utils/Permission/common');
66
75
 
67
76
  const position = {
@@ -82,11 +91,12 @@ jest.mock('react-native-permissions', () => {
82
91
  });
83
92
 
84
93
  describe('Test SelectAddress', () => {
85
- let tree;
86
- let route;
94
+ let tree, route, Platform;
87
95
  const mockUpdateLocation = jest.fn();
88
96
 
89
97
  beforeAll(() => {
98
+ Platform = require('react-native').Platform;
99
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded.mockClear();
90
100
  RNP.check.mockClear();
91
101
  route = {
92
102
  params: {
@@ -209,7 +219,74 @@ describe('Test SelectAddress', () => {
209
219
  OpenSetting.mockClear();
210
220
  });
211
221
 
212
- test('test get current failed error not handle', async () => {
222
+ test('test get current location failed location not enabled android', async () => {
223
+ Platform.OS = 'android';
224
+ await act(async () => {
225
+ tree = await create(wrapComponent(route));
226
+ });
227
+ const instance = tree.root;
228
+ const button = instance.find(
229
+ (el) => el.props.testID === TESTID.BUTTON_YOUR_LOCATION
230
+ );
231
+
232
+ global.navigator.geolocation = {
233
+ getCurrentPosition: (onSuccess, onError, options) => {
234
+ onError({ code: GEOLOCATION_ERROR.POSITION_UNAVAILABLE });
235
+ },
236
+ };
237
+
238
+ // cancel
239
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded.mockImplementationOnce(
240
+ async () => {
241
+ throw new Error();
242
+ }
243
+ );
244
+ await act(async () => {
245
+ await button.props.onPress();
246
+ });
247
+ expect(
248
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded
249
+ ).toBeCalled();
250
+
251
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded.mockClear();
252
+
253
+ // enabled
254
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded.mockImplementationOnce(
255
+ async () => true
256
+ );
257
+ await act(async () => {
258
+ await button.props.onPress();
259
+ });
260
+ expect(
261
+ RNAndroidLocationEnabler.promptForEnableLocationIfNeeded
262
+ ).toBeCalled();
263
+ });
264
+
265
+ test('test get current location failed location not enabled ios', async () => {
266
+ Platform.OS = 'ios';
267
+ jest.spyOn(Alert, 'alert');
268
+
269
+ await act(async () => {
270
+ tree = await create(wrapComponent(route));
271
+ });
272
+ const instance = tree.root;
273
+ const button = instance.find(
274
+ (el) => el.props.testID === TESTID.BUTTON_YOUR_LOCATION
275
+ );
276
+
277
+ global.navigator.geolocation = {
278
+ getCurrentPosition: (onSuccess, onError, options) => {
279
+ onError({ code: GEOLOCATION_ERROR.POSITION_UNAVAILABLE });
280
+ },
281
+ };
282
+
283
+ await act(async () => {
284
+ await button.props.onPress();
285
+ });
286
+ // expect AlertAsync is called
287
+ });
288
+
289
+ test('test get current location failed error not handle', async () => {
213
290
  await act(async () => {
214
291
  tree = await create(wrapComponent(route));
215
292
  });
@@ -1,12 +1,12 @@
1
1
  import React, { useState, useCallback } from 'react';
2
- import { Image, StyleSheet, View, TouchableOpacity } from 'react-native';
2
+ import { StyleSheet, View, TouchableOpacity } from 'react-native';
3
3
  import { useNavigation } from '@react-navigation/native';
4
4
 
5
5
  import ItemQuickAction from '../../../../commons/Action/ItemQuickAction';
6
6
  import Text from '../../../../commons/Text';
7
7
  import Routes from '../../../../utils/Route';
8
-
9
- import { Colors, Images } from '../../../../configs';
8
+ import { Colors } from '../../../../configs';
9
+ import FImage from '../../../../commons/FImage';
10
10
 
11
11
  const MyUnitDevice = ({ sensor, unit }) => {
12
12
  const [status, setStatus] = useState(sensor.status);
@@ -26,7 +26,7 @@ const MyUnitDevice = ({ sensor, unit }) => {
26
26
  <View style={styles.item}>
27
27
  <TouchableOpacity style={styles.flex1} onPress={goToSensorDisplay}>
28
28
  <View style={styles.rowCenter}>
29
- <Image style={styles.image} source={Images.mainDoor} />
29
+ <FImage style={styles.image} source={{ uri: sensor?.icon_kit }} />
30
30
  <View style={styles.marginTop3}>
31
31
  <Text semibold style={styles.nameDevice}>
32
32
  {sensor.name}
@@ -43,7 +43,7 @@ describe('Test MyUnitDevice', () => {
43
43
  });
44
44
  const instance = tree.root;
45
45
  const Views = instance.findAllByType(View);
46
- expect(Views).toHaveLength(11);
46
+ expect(Views).toHaveLength(12);
47
47
 
48
48
  const touches = instance.findAllByType(TouchableOpacity);
49
49
  expect(touches).toHaveLength(1);
@@ -60,6 +60,6 @@ describe('Test MyUnitDevice', () => {
60
60
  });
61
61
  const instance = tree.root;
62
62
  const Views = instance.findAllByType(View);
63
- expect(Views).toHaveLength(11);
63
+ expect(Views).toHaveLength(12);
64
64
  });
65
65
  });
@@ -2,6 +2,9 @@ import { create } from 'apisauce';
2
2
  import { getData, storeData } from '../Storage';
3
3
  import { ToastBottomHelper } from '../Utils';
4
4
  import NetInfo from '@react-native-community/netinfo';
5
+ import { PROBLEM_CODE } from '../../configs/Constants';
6
+ import { getTranslate } from '../I18n';
7
+ import { SCConfig } from '../../configs';
5
8
 
6
9
  const api = create({
7
10
  headers: {
@@ -39,6 +42,11 @@ const parseErrorResponse = async (error) => {
39
42
  }
40
43
  }
41
44
  if (!hideError) {
45
+ switch (error.problem) {
46
+ case PROBLEM_CODE.SERVER_ERROR:
47
+ message = getTranslate(SCConfig.language, 'server_error');
48
+ break;
49
+ }
42
50
  ToastBottomHelper.error(message);
43
51
  }
44
52
  }
@@ -102,7 +110,7 @@ export async function axiosGet(URL, config = {}, cache = false) {
102
110
  return await parseErrorResponse(error);
103
111
  }
104
112
  const { data, problem } = response;
105
- if (problem === 'NETWORK_ERROR') {
113
+ if (problem) {
106
114
  if (cache) {
107
115
  return (
108
116
  (await axiosCache(URL, 500)) || (await parseErrorResponse(response))
@@ -989,5 +989,7 @@
989
989
  "This {name} was removed!" : "This {name} was removed!",
990
990
  "camera_request_permission": "Camera request permission",
991
991
  "camera_request_permission_des": "To use camera, please unlock camera permission",
992
- "location_require_message": "EoH needs your location permission to get current location."
992
+ "location_require_message": "EoH needs your location permission to get current location.",
993
+ "turn_on_location_service": "Turn on location service for your phone",
994
+ "turn_on_location_service_des": "To continue, let your device turn on location by turning on location service in Settings"
993
995
  }
@@ -990,5 +990,7 @@
990
990
  "location_perm_denied": "Ứng dụng không cho phép truy cập vị trí.",
991
991
  "camera_request_permission": "Quyền yêu cầu máy ảnh",
992
992
  "camera_request_permission_des": "Để sử dụng máy ảnh, vui lòng mở khóa quyền đối với máy ảnh",
993
- "location_require_message": "Eoh cần quyền truy cập vị trí của bạn để lấy vị trí hiện tại."
993
+ "location_require_message": "Eoh cần quyền truy cập vị trí của bạn để lấy vị trí hiện tại.",
994
+ "turn_on_location_service": "Bật dịch vụ vị trí cho điện thoại của bạn",
995
+ "turn_on_location_service_des": "Để tiếp tục, bật dịch vụ vị trí cho điện thoại của bạn trong Cài Đặt"
994
996
  }
@@ -0,0 +1,30 @@
1
+ import { Platform } from 'react-native';
2
+ import RNAndroidLocationEnabler from 'react-native-android-location-enabler';
3
+ import AlertAsync from 'react-native-alert-async';
4
+ import t from '../../hooks/Common/useTranslations';
5
+
6
+ export const openPromptEnableLocation = async () => {
7
+ if (Platform.OS === 'android') {
8
+ try {
9
+ await RNAndroidLocationEnabler.promptForEnableLocationIfNeeded({
10
+ interval: 10000,
11
+ fastInterval: 5000,
12
+ });
13
+ return true;
14
+ } catch (err) {
15
+ return false;
16
+ }
17
+ } else {
18
+ await AlertAsync(
19
+ t('turn_on_location_service'),
20
+ t('turn_on_location_service_des'),
21
+ [
22
+ {
23
+ text: t('ok'),
24
+ onPress: () => {},
25
+ },
26
+ ]
27
+ );
28
+ return false;
29
+ }
30
+ };
@@ -0,0 +1,12 @@
1
+ import { validateEmail, formatMoney } from '../Utils';
2
+
3
+ describe('Test utils', () => {
4
+ test('test validateEmail', () => {
5
+ const result = validateEmail('eoh.2020@gmail.com');
6
+ expect(result).toEqual(true);
7
+ });
8
+ test('test formatMoney', () => {
9
+ const result = formatMoney(10000);
10
+ expect(result).toEqual('10.000 đ');
11
+ });
12
+ });