@eohjsc/react-native-smart-city 0.2.57 → 0.2.58

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.2.57",
4
+ "version": "0.2.58",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -43,11 +43,7 @@
43
43
  "pods": "cd example && pod-install --quiet",
44
44
  "bootstrap": "yarn example && yarn && yarn pods",
45
45
  "build": "sync-files ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src",
46
- "watch": "sync-files --watch ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src",
47
- "test:code-formatter": "prettier src/**/**/*.js --write",
48
- "test:code-formatter-step-1": "prettier --check src/**/*.js",
49
- "test:code-formatter-step-2": "prettier --check src/**/**/*.js",
50
- "test:code-formatter-step-3": "prettier --check src/**/**/**/*.js"
46
+ "watch": "sync-files --watch ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src"
51
47
  },
52
48
  "repository": {
53
49
  "type": "git",
@@ -85,7 +81,6 @@
85
81
  "eslint-plugin-react": "^7.21.5",
86
82
  "eslint-plugin-react-native": "^3.10.0",
87
83
  "factory-girl": "^5.0.4",
88
- "husky": "^2.7.0",
89
84
  "jest": "^26.6.3",
90
85
  "jest-circus": "^26.6.3",
91
86
  "jetifier": "^1.6.6",
@@ -146,6 +141,7 @@
146
141
  "react-hooks-global-state": "^1.0.1",
147
142
  "react-i18next": "^11.8.12",
148
143
  "react-native-alert-async": "^1.0.5",
144
+ "react-native-android-wifi": "^0.0.41",
149
145
  "react-native-appearance": "^0.3.4",
150
146
  "react-native-base64": "^0.1.0",
151
147
  "react-native-ble-plx": "^2.0.1",
@@ -206,21 +202,6 @@
206
202
  "validator": "^13.1.1",
207
203
  "victory-native": "^35.0.1"
208
204
  },
209
- "husky": {
210
- "hooks": {
211
- "pre-commit": "lint-staged",
212
- "pre-push": "yarn jest"
213
- }
214
- },
215
- "lint-staged": {
216
- "*.{js, ts}": [
217
- "yarn lint",
218
- "yarn test:code-formatter",
219
- "yarn test:code-formatter-step-1",
220
- "yarn test:code-formatter-step-2",
221
- "yarn test:code-formatter-step-3"
222
- ]
223
- },
224
205
  "bugs": {
225
206
  "url": "https://github.com/github_account/react-native-smart-city/issues"
226
207
  },
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { ScrollView } from 'react-native';
3
+ import { act, create } from 'react-test-renderer';
4
+ import AccountList from '../AccountList';
5
+
6
+ describe('Test AccountList', () => {
7
+ let tree;
8
+ const accounts = [
9
+ {
10
+ id: 1,
11
+ name: 'test',
12
+ phone_number: 123456677,
13
+ },
14
+ ];
15
+
16
+ it('test render has data', async () => {
17
+ await act(() => {
18
+ tree = create(<AccountList accounts={accounts} />);
19
+ });
20
+ const instance = tree.root;
21
+ const scrollViews = instance.findAllByType(ScrollView);
22
+ expect(scrollViews).toHaveLength(1);
23
+ });
24
+
25
+ it('test render has no data', async () => {
26
+ await act(() => {
27
+ tree = create(<AccountList accounts={[]} />);
28
+ });
29
+ const instance = tree.root;
30
+ const scrollViews = instance.findAllByType(ScrollView);
31
+ expect(scrollViews).toHaveLength(1);
32
+ });
33
+ });
@@ -59,6 +59,8 @@ const CameraDevice = ({ station, handleFullScreen, goToPlayBack }) => {
59
59
  cameraName={device.configuration.name}
60
60
  handleFullScreen={handleFullScreen}
61
61
  goToPlayBack={goToPlayBack(device, { uri: station?.background })}
62
+ width={standardizeWidth}
63
+ height={standardizeHeight}
62
64
  />
63
65
  </View>
64
66
  ))}
@@ -7,7 +7,7 @@ const Card = memo(({ title, children, style }) => {
7
7
  return (
8
8
  <View style={[styles.card, style]}>
9
9
  {title && <Text style={styles.headerTitle}>{title}</Text>}
10
- <View>{children}</View>
10
+ {children}
11
11
  </View>
12
12
  );
13
13
  });
@@ -3,11 +3,9 @@ import { StyleSheet } from 'react-native';
3
3
 
4
4
  export default StyleSheet.create({
5
5
  card: {
6
- marginTop: 14,
7
- marginBottom: 14,
6
+ marginVertical: 14,
8
7
  marginHorizontal: 16,
9
8
  padding: 16,
10
- flexDirection: 'column',
11
9
  backgroundColor: Colors.White,
12
10
  borderRadius: 10,
13
11
  shadowColor: Colors.Shadow,
@@ -56,40 +56,6 @@ const chartOptions = {
56
56
  },
57
57
  minRange: 3600 * 24 * 1000,
58
58
  },
59
- plotOptions: {
60
- series: {
61
- point: {
62
- events: {
63
- click: () => {
64
- let series = this.series.chart.series;
65
- const seriesIndex = this.series.index;
66
- for (let i = 0; i < series.length; i++) {
67
- if (series[i].index !== seriesIndex) {
68
- series[i].visible ? series[i].hide() : series[i].show();
69
- }
70
- }
71
- return false;
72
- },
73
- },
74
- },
75
- events: {
76
- legendItemClick: (event) => {
77
- if (!this.visible) {
78
- return true;
79
- }
80
- let seriesIndex = this.index;
81
- let series = this.chart.series;
82
- for (let i = 0; i < series.length; i++) {
83
- if (series[i].index !== seriesIndex) {
84
- series[i].visible ? series[i].hide() : series[i].show();
85
- }
86
- }
87
- return false;
88
- },
89
- },
90
- showInNavigator: true,
91
- },
92
- },
93
59
  };
94
60
 
95
61
  function LinearChart({ datas }) {
@@ -1,7 +1,6 @@
1
1
  import React, { memo, useCallback, useState, useEffect } from 'react';
2
2
  import { Image, View, StyleSheet, Text, TouchableOpacity } from 'react-native';
3
3
  import { VLCPlayer } from 'react-native-vlc-media-player';
4
- import { getStatusBarHeight } from 'react-native-iphone-x-helper';
5
4
  import { useTranslations } from '../../hooks/Common/useTranslations';
6
5
 
7
6
  import PauseIcon from '../../../assets/images/Common/Pause.svg';
@@ -11,173 +10,174 @@ import styles from './Styles/MediaPlayerDetailStyles';
11
10
  import FImage from '../../commons/FImage';
12
11
  import { TESTID } from '../../configs/Constants';
13
12
 
14
- const MediaPlayerDetail = memo(
15
- ({
16
- uri,
17
- cameraName,
18
- thumbnail,
19
- style,
20
- wrapStyles,
21
- resizeMode = 'none',
22
- amount,
23
- handleFullScreen,
24
- isFullScreen = false,
25
- isPaused = true,
26
- goToPlayBack,
27
- isShowFullScreenIcon = false,
28
- }) => {
29
- const t = useTranslations();
30
- const [paused, setPaused] = useState(isPaused);
31
- const onTapPause = useCallback(() => {
32
- setPaused(false);
33
- }, []);
13
+ const MediaPlayerDetail = ({
14
+ uri,
15
+ cameraName,
16
+ thumbnail,
17
+ style,
18
+ wrapStyles,
19
+ resizeMode = 'none',
20
+ amount,
21
+ handleFullScreen,
22
+ isPaused = true,
23
+ goToPlayBack,
24
+ isShowFullScreenIcon = false,
25
+ width,
26
+ height,
27
+ }) => {
28
+ const t = useTranslations();
29
+ const [paused, setPaused] = useState(isPaused);
30
+ const onTapPause = useCallback(() => {
31
+ setPaused(false);
32
+ }, []);
34
33
 
35
- const onTapGoDetail = useCallback(() => {
36
- if (!paused) {
37
- setPaused(true);
38
- } else {
39
- goToPlayBack && goToPlayBack();
40
- }
41
- // eslint-disable-next-line react-hooks/exhaustive-deps
42
- }, [paused]);
34
+ const onTapGoDetail = useCallback(() => {
35
+ if (!paused) {
36
+ setPaused(true);
37
+ } else {
38
+ goToPlayBack && goToPlayBack();
39
+ }
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, [paused]);
43
42
 
44
- const onFullScreen = useCallback(() => {
45
- handleFullScreen && handleFullScreen({ uri, cameraName, thumbnail });
46
- // eslint-disable-next-line react-hooks/exhaustive-deps
47
- }, []);
43
+ const onFullScreen = useCallback(() => {
44
+ handleFullScreen && handleFullScreen({ uri, cameraName, thumbnail });
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, []);
48
47
 
49
- useEffect(() => {
50
- setPaused(isPaused);
51
- }, [isPaused]);
48
+ const getWidthHeight = useCallback(() => {
49
+ let width = 0,
50
+ height = 0;
51
+ switch (amount) {
52
+ case 1:
53
+ width = Constants.width;
54
+ height = 224;
55
+ break;
56
+ case 4:
57
+ width = Constants.width / 2;
58
+ height = 112;
59
+ break;
60
+ case 6:
61
+ width = Constants.width / 3;
62
+ height = 112;
63
+ break;
64
+ default:
65
+ width = Constants.width - 28;
66
+ height = 162;
67
+ break;
68
+ }
69
+ return {
70
+ width,
71
+ height,
72
+ };
73
+ }, [amount]);
52
74
 
53
- const source = !thumbnail || !thumbnail.uri ? Images.BgDevice : thumbnail;
54
- return (
55
- <View style={[styles.wrap, wrapStyles]}>
56
- <View
57
- style={[
58
- styles.loadingWrap,
59
- isFullScreen && { transform: [{ rotate: '90deg' }] },
60
- ]}
61
- >
62
- <Text style={styles.loadingText}>{t('loading')}</Text>
63
- </View>
64
- <TouchableOpacity
65
- activeOpacity={1}
66
- style={styles.videoBtn}
67
- onPress={onTapGoDetail}
68
- >
69
- {paused ? (
70
- <View style={[styles.player, style]}>
71
- <FImage
72
- source={source}
73
- style={[
74
- styles.player,
75
- style,
76
- isFullScreen && {
77
- marginLeft: -Constants.width / 2 - getStatusBarHeight(),
78
- width: Constants.height,
79
- height: Constants.width,
80
- transform: [{ rotate: '90deg' }],
81
- },
82
- ]}
83
- defaultSource={Images.BgDevice}
84
- resizeMode="cover"
85
- />
86
- <View
87
- style={[
88
- StyleSheet.absoluteFillObject,
89
- { backgroundColor: colorOpacity(Colors.Black, 0.3) },
90
- ]}
91
- />
92
- </View>
93
- ) : isFullScreen ? (
94
- <View style={{ transform: [{ rotate: '90deg' }] }}>
95
- <VLCPlayer
96
- autoAspectRatio={true}
97
- videoAspectRatio={`${Constants.height}:${Constants.width}`}
98
- source={{ uri }}
99
- style={[
100
- {
101
- width: Constants.height,
102
- height: Constants.width,
103
- },
104
- ]}
105
- />
106
- </View>
107
- ) : (
108
- <VLCPlayer
109
- autoAspectRatio={true}
110
- videoAspectRatio={
111
- !amount
112
- ? '21:9'
113
- : amount === 1
114
- ? '15:9'
115
- : amount === 4
116
- ? '5:3'
117
- : '10:9'
118
- }
119
- source={{
120
- initType: 2,
121
- hwDecoderEnabled: 1,
122
- hwDecoderForced: 1,
123
- uri,
124
- initOptions: [
125
- '--no-audio',
126
- '--rtsp-tcp',
127
- '--network-caching=150',
128
- '--rtsp-caching=150',
129
- '--no-stats',
130
- '--tcp-caching=150',
131
- '--realrtsp-caching=150',
132
- ],
133
- }}
75
+ useEffect(() => {
76
+ setPaused(isPaused);
77
+ }, [isPaused]);
78
+
79
+ const source = !thumbnail || !thumbnail.uri ? Images.BgDevice : thumbnail;
80
+ return (
81
+ <View style={[styles.wrap, wrapStyles]}>
82
+ <View style={[styles.loadingWrap]}>
83
+ <Text style={styles.loadingText}>{t('loading')}</Text>
84
+ </View>
85
+ <TouchableOpacity
86
+ activeOpacity={1}
87
+ style={styles.videoBtn}
88
+ onPress={onTapGoDetail}
89
+ >
90
+ {paused ? (
91
+ <View style={[styles.player, style]}>
92
+ <FImage
93
+ source={source}
134
94
  style={[styles.player, style]}
135
- resizeMode={resizeMode}
136
- isLive={true}
95
+ defaultSource={Images.BgDevice}
96
+ resizeMode="cover"
97
+ />
98
+ <View
99
+ style={[
100
+ StyleSheet.absoluteFillObject,
101
+ { backgroundColor: colorOpacity(Colors.Black, 0.3) },
102
+ ]}
137
103
  />
138
- )}
139
-
140
- <View style={styles.buttonView}>
141
- <View style={styles.buttonPause}>
142
- <TouchableOpacity
143
- onPress={onTapPause}
144
- style={[
145
- styles.btn,
146
- isFullScreen && { transform: [{ rotate: '90deg' }] },
147
- ]}
148
- activeOpacity={0.8}
149
- >
150
- {paused && <PauseIcon />}
151
- </TouchableOpacity>
152
- </View>
153
104
  </View>
154
- </TouchableOpacity>
155
-
156
- {cameraName && paused && (
157
- <Text
158
- style={[
159
- styles.cameraName,
160
- amount && amount !== 1 && styles.cameraName2,
161
- ]}
162
- >
163
- {cameraName}
164
- </Text>
165
- )}
166
- {isShowFullScreenIcon && (
167
- <TouchableOpacity
168
- onPress={onFullScreen}
105
+ ) : (
106
+ <VLCPlayer
107
+ autoAspectRatio={true}
108
+ videoAspectRatio={
109
+ width && height
110
+ ? `${width}:${height}`
111
+ : `${getWidthHeight().width}:${getWidthHeight().height}`
112
+ }
113
+ source={{
114
+ initType: 2,
115
+ hwDecoderEnabled: 1,
116
+ hwDecoderForced: 1,
117
+ uri,
118
+ initOptions: [
119
+ '--no-audio',
120
+ '--rtsp-tcp',
121
+ '--network-caching=150',
122
+ '--rtsp-caching=150',
123
+ '--no-stats',
124
+ '--tcp-caching=150',
125
+ '--realrtsp-caching=150',
126
+ // '--avcodec-threads=20',
127
+ // '--live-cacheing=150',
128
+ ],
129
+ }}
169
130
  style={[
170
- styles.iconFullScreen,
171
- (amount === 4 || amount === 6) && styles.iconFullScreen2,
131
+ styles.player,
132
+ style,
133
+ {
134
+ width: width || getWidthHeight().width,
135
+ height: height || getWidthHeight().height,
136
+ },
172
137
  ]}
173
- testID={TESTID.SUB_UNIT_FULL_CAMERA}
174
- >
175
- <Image source={Images.fullscreen} />
176
- </TouchableOpacity>
138
+ // resizeMode={resizeMode}
139
+ isLive={true}
140
+ // playInBackground={true}
141
+ />
177
142
  )}
178
- </View>
179
- );
180
- }
181
- );
182
143
 
183
- export default MediaPlayerDetail;
144
+ <View style={styles.buttonView}>
145
+ <View style={styles.buttonPause}>
146
+ <TouchableOpacity
147
+ onPress={onTapPause}
148
+ style={[styles.btn]}
149
+ activeOpacity={0.8}
150
+ >
151
+ {paused && <PauseIcon />}
152
+ </TouchableOpacity>
153
+ </View>
154
+ </View>
155
+ </TouchableOpacity>
156
+
157
+ {cameraName && paused && (
158
+ <Text
159
+ style={[
160
+ styles.cameraName,
161
+ amount && amount !== 1 && styles.cameraName2,
162
+ ]}
163
+ >
164
+ {cameraName}
165
+ </Text>
166
+ )}
167
+ {isShowFullScreenIcon && (
168
+ <TouchableOpacity
169
+ onPress={onFullScreen}
170
+ style={[
171
+ styles.iconFullScreen,
172
+ (amount === 4 || amount === 6) && styles.iconFullScreen2,
173
+ ]}
174
+ testID={TESTID.SUB_UNIT_FULL_CAMERA}
175
+ >
176
+ <Image source={Images.fullscreen} />
177
+ </TouchableOpacity>
178
+ )}
179
+ </View>
180
+ );
181
+ };
182
+
183
+ export default memo(MediaPlayerDetail);
@@ -5,6 +5,7 @@ import { useTranslations } from '../../hooks/Common/useTranslations';
5
5
 
6
6
  import { Colors } from '../../configs';
7
7
  import Text from '../../commons/Text';
8
+ import { shortEmailName } from '../../utils/Utils';
8
9
 
9
10
  import BtnRemoveMember from './BtnRemoveMember';
10
11
 
@@ -37,7 +38,6 @@ const RowMember = memo(
37
38
  if (member?.id === ownerId && member?.share_id) {
38
39
  return null;
39
40
  }
40
-
41
41
  return (
42
42
  <View style={styles.rowContainer}>
43
43
  <View
@@ -49,7 +49,9 @@ const RowMember = memo(
49
49
  <IconOutline name={'user'} size={20} color={Colors.White} />
50
50
  </View>
51
51
  <View style={[styles.infoContainer, { paddingBottom: paddingBottom }]}>
52
- <Text style={styles.textName}>{member.name ?? 'N/A'}</Text>
52
+ <Text style={styles.textName}>
53
+ {member?.name || shortEmailName(member.email) || ''}
54
+ </Text>
53
55
  {!!role && (
54
56
  <Text style={[styles.textRole, { color: roleColor }]}>{role}</Text>
55
57
  )}
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import renderer, { act } from 'react-test-renderer';
3
+ import RowMember from '../RowMember';
4
+ import Text from '../../Text';
5
+ import { SCProvider } from '../../../context';
6
+ import { mockSCStore } from '../../../context/mockStore';
7
+
8
+ const wrapComponent = (member, ownerId, currentUserId, mockFunc) => (
9
+ <SCProvider initState={mockSCStore({})}>
10
+ <RowMember
11
+ member={member}
12
+ ownerId={ownerId}
13
+ currentUserId={currentUserId}
14
+ onPressRemove={mockFunc}
15
+ />
16
+ </SCProvider>
17
+ );
18
+
19
+ describe('RowMember', () => {
20
+ let tree;
21
+ const mockFunc = jest.fn();
22
+ test('RowMember owner have name', () => {
23
+ const dataMember = { id: 1, name: 'CEO' };
24
+ act(() => {
25
+ tree = renderer.create(wrapComponent(dataMember, 1, 1, mockFunc));
26
+ });
27
+ const instance = tree.root;
28
+ const textInputs = instance.findAllByType(Text);
29
+ expect(textInputs.length).toBe(2);
30
+ expect(textInputs[0].props.children).toEqual('CEO');
31
+ });
32
+ test('RowMember owner dont have name show start of email ', () => {
33
+ const dataMember = { id: 1, name: '', email: 'abc@gmail.com' };
34
+ act(() => {
35
+ tree = renderer.create(wrapComponent(dataMember, 1, 1, mockFunc));
36
+ });
37
+ const instance = tree.root;
38
+ const textInputs = instance.findAllByType(Text);
39
+ expect(textInputs.length).toBe(2);
40
+ expect(textInputs[0].props.children).toEqual('abc');
41
+ });
42
+ });
@@ -9,10 +9,10 @@ import { Section } from '../Section';
9
9
  import Text from '../Text';
10
10
  import ItemDevice from '../Device/ItemDevice';
11
11
  import ItemAddNew from '../Device/ItemAddNew';
12
- import MediaPlayer from '../MediaPlayer';
13
12
  import { standardizeCameraScreenSize } from '../../utils/Utils';
14
13
  import Routes from '../../utils/Route';
15
14
  import FastImage from 'react-native-fast-image';
15
+ import MediaPlayerDetail from '../MediaPlayerDetail';
16
16
 
17
17
  const { standardizeWidth, standardizeHeight } = standardizeCameraScreenSize(
18
18
  Device.screenWidth - 32
@@ -21,23 +21,29 @@ const { standardizeWidth, standardizeHeight } = standardizeCameraScreenSize(
21
21
  const ShortDetailSubUnit = ({ unit, station, isGGHomeConnected }) => {
22
22
  const t = useTranslations();
23
23
  const { navigate } = useNavigation();
24
+
24
25
  const renderCamera = () => {
25
26
  if (station?.camera) {
26
27
  return (
27
28
  <View
28
29
  style={[
29
30
  styles.boxImage,
30
- { width: standardizeWidth, height: standardizeHeight },
31
+ {
32
+ width: standardizeWidth,
33
+ height: standardizeHeight,
34
+ },
31
35
  ]}
32
36
  testID={TESTID.SUB_UNIT_CAMERA_VIEW}
33
37
  >
34
- <MediaPlayer
38
+ <MediaPlayerDetail
35
39
  uri={station.camera.uri}
36
- previewUri={station.camera.preview_uri}
37
40
  thumbnail={{
38
41
  uri: station.background,
39
42
  }}
40
- key={`camera-${station.camera.id}`}
43
+ key={`camera-device-${station?.camera?.id}`}
44
+ cameraName={station?.camera?.name}
45
+ width={standardizeWidth}
46
+ height={standardizeHeight}
41
47
  />
42
48
  </View>
43
49
  );
@@ -163,7 +163,7 @@ const SegmentedRoundDisplay = ({
163
163
  <Text
164
164
  x={svgWidth / 2 + 50}
165
165
  fontWeight="normal"
166
- fontSize={valueText.toString().length >= 6 ? 46 : 56}
166
+ fontSize={valueText.toString().length >= 6 ? 35 : 56}
167
167
  y={svgHeight / 2 + 30}
168
168
  fill={filledArcColor}
169
169
  textAnchor="middle"
@@ -172,6 +172,11 @@ const API = {
172
172
  SCConfig.apiRoot +
173
173
  `/connection_manager/lg_thinq/device_status/${sensorId}/`,
174
174
  },
175
+ VCONNEX: {
176
+ AUTHORIZE: (client_id, redirect_uri, user_id, station_id) =>
177
+ // eslint-disable-next-line max-len
178
+ `https://partner-api-stg.vconnex.vn/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code&scope=SYNCH&scope=CONTROL&scope=QUERY&state=${user_id}@${station_id}`,
179
+ },
175
180
  },
176
181
  NOTIFICATION: {
177
182
  LIST_ALL_NOTIFICATIONS: (page, type) =>
@@ -1,6 +1,6 @@
1
1
  import { Platform, Dimensions, StatusBar } from 'react-native';
2
2
  import { RFValue } from 'react-native-responsive-fontsize';
3
- import OneTap from '../../assets/images/OneTap@1x.svg';
3
+ import OneTap from '../../assets/images/OneTap.svg';
4
4
  import ValueChange from '../../assets/images/ValueChange.svg';
5
5
  import Schedule from '../../assets/images/Schedule.svg';
6
6
 
@@ -90,6 +90,8 @@ const SCDefaultConfig = {
90
90
  LG_CLIENT_ID: '2b85aee334f046848341547894bb7c4e',
91
91
  LG_REDIRECT_URI_APP: 'app://eoh/sync-lg-device',
92
92
  LG_URL: 'https://qt-vn.m.lgaccount.com/emp/v2',
93
+ VCONNEX_CLIENT_ID: '',
94
+ VCONNEX_REDIRECT_URI_APP: '',
93
95
  pusherAppKey: '9a591ae4a764acc08714',
94
96
  pusherAppCluster: 'ap1',
95
97
  };
@@ -100,6 +102,8 @@ export class SCConfig {
100
102
  static LG_CLIENT_ID = SCDefaultConfig.LG_CLIENT_ID;
101
103
  static LG_REDIRECT_URI_APP = SCDefaultConfig.LG_REDIRECT_URI_APP;
102
104
  static LG_URL = SCDefaultConfig.LG_URL;
105
+ static VCONNEX_CLIENT_ID = SCDefaultConfig.VCONNEX_CLIENT_ID;
106
+ static VCONNEX_REDIRECT_URI_APP = SCDefaultConfig.VCONNEX_REDIRECT_URI_APP;
103
107
  static pusherAppKey = SCDefaultConfig.pusherAppKey;
104
108
  static pusherAppCluste = SCDefaultConfig.pusherAppCluster;
105
109
  }
@@ -113,6 +117,10 @@ export const initSCConfig = (config) => {
113
117
  SCConfig.LG_REDIRECT_URI_APP =
114
118
  config.LG_REDIRECT_URI_APP ?? SCDefaultConfig.LG_REDIRECT_URI_APP;
115
119
  SCConfig.LG_URL = config.LG_URL ?? SCDefaultConfig.LG_URL;
120
+ SCConfig.VCONNEX_CLIENT_ID =
121
+ config.VCONNEX_CLIENT_ID ?? SCDefaultConfig.VCONNEX_CLIENT_ID;
122
+ SCConfig.VCONNEX_REDIRECT_URI_APP =
123
+ config.VCONNEX_REDIRECT_URI_APP ?? SCDefaultConfig.VCONNEX_REDIRECT_URI_APP;
116
124
  SCConfig.pusherAppKey = config.pusherAppKey ?? SCDefaultConfig.pusherAppKey;
117
125
  SCConfig.pusherAppCluster =
118
126
  config.pusherAppCluster ?? SCDefaultConfig.pusherAppCluster;
@@ -5,6 +5,7 @@ import t from '../../hooks/Common/useTranslations';
5
5
  import base64 from 'react-native-base64';
6
6
  import { BleManager } from 'react-native-ble-plx';
7
7
  import { ToastBottomHelper } from '../../utils/Utils';
8
+ import { useEffect, useState } from 'react';
8
9
 
9
10
  const bluetoothDevices = {};
10
11
  const needToScanDevices = [];
@@ -165,6 +166,19 @@ export const isBluetoothEnabled = async () => {
165
166
  return state === 'PoweredOn';
166
167
  };
167
168
 
169
+ export const useIsBluetoothEnabled = () => {
170
+ const [isEnabled, setIsEnabled] = useState(null);
171
+
172
+ useEffect(() => {
173
+ const subscription = bleManager.onStateChange((state) => {
174
+ setIsEnabled(state === 'PoweredOn');
175
+ }, true);
176
+
177
+ return () => subscription.remove();
178
+ }, []);
179
+ return isEnabled;
180
+ };
181
+
168
182
  export const enableBluetoothForAndroid = async () => {
169
183
  await bleManager.enable();
170
184
  };
@@ -16,7 +16,6 @@ export const sendRemoteCommand = async (sensor, action, data) => {
16
16
  );
17
17
  return;
18
18
  }
19
-
20
19
  if (action.command_prefer_over_bluetooth) {
21
20
  try {
22
21
  await sendCommandOverBluetooth(sensor, action, data);
@@ -11,6 +11,7 @@ import Routes from '../../utils/Route';
11
11
  import { TESTID } from '../../configs/Constants';
12
12
  import styles from './SelectSubUnitStyles';
13
13
  import Button from '../../commons/Button';
14
+ import { SCConfig } from '../../configs/SCConfig';
14
15
 
15
16
  const AddCommonSelectSubUnit = ({ route }) => {
16
17
  const t = useTranslations();
@@ -31,6 +32,9 @@ const AddCommonSelectSubUnit = ({ route }) => {
31
32
  case 'AddHassiDevice':
32
33
  setTitle(t('select_a_sub_unit'));
33
34
  break;
35
+ case 'AddVconnexDevice':
36
+ setTitle(t('select_a_sub_unit'));
37
+ break;
34
38
  default:
35
39
  setTitle(t('add_new_gateway'));
36
40
  setSubTitle(t('select_a_sub_unit'));
@@ -70,10 +74,28 @@ const AddCommonSelectSubUnit = ({ route }) => {
70
74
  station: subUnits[selectedIndex]?.id,
71
75
  });
72
76
  break;
77
+ case 'AddVconnexDevice':
78
+ navigation.navigate(Routes.Browser, {
79
+ link: API.IOT.VCONNEX.AUTHORIZE(
80
+ SCConfig.VCONNEX_CLIENT_ID,
81
+ SCConfig.VCONNEX_REDIRECT_URI_APP,
82
+ unit.user_id,
83
+ subUnits[selectedIndex]?.id
84
+ ),
85
+ });
86
+ break;
73
87
  default:
74
88
  break;
75
89
  }
76
- }, [addType, navigation, subUnits, selectedIndex, unit.name, route.params]);
90
+ }, [
91
+ addType,
92
+ navigation,
93
+ subUnits,
94
+ selectedIndex,
95
+ unit?.name,
96
+ unit.user_id,
97
+ route.params,
98
+ ]);
77
99
 
78
100
  const handleSelectIndex = (index) => {
79
101
  if (index !== selectedIndex) {
@@ -48,6 +48,9 @@ const AddCommonSelectUnit = ({ route }) => {
48
48
  case 'AddHassioDevice':
49
49
  setTitle(t('text_select_a_unit'));
50
50
  break;
51
+ case 'AddVconnexDevice':
52
+ setTitle(t('text_select_a_unit'));
53
+ break;
51
54
  default:
52
55
  setTitle(t('add_new_sub_unit'));
53
56
  setSubTitle(t('add_new_subunit_select_unit'));
@@ -102,6 +105,15 @@ const AddCommonSelectUnit = ({ route }) => {
102
105
  unit_id: units[selectedIndex].id,
103
106
  });
104
107
  break;
108
+ case 'AddVconnexDevice':
109
+ navigation.navigate(Routes.AddDeviceStack, {
110
+ screen: Routes.AddCommonSelectSubUnit,
111
+ params: {
112
+ unit_id: units[selectedIndex].id,
113
+ addType: 'AddVconnexDevice',
114
+ },
115
+ });
116
+ break;
105
117
  default:
106
118
  break;
107
119
  }
@@ -239,11 +239,6 @@ describe('test DeviceDetail', () => {
239
239
  );
240
240
  expect(sensorDisplayItem.length).toEqual(2);
241
241
 
242
- const itemMediaPlayer = instance.find(
243
- (el) => el.props.testID === TESTID.DEVICE_DETAIL_MEDIA_PLAYER
244
- );
245
- expect(itemMediaPlayer).toBeDefined();
246
-
247
242
  const itemActionGroup = instance.find(
248
243
  (el) => el.props.testID === TESTID.DEVICE_DETAIL_ACTION_GROUP
249
244
  );
@@ -2,11 +2,8 @@ import React, { useCallback } from 'react';
2
2
  import { View } from 'react-native';
3
3
  import ActionGroup from '../../../commons/ActionGroup';
4
4
  import { Card } from '../../../commons/CardShadow';
5
- import MediaPlayer from '../../../commons/MediaPlayer';
6
- import { Device } from '../../../configs';
7
5
  import { TESTID } from '../../../configs/Constants';
8
6
  import { useTranslations } from '../../../hooks/Common/useTranslations';
9
- import { standardizeCameraScreenSize } from '../../../utils/Utils';
10
7
  import { DetailHistoryChart } from './DetailHistoryChart';
11
8
  import { sendRemoteCommand } from '../../../iot/RemoteControl';
12
9
  import CurrentRainSensor from '../../../commons/Device/RainningSensor/CurrentRainSensor';
@@ -20,8 +17,11 @@ import ListQualityIndicator from '../../../commons/Device/WaterQualitySensor/Lis
20
17
  import EmergencyDetail from '../../../commons/Device/Emergency/EmergencyDetail';
21
18
  import EmergencyButton from '../../../commons/Device/Emergency/EmergencyButton';
22
19
  import FooterInfo from '../../../commons/Device/FooterInfo';
20
+ import MediaPlayerDetail from '../../../commons/MediaPlayerDetail';
21
+ import { standardizeCameraScreenSize } from '../../../utils/Utils';
22
+ import { Device } from '../../../configs';
23
23
 
24
- const { standardizeHeight, standardizeWidth } = standardizeCameraScreenSize(
24
+ const { standardizeWidth, standardizeHeight } = standardizeCameraScreenSize(
25
25
  Device.screenWidth - 32
26
26
  );
27
27
 
@@ -51,15 +51,15 @@ export const SensorDisplayItem = ({
51
51
  return (
52
52
  <Card title={t('camera')}>
53
53
  <View style={styles.mediaContainer}>
54
- <MediaPlayer
55
- testID={TESTID.DEVICE_DETAIL_MEDIA_PLAYER}
54
+ <MediaPlayerDetail
56
55
  uri={item.configuration.uri}
57
- style={{ height: standardizeHeight }}
58
- ratioWidth={standardizeWidth - 32}
59
56
  thumbnail={{
60
- uri: item.configuration.preview_uri,
57
+ uri: background,
61
58
  }}
62
- background={{ uri: background }}
59
+ key={`camera-device-${item.configuration.id}`}
60
+ cameraName={item.configuration.name}
61
+ width={standardizeWidth - 32}
62
+ height={standardizeHeight - 16}
63
63
  />
64
64
  </View>
65
65
  </Card>
@@ -79,7 +79,21 @@ const DeviceDetail = ({ route }) => {
79
79
  [sensor]
80
80
  );
81
81
 
82
- useDisconnectedDevice(isConnected, sensorName);
82
+ const isDeviceHasBle = useMemo(() => {
83
+ const action = display.items.filter((item) => item.type === 'action');
84
+ if (action.length === 0) {
85
+ return false;
86
+ }
87
+
88
+ return action.some((item) => {
89
+ const { configuration } = item?.configuration;
90
+ return JSON.stringify(configuration).includes(
91
+ '"command_prefer_over_bluetooth":true'
92
+ );
93
+ });
94
+ }, [display]);
95
+
96
+ useDisconnectedDevice(sensorName, isDeviceHasBle);
83
97
 
84
98
  const netInfo = useNetInfo();
85
99
 
@@ -4,10 +4,11 @@ import { Alert, Linking, Platform } from 'react-native';
4
4
  import { useTranslations } from '../../../hooks/Common/useTranslations';
5
5
  import {
6
6
  enableBluetoothForAndroid,
7
- isBluetoothEnabled,
7
+ useIsBluetoothEnabled,
8
8
  } from '../../../iot/RemoteControl/Bluetooth';
9
+ import { ToastBottomHelper } from '../../../utils/Utils';
9
10
 
10
- export const useDisconnectedDevice = (isConnected, sensorName) => {
11
+ export const useDisconnectedDevice = (sensorName, isDeviceHasBle) => {
11
12
  const t = useTranslations();
12
13
  const openBluetoothIOS = () => {
13
14
  Linking.openURL('App-Prefs:Bluetooth');
@@ -32,32 +33,36 @@ export const useDisconnectedDevice = (isConnected, sensorName) => {
32
33
  onPress: () => enableBluetoothForAndroid(),
33
34
  },
34
35
  ];
35
- const checkNetWorkConnect = useCallback(async () => {
36
- const netState = await NetInfo.fetch();
37
- if (!isConnected || !netState.isConnected) {
38
- const isBtEnabled = await isBluetoothEnabled();
39
- if (isBtEnabled) {
40
- Alert.alert(
41
- '',
42
- t(
43
- 'your_internet_is_disconnected_change_to_control_via_bluetooth_connection',
44
- { name: sensorName }
45
- )
46
- );
47
- } else {
48
- Alert.alert(
49
- '',
50
- t(
51
- 'your_connection_to_the_server_was_disconnected_please_open_the_bluetooth_to_continue'
52
- ),
53
- actions
54
- );
36
+
37
+ const netState = NetInfo.useNetInfo();
38
+ const isBluetoothEnabled = useIsBluetoothEnabled();
39
+
40
+ const checkNetWorkConnect = useCallback(
41
+ async (isHavingInternet, isBtEnabled) => {
42
+ if (!isHavingInternet && isDeviceHasBle) {
43
+ if (isBtEnabled === true) {
44
+ ToastBottomHelper.info(
45
+ t('your_internet_is_disconnected', { name: sensorName }),
46
+ t('change_to_control_via_bluetooth_connection', {
47
+ name: sensorName,
48
+ })
49
+ );
50
+ } else if (isBtEnabled === false) {
51
+ Alert.alert(
52
+ '',
53
+ t(
54
+ 'your_connection_to_the_server_was_disconnected_please_open_the_bluetooth_to_continue'
55
+ ),
56
+ actions
57
+ );
58
+ }
55
59
  }
56
- }
60
+ },
57
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
- }, [isConnected]);
62
+ [isDeviceHasBle]
63
+ );
59
64
 
60
65
  useEffect(() => {
61
- checkNetWorkConnect();
62
- }, [checkNetWorkConnect]);
66
+ checkNetWorkConnect(netState.isConnected, isBluetoothEnabled);
67
+ }, [netState.isConnected, isBluetoothEnabled, checkNetWorkConnect]);
63
68
  };
@@ -1,12 +1,7 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { IconOutline } from '@ant-design/icons-react-native';
3
3
  import { useNavigation } from '@react-navigation/native';
4
- import {
5
- StyleSheet,
6
- TouchableOpacity,
7
- View,
8
- ActivityIndicator,
9
- } from 'react-native';
4
+ import { StyleSheet, TouchableOpacity, View } from 'react-native';
10
5
  import { useTranslations } from '../../hooks/Common/useTranslations';
11
6
 
12
7
  import { Colors } from '../../configs';
@@ -91,9 +86,7 @@ const MemberList = ({ route }) => {
91
86
  loading={isRefresh}
92
87
  onRefresh={onRefresh}
93
88
  >
94
- {loading ? (
95
- <ActivityIndicator />
96
- ) : (
89
+ {!loading && (
97
90
  <SharingMembers
98
91
  dataMember={dataMembers}
99
92
  ownerId={unit.user_id}
@@ -1,5 +1,5 @@
1
1
  import React, { memo } from 'react';
2
- import { StyleSheet, SafeAreaView, ScrollView } from 'react-native';
2
+ import { StyleSheet, View, ScrollView } from 'react-native';
3
3
  import { useTranslations } from '../../hooks/Common/useTranslations';
4
4
  import { TESTID } from '../../configs/Constants';
5
5
 
@@ -12,8 +12,8 @@ const TDSGuide = memo(() => {
12
12
  useTitleHeader(t('tds_infomation'));
13
13
 
14
14
  return (
15
- <SafeAreaView style={styles.container}>
16
- <ScrollView>
15
+ <View style={styles.container}>
16
+ <ScrollView style={styles.paddingHorizontal16}>
17
17
  <Text
18
18
  type="H3"
19
19
  semibold
@@ -38,7 +38,7 @@ const TDSGuide = memo(() => {
38
38
  </Text>
39
39
  </>
40
40
  </ScrollView>
41
- </SafeAreaView>
41
+ </View>
42
42
  );
43
43
  });
44
44
 
@@ -48,6 +48,8 @@ const styles = StyleSheet.create({
48
48
  container: {
49
49
  flex: 1,
50
50
  backgroundColor: Theme.color.backgroundColor,
51
+ },
52
+ paddingHorizontal16: {
51
53
  paddingHorizontal: 16,
52
54
  },
53
55
  titlePadding: {
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { create } from 'react-test-renderer';
3
+ import { act } from 'react-test-renderer';
4
+ import { Section } from '../../../commons';
5
+ import { SCProvider } from '../../../context';
6
+ import { mockSCStore } from '../../../context/mockStore';
7
+ import ManageUnit from './';
8
+
9
+ const wrapComponent = (route) => (
10
+ <SCProvider initState={mockSCStore({})}>
11
+ <ManageUnit route={route} />
12
+ </SCProvider>
13
+ );
14
+
15
+ describe('Test ManageUnit', () => {
16
+ let tree;
17
+ const route = {
18
+ params: {
19
+ unit: {
20
+ user_id: 1,
21
+ name: 'test',
22
+ },
23
+ },
24
+ };
25
+
26
+ it('Test render', async () => {
27
+ await act(() => {
28
+ tree = create(wrapComponent(route));
29
+ });
30
+ const instance = tree.root;
31
+ const Sections = instance.findAllByType(Section);
32
+ expect(Sections).toHaveLength(1);
33
+ });
34
+ });
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { deleteData, getData, storeData } from '../Storage';
3
3
  import { ToastBottomHelper } from '../Utils';
4
+ import NetInfo from '@react-native-community/netinfo';
4
5
 
5
6
  export const replaceParams = (apiURL, params) => {
6
7
  let _result = apiURL;
@@ -12,7 +13,7 @@ export const replaceParams = (apiURL, params) => {
12
13
  return _result;
13
14
  };
14
15
 
15
- const parseErrorResponse = (error) => {
16
+ const parseErrorResponse = async (error) => {
16
17
  let message;
17
18
  let data = {};
18
19
  if (
@@ -31,7 +32,18 @@ const parseErrorResponse = (error) => {
31
32
  message = error.message;
32
33
  }
33
34
 
34
- typeof message === 'string' && ToastBottomHelper.error(message);
35
+ if (typeof message === 'string') {
36
+ let hideError = false;
37
+ if (message === 'Network Error') {
38
+ const netState = await NetInfo.fetch();
39
+ if (!netState.isConnected) {
40
+ hideError = true;
41
+ }
42
+ }
43
+ if (!hideError) {
44
+ ToastBottomHelper.error(message);
45
+ }
46
+ }
35
47
 
36
48
  return {
37
49
  success: false,
@@ -77,12 +89,12 @@ export async function axiosGet(URL, config = {}, cache = false) {
77
89
  if (cache) {
78
90
  // only network error or server error
79
91
  if (!error.response || error.response.status >= 500) {
80
- return (await axiosCache(URL)) || parseErrorResponse(error);
92
+ return (await axiosCache(URL)) || (await parseErrorResponse(error));
81
93
  } else {
82
94
  await deleteData(cacheKey);
83
95
  }
84
96
  }
85
- return parseErrorResponse(error);
97
+ return await parseErrorResponse(error);
86
98
  }
87
99
  const { data } = response;
88
100
  if (response.status === 200) {
@@ -106,7 +118,7 @@ async function axiosCall(method, ...args) {
106
118
  try {
107
119
  response = await axios[method](...args);
108
120
  } catch (error) {
109
- return parseErrorResponse(error);
121
+ return await parseErrorResponse(error);
110
122
  }
111
123
 
112
124
  const { data } = response;
@@ -826,7 +826,8 @@
826
826
  "quick_button_create_at_dashboard": "Quick button create at dashboard",
827
827
  "every_day_at": "Every day at {time}",
828
828
  "you_do_not_have_the_device_or_have_share_control_device": "You do not have the device or you haven’t been shared the control for the device.",
829
- "your_internet_is_disconnected_change_to_control_via_bluetooth_connection": "Your internet is disconnected, change to control {name} via Bluetooth connection",
829
+ "your_internet_is_disconnected": "Your internet is disconnected",
830
+ "change_to_control_via_bluetooth_connection": "change to control {name} via Bluetooth connection",
830
831
  "your_connection_to_the_server_was_disconnected_please_open_the_bluetooth_to_continue": "Your connection to the server was disconnected, Please open the Bluetooth to continue",
831
832
  "error_please_try_later": "Error! Please try later",
832
833
  "mon": "Mon",
@@ -827,7 +827,8 @@
827
827
  "quick_button_create_at_dashboard": "Tạo nút nhanh trên bảng điều khiển",
828
828
  "every_day_at": "Mỗi ngày vào lúc {time}",
829
829
  "you_do_not_have_the_device_or_have_share_control_device": "Bạn không có thiết bị nào hoặc chưa được cấp quyền để điều khiển thiết bị.",
830
- "your_internet_is_disconnected_change_to_control_via_bluetooth_connection":"Internet của bạn bị ngắt kết nối, thay đổi sang điều khiển {name} qua kết nối Bluetooth",
830
+ "your_internet_is_disconnected": "Internet của bạn bị ngắt kết nối",
831
+ "change_to_control_via_bluetooth_connection": "thay đổi sang điều khiển {name} qua kết nối Bluetooth",
831
832
  "your_connection_to_the_server_was_disconnected_please_open_the_bluetooth_to_continue": "Kết nối của bạn với máy chủ đã bị ngắt, vui lòng mở Bluetooth để tiếp tục",
832
833
  "error_please_try_later": "Lỗi! Vui lòng thử lại sau",
833
834
  "mon": "T2",
@@ -26,6 +26,11 @@ export const isObjectEmpty = (obj) => {
26
26
  return Object.keys(obj).length === 0;
27
27
  };
28
28
 
29
+ export const shortEmailName = (email) => {
30
+ const regex = '([^@]+)';
31
+ return email?.match(regex)[0];
32
+ };
33
+
29
34
  export const formatNumberCompact = (number) => {
30
35
  return new Intl.NumberFormat('en-US', {
31
36
  notation: 'compact',
@@ -66,7 +71,7 @@ export const openMapDirection = (item) => () => {
66
71
  };
67
72
 
68
73
  export const ToastBottomHelper = {
69
- success: (msg) => {
74
+ success: (msg, line2) => {
70
75
  if (!Toast._ref) {
71
76
  return;
72
77
  }
@@ -75,10 +80,11 @@ export const ToastBottomHelper = {
75
80
  type: 'success',
76
81
  position: 'bottom',
77
82
  text1: msg,
83
+ text2: line2,
78
84
  visibilityTime: 1000,
79
85
  });
80
86
  },
81
- error: (msg) => {
87
+ error: (msg, line2) => {
82
88
  if (!Toast._ref) {
83
89
  return;
84
90
  }
@@ -87,6 +93,20 @@ export const ToastBottomHelper = {
87
93
  type: 'error',
88
94
  position: 'bottom',
89
95
  text1: msg,
96
+ text2: line2,
97
+ visibilityTime: 1000,
98
+ });
99
+ },
100
+ info: (msg, line2) => {
101
+ if (!Toast._ref) {
102
+ return;
103
+ }
104
+
105
+ Toast.show({
106
+ type: 'info',
107
+ position: 'bottom',
108
+ text1: msg,
109
+ text2: line2,
90
110
  visibilityTime: 1000,
91
111
  });
92
112
  },