@eohjsc/react-native-smart-city 0.7.26 → 0.7.30
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/index.js +2 -0
- package/package.json +2 -1
- package/src/commons/Dashboard/MyDashboardDevice/__test__/index.test.js +68 -0
- package/src/commons/Dashboard/MyDashboardDevice/index.js +46 -11
- package/src/commons/Dashboard/MyUnit/__test__/MyUnit.test.js +43 -11
- package/src/commons/Dashboard/MyUnit/index.js +40 -32
- package/src/commons/ModalAlert/index.js +51 -0
- package/src/commons/ModalAlert/styles.js +54 -0
- package/src/commons/SubUnit/ShortDetail.js +20 -4
- package/src/commons/SubUnit/__test__/ShortDetail.test.js +46 -1
- package/src/configs/API.js +6 -0
- package/src/configs/AccessibilityLabel.js +1 -0
- package/src/configs/Constants.js +7 -0
- package/src/configs/SCConfig.js +6 -0
- package/src/context/SCContext.tsx +12 -1
- package/src/context/SCStore.ts +14 -0
- package/src/context/actionType.ts +10 -0
- package/src/context/mockStore.ts +30 -1
- package/src/context/reducer.ts +35 -0
- package/src/hooks/IoT/useRemoteControl.js +4 -1
- package/src/hooks/IoT/useWatchSharedChips.js +130 -0
- package/src/hooks/Review/__test__/useInAppReview.test.js +99 -0
- package/src/hooks/Review/useInAppReview.js +70 -0
- package/src/hooks/useMqtt.js +78 -27
- package/src/iot/Monitor.js +149 -26
- package/src/iot/UpdateStates.js +60 -0
- package/src/iot/mqtt.js +177 -22
- package/src/navigations/UnitStack.js +16 -0
- package/src/screens/ActivityLog/FilterPopup.js +4 -79
- package/src/screens/ActivityLog/ItemLog.js +1 -0
- package/src/screens/ActivityLog/__test__/FilterPopup.test.js +2 -6
- package/src/screens/ActivityLog/__test__/index.test.js +51 -29
- package/src/screens/ActivityLog/index.js +0 -1
- package/src/screens/ActivityLog/styles/filterPopupStyles.js +5 -2
- package/src/screens/AddNewGateway/RenameNewDevices.js +5 -0
- package/src/screens/AddNewGateway/__test__/RenameNewDevices.test.js +18 -0
- package/src/screens/Automate/AddNewAction/ReceiverSelect.js +208 -0
- package/src/screens/Automate/AddNewAction/SetupScriptEmail.js +1 -1
- package/src/screens/Automate/AddNewAction/SetupScriptNotify.js +18 -28
- package/src/screens/Automate/AddNewAction/SetupScriptReceiverEmail.js +22 -129
- package/src/screens/Automate/AddNewAction/SetupScriptReceiverNotify.js +59 -0
- package/src/screens/Automate/AddNewAction/SetupScriptReceiverSms.js +22 -129
- package/src/screens/Automate/AddNewAction/SetupScriptSms.js +1 -1
- package/src/screens/Automate/AddNewAction/Styles/{SetupScriptReceiverEmailStyles.js → ReceiverSelectStyles.js} +18 -1
- package/src/screens/Automate/AddNewAction/__test__/SetupScriptNotify.test.js +16 -33
- package/src/screens/Automate/AddNewAction/__test__/SetupScriptReceiverEmail.test.js +10 -8
- package/src/screens/Automate/AddNewAction/__test__/SetupScriptReceiverNotify.test.js +217 -0
- package/src/screens/Automate/AddNewAction/__test__/SetupScriptReceiverSms.test.js +10 -8
- package/src/screens/Automate/Components/InputName.js +5 -1
- package/src/screens/Automate/OneTap/__test__/AddNewOneTap.test.js +18 -0
- package/src/screens/Automate/ScriptDetail/index.js +6 -6
- package/src/screens/CreatePassword/__test__/index.test.js +133 -0
- package/src/screens/CreatePassword/index.js +134 -0
- package/src/screens/CreatePassword/styles.js +45 -0
- package/src/screens/Device/__test__/DeviceDetail-3rdparty.test.js +447 -0
- package/src/screens/Device/__test__/DeviceDetail-arduino.test.js +344 -0
- package/src/screens/Device/__test__/{mqttDetail.test.js → DeviceDetail-modbus.test.js} +287 -320
- package/src/screens/Device/__test__/DeviceDetail-zigbee.test.js +451 -0
- package/src/screens/Device/__test__/DeviceDetail.test.js +502 -0
- package/src/screens/Device/__test__/detail.test.js +61 -3
- package/src/screens/Device/__test__/sensorDisplayItem.test.js +28 -3
- package/src/screens/Device/components/SensorDisplayItem.js +16 -14
- package/src/screens/Device/detail.js +14 -6
- package/src/screens/Device/hooks/useDeviceWatchConfigControl.js +3 -2
- package/src/screens/Device/styles.js +0 -5
- package/src/screens/EnterPassword/__test__/EnterPassword.test.js +76 -1
- package/src/screens/EnterPassword/index.js +34 -4
- package/src/screens/EnterPassword/styles.js +1 -1
- package/src/utils/FactoryGateway.js +597 -0
- package/src/utils/I18n/translations/en.js +10 -0
- package/src/utils/I18n/translations/vi.js +10 -0
- package/src/utils/Route/index.js +3 -1
- package/src/utils/Validation.js +5 -0
- package/src/utils/store.js +5 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import MockAdapter from 'axios-mock-adapter';
|
|
2
3
|
import { Image, View } from 'react-native';
|
|
3
4
|
import mock from 'react-native-permissions/mock';
|
|
4
5
|
import { act, create } from 'react-test-renderer';
|
|
6
|
+
import { API } from '../../../configs';
|
|
5
7
|
import { AccessibilityLabel } from '../../../configs/Constants';
|
|
6
8
|
import { SCProvider } from '../../../context';
|
|
7
9
|
import { mockSCStore } from '../../../context/mockStore';
|
|
8
10
|
import { watchMultiConfigs } from '../../../iot/Monitor';
|
|
9
11
|
import { keyPermission } from '../../../utils/Permission/common';
|
|
12
|
+
import api from '../../../utils/Apis/axios';
|
|
10
13
|
import Routes from '../../../utils/Route';
|
|
14
|
+
import { gatewayDataFactory } from '../../../utils/FactoryGateway';
|
|
11
15
|
import ItemHanetDevice from '../../Device/Hanet/ItemHanetDevice';
|
|
12
16
|
import ItemAddNew from '../../Device/ItemAddNew';
|
|
13
17
|
import ItemDevice from '../../Device/ItemDevice';
|
|
@@ -15,6 +19,8 @@ import Text from '../../Text';
|
|
|
15
19
|
import { DeviceTemplate } from '../DeviceTemplate/DeviceTemplate';
|
|
16
20
|
import ShortDetailSubUnit from '../ShortDetail';
|
|
17
21
|
|
|
22
|
+
const mockAxios = new MockAdapter(api.axiosInstance);
|
|
23
|
+
|
|
18
24
|
jest.mock('../../../iot/Monitor');
|
|
19
25
|
|
|
20
26
|
const wrapComponent = (props) => (
|
|
@@ -32,7 +38,9 @@ jest.mock('../../../iot/states', () => ({
|
|
|
32
38
|
}));
|
|
33
39
|
|
|
34
40
|
describe('test ShortDetail Subunit', () => {
|
|
35
|
-
let tree
|
|
41
|
+
let tree;
|
|
42
|
+
let props;
|
|
43
|
+
|
|
36
44
|
beforeEach(() => {
|
|
37
45
|
jest.useFakeTimers();
|
|
38
46
|
props = {
|
|
@@ -69,6 +77,7 @@ describe('test ShortDetail Subunit', () => {
|
|
|
69
77
|
isNetworkConnected: true,
|
|
70
78
|
isGGHomeConnected: true,
|
|
71
79
|
};
|
|
80
|
+
watchMultiConfigs.mockClear();
|
|
72
81
|
});
|
|
73
82
|
|
|
74
83
|
it('render ShortDetail', async () => {
|
|
@@ -182,6 +191,42 @@ describe('test ShortDetail Subunit', () => {
|
|
|
182
191
|
expect(watchMultiConfigs).toHaveBeenCalledWith([1]);
|
|
183
192
|
});
|
|
184
193
|
|
|
194
|
+
it('render using mqtt not call watch config', async () => {
|
|
195
|
+
mockAxios
|
|
196
|
+
.onGet(API.CHIP.JSON_CONFIGURATION)
|
|
197
|
+
.reply(200, [gatewayDataFactory]);
|
|
198
|
+
props.station.devices = [
|
|
199
|
+
{
|
|
200
|
+
action: {
|
|
201
|
+
color: '#00979D',
|
|
202
|
+
icon: 'caretup',
|
|
203
|
+
id: 1,
|
|
204
|
+
key: '',
|
|
205
|
+
},
|
|
206
|
+
action2: null,
|
|
207
|
+
chip_id: 7689,
|
|
208
|
+
description: null,
|
|
209
|
+
icon: '',
|
|
210
|
+
id: 1,
|
|
211
|
+
name: 'People Counting',
|
|
212
|
+
quick_action: { config_id: 128282 },
|
|
213
|
+
remote_control_options: {},
|
|
214
|
+
station: {},
|
|
215
|
+
status: null,
|
|
216
|
+
status2: null,
|
|
217
|
+
is_managed_by_backend: true,
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
tree = await create(wrapComponent(props));
|
|
223
|
+
});
|
|
224
|
+
await act(async () => {
|
|
225
|
+
jest.runOnlyPendingTimers();
|
|
226
|
+
});
|
|
227
|
+
expect(watchMultiConfigs).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
185
230
|
['ConfigAndEvaluation', 'ConfigValue', 'EvaluationOverConfig'].forEach(
|
|
186
231
|
(template) => {
|
|
187
232
|
it(`render with device template ${template}`, async () => {
|
package/src/configs/API.js
CHANGED
|
@@ -2,8 +2,12 @@ import { SCConfig } from './SCConfig';
|
|
|
2
2
|
|
|
3
3
|
const API = {
|
|
4
4
|
AUTH: {
|
|
5
|
+
CREATE_PASSWORD: '/accounts/create_password/',
|
|
5
6
|
PERMISSIONS: () => '/accounts/permissions/',
|
|
6
7
|
},
|
|
8
|
+
ACCOUNTS: {
|
|
9
|
+
USABLE_PASSWORD: '/accounts/usable_password/',
|
|
10
|
+
},
|
|
7
11
|
UNIT: {
|
|
8
12
|
MY_UNITS: () => '/property_manager/units/mine/',
|
|
9
13
|
SHARED_UNITS: () => '/property_manager/shared_units/',
|
|
@@ -65,6 +69,7 @@ const API = {
|
|
|
65
69
|
`/iot/modules/zigbee/chips/${id}/device_permit_to_join/`,
|
|
66
70
|
},
|
|
67
71
|
JSON_CONFIGURATION: '/chip_manager/chips/json_configuration/',
|
|
72
|
+
SHARED_CONFIGURATION: '/chip_manager/chips/shared_configuration/',
|
|
68
73
|
},
|
|
69
74
|
DEVICE: {
|
|
70
75
|
DEVICE_DETAIL: (id) => `/property_manager/devices/${id}/`,
|
|
@@ -203,6 +208,7 @@ const API = {
|
|
|
203
208
|
IOT: {
|
|
204
209
|
CHIP_MANAGER: {
|
|
205
210
|
WATCH_CONFIGS: () => '/chip_manager/watch_configs_v2/',
|
|
211
|
+
WATCH_DATA_CHIPS: () => '/chip_manager/watch_data_chips/',
|
|
206
212
|
PUSHER_AUTH: () => '/chip_manager/pusher/auth_v2/',
|
|
207
213
|
},
|
|
208
214
|
LG: {
|
|
@@ -164,6 +164,7 @@ export default {
|
|
|
164
164
|
//Enter Password
|
|
165
165
|
ENTER_PASSWORD_TEXT_INPUT_PASSWORD: 'ENTER_PASSWORD_TEXT_INPUT_PASSWORD',
|
|
166
166
|
ENTER_PASSWORD_BUTTON_DONE: 'ENTER_PASSWORD_BUTTON_DONE',
|
|
167
|
+
CREATE_PASSWORD_BUTTON_DONE: 'CREATE_PASSWORD_BUTTON_DONE',
|
|
167
168
|
|
|
168
169
|
// Map Dashboard
|
|
169
170
|
PARKING_MARKER: 'PARKING_MARKER',
|
package/src/configs/Constants.js
CHANGED
package/src/configs/SCConfig.js
CHANGED
|
@@ -99,6 +99,7 @@ const SCDefaultConfig = {
|
|
|
99
99
|
pusherAppKey: '8557fcc63959f564f1aa',
|
|
100
100
|
pusherAppCluster: 'ap1',
|
|
101
101
|
intervalWatchConfigTime: 30000,
|
|
102
|
+
intervalWatchervalWatchDataChipTime: 30000,
|
|
102
103
|
setCurrentSensorDisplay: () => {},
|
|
103
104
|
appName: 'E-Ra',
|
|
104
105
|
appLogo: Images.logo,
|
|
@@ -118,6 +119,8 @@ export class SCConfig {
|
|
|
118
119
|
static pusherAppCluste = SCDefaultConfig.pusherAppCluster;
|
|
119
120
|
static language = 'en';
|
|
120
121
|
static intervalWatchConfigTime = SCDefaultConfig.intervalWatchConfigTime;
|
|
122
|
+
static intervalWatchervalWatchDataChipTime =
|
|
123
|
+
SCDefaultConfig.intervalWatchervalWatchDataChipTime;
|
|
121
124
|
static setCurrentSensorDisplay = SCDefaultConfig.setCurrentSensorDisplay;
|
|
122
125
|
static appName = SCDefaultConfig.appName;
|
|
123
126
|
static appLogo = SCDefaultConfig.appLogo;
|
|
@@ -150,6 +153,9 @@ export const initSCConfig = (config) => {
|
|
|
150
153
|
config.pusherAppCluster ?? SCDefaultConfig.pusherAppCluster;
|
|
151
154
|
SCConfig.intervalWatchConfigTime =
|
|
152
155
|
config.intervalWatchConfigTime ?? SCDefaultConfig.intervalWatchConfigTime;
|
|
156
|
+
SCConfig.intervalWatchervalWatchDataChipTime =
|
|
157
|
+
config.intervalWatchervalWatchDataChipTime ??
|
|
158
|
+
SCDefaultConfig.intervalWatchervalWatchDataChipTime;
|
|
153
159
|
SCConfig.setCurrentSensorDisplay =
|
|
154
160
|
config.setCurrentSensorDisplay ?? SCDefaultConfig.setCurrentSensorDisplay;
|
|
155
161
|
SCConfig.appName = config.appName ?? SCDefaultConfig.appName;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useReducer } from 'react';
|
|
1
|
+
import React, { useContext, useEffect, useRef, useReducer } from 'react';
|
|
2
2
|
import { StyleSheet, View } from 'react-native';
|
|
3
3
|
import Toast from 'react-native-toast-message';
|
|
4
4
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
StatusBar,
|
|
11
11
|
} from './actionType';
|
|
12
12
|
import { initialState, Action, ContextData, reducer } from './reducer';
|
|
13
|
+
import { setSCStoreGetter } from './SCStore';
|
|
13
14
|
import { setConfigGlobalState } from '../iot/states.js';
|
|
14
15
|
import { setAxiosDefaultLanguage } from '../utils/Utils';
|
|
15
16
|
import { Alert } from '../commons';
|
|
@@ -50,6 +51,16 @@ export const SCProvider = ({ children, initState = initialState }) => {
|
|
|
50
51
|
initState
|
|
51
52
|
);
|
|
52
53
|
|
|
54
|
+
const stateRef = useRef<ContextData>(stateData);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
stateRef.current = stateData;
|
|
57
|
+
}, [stateData]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setSCStoreGetter(() => stateRef.current);
|
|
61
|
+
return () => setSCStoreGetter(null);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
53
64
|
const setAuth = (authData: AuthData) => {
|
|
54
65
|
setAction('UPDATE_AUTH', authData);
|
|
55
66
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContextData } from './reducer';
|
|
2
|
+
|
|
3
|
+
let getStateFn: (() => ContextData) | null = null;
|
|
4
|
+
|
|
5
|
+
export const setSCStoreGetter = (getter: () => ContextData) => {
|
|
6
|
+
getStateFn = getter;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getSCState = (): ContextData => {
|
|
10
|
+
if (!getStateFn) {
|
|
11
|
+
throw new Error('SC store is not ready');
|
|
12
|
+
}
|
|
13
|
+
return getStateFn();
|
|
14
|
+
};
|
|
@@ -29,6 +29,8 @@ export const Action = {
|
|
|
29
29
|
SET_HOME_ASSISTANT_CONNECTIONS: 'SET_HOME_ASSISTANT_CONNECTIONS',
|
|
30
30
|
CHANGE_HOME_ASSISTANT_CONN_STATE: 'CHANGE_HOME_ASSISTANT_CONN_STATE',
|
|
31
31
|
SET_LG_THINQ_CONNECTED: 'SET_LG_THINQ_CONNECTED',
|
|
32
|
+
UPDATE_CONFIGS_BY_ID: 'UPDATE_CONFIGS_BY_ID',
|
|
33
|
+
UPDATE_SHARED_CHIPS_BY_ID: 'UPDATE_SHARED_CHIPS_BY_ID',
|
|
32
34
|
UPDATE_VALUE_EVALUATIONS: 'UPDATE_VALUE_EVALUATIONS',
|
|
33
35
|
INIT_VALUE_EVALUATIONS: 'INIT_VALUE_EVALUATIONS',
|
|
34
36
|
NEED_UPDATE_VALUE_EVALUATIONS: 'NEED_UPDATE_VALUE_EVALUATIONS',
|
|
@@ -91,6 +93,14 @@ export type AutomateType = {
|
|
|
91
93
|
isUpdateCondition: false;
|
|
92
94
|
};
|
|
93
95
|
|
|
96
|
+
export type ChipType = {
|
|
97
|
+
sharedChipsById: {};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type ConfigType = {
|
|
101
|
+
configsById: {};
|
|
102
|
+
};
|
|
103
|
+
|
|
94
104
|
export type ActionType = keyof typeof Action;
|
|
95
105
|
|
|
96
106
|
export type ActionDataMap = {
|
package/src/context/mockStore.ts
CHANGED
|
@@ -32,14 +32,27 @@ export const mockDataStore: ContextData = {
|
|
|
32
32
|
},
|
|
33
33
|
automate: {
|
|
34
34
|
starredScriptIds: [],
|
|
35
|
+
isCreateCondition: false,
|
|
36
|
+
isUpdateCondition: false,
|
|
37
|
+
},
|
|
38
|
+
chip: {
|
|
39
|
+
sharedChipsById: {},
|
|
40
|
+
},
|
|
41
|
+
config: {
|
|
42
|
+
configsById: {},
|
|
35
43
|
},
|
|
36
44
|
app: {
|
|
37
45
|
isFirstOpenCamera: true,
|
|
38
46
|
isLavidaSource: false,
|
|
39
47
|
isConnectWifiGateway: false,
|
|
40
|
-
isNetworkConnected:
|
|
48
|
+
isNetworkConnected: false,
|
|
41
49
|
camera_opened: [],
|
|
50
|
+
notificationData: null,
|
|
51
|
+
popoverAnimating: false,
|
|
42
52
|
isLockWhenPickColor: false,
|
|
53
|
+
isNeedUpdateCache: false,
|
|
54
|
+
isDeleteUnitSuccessFully: false,
|
|
55
|
+
appState: 'active',
|
|
43
56
|
},
|
|
44
57
|
iot: {
|
|
45
58
|
bluetooth: {
|
|
@@ -114,11 +127,27 @@ export const mockSCStore = (data: ContextData): ContextData => {
|
|
|
114
127
|
],
|
|
115
128
|
},
|
|
116
129
|
automate: {
|
|
130
|
+
...mockDataStore.automate,
|
|
131
|
+
...data?.automate,
|
|
117
132
|
starredScriptIds: [
|
|
118
133
|
...mockDataStore.automate.starredScriptIds,
|
|
119
134
|
...(data?.automate?.starredScriptIds || []),
|
|
120
135
|
],
|
|
121
136
|
},
|
|
137
|
+
chip: {
|
|
138
|
+
...mockDataStore.chip,
|
|
139
|
+
sharedChipsById: {
|
|
140
|
+
...mockDataStore.chip.sharedChipsById,
|
|
141
|
+
...data?.chip?.sharedChipsById,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
config: {
|
|
145
|
+
...mockDataStore.config,
|
|
146
|
+
configsById: {
|
|
147
|
+
...mockDataStore.config.configsById,
|
|
148
|
+
...data?.config?.configsById,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
122
151
|
iot: {
|
|
123
152
|
...mockDataStore.iot,
|
|
124
153
|
homeassistant: {
|
package/src/context/reducer.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ListAction,
|
|
10
10
|
UnitType,
|
|
11
11
|
AutomateType,
|
|
12
|
+
ChipType,
|
|
13
|
+
ConfigType,
|
|
12
14
|
AppType,
|
|
13
15
|
IoTType,
|
|
14
16
|
DevModeType,
|
|
@@ -25,6 +27,8 @@ export type ContextData = {
|
|
|
25
27
|
statusBar: StatusBar;
|
|
26
28
|
unit: UnitType;
|
|
27
29
|
automate: AutomateType;
|
|
30
|
+
chip: ChipType;
|
|
31
|
+
config: ConfigType;
|
|
28
32
|
app: AppType;
|
|
29
33
|
iot: IoTType;
|
|
30
34
|
valueEvaluations: {};
|
|
@@ -62,6 +66,12 @@ export const initialState = {
|
|
|
62
66
|
automate: {
|
|
63
67
|
starredScriptIds: [],
|
|
64
68
|
},
|
|
69
|
+
chip: {
|
|
70
|
+
sharedChipsById: {},
|
|
71
|
+
},
|
|
72
|
+
config: {
|
|
73
|
+
configsById: {},
|
|
74
|
+
},
|
|
65
75
|
app: {
|
|
66
76
|
isFirstOpenCamera: true,
|
|
67
77
|
isLavidaSource: false,
|
|
@@ -366,6 +376,31 @@ export const reducer = (currentState: ContextData, action: Action) => {
|
|
|
366
376
|
isUpdateCondition: payload,
|
|
367
377
|
},
|
|
368
378
|
};
|
|
379
|
+
|
|
380
|
+
case Action.UPDATE_CONFIGS_BY_ID:
|
|
381
|
+
return {
|
|
382
|
+
...currentState,
|
|
383
|
+
config: {
|
|
384
|
+
...currentState.config,
|
|
385
|
+
configsById: {
|
|
386
|
+
...currentState.config.configsById,
|
|
387
|
+
...payload,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
case Action.UPDATE_SHARED_CHIPS_BY_ID:
|
|
393
|
+
return {
|
|
394
|
+
...currentState,
|
|
395
|
+
chip: {
|
|
396
|
+
...currentState.chip,
|
|
397
|
+
sharedChipsById: {
|
|
398
|
+
...currentState.chip.sharedChipsById,
|
|
399
|
+
...payload,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
369
404
|
case Action.SET_BLUETOOTH_CONNECTED_DEVICE:
|
|
370
405
|
return {
|
|
371
406
|
...currentState,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '../../iot/RemoteControl/Bluetooth';
|
|
10
10
|
import { sendCommandOverHomeAssistant } from '../../iot/RemoteControl/HomeAssistant';
|
|
11
11
|
import { sendCommandOverInternet } from '../../iot/RemoteControl/Internet';
|
|
12
|
+
import useInAppReview from '../Review/useInAppReview';
|
|
12
13
|
import { ToastBottomHelper } from '../../utils/Utils';
|
|
13
14
|
|
|
14
15
|
const handleReconnectionDeviceBle = async (device, action, data, userId) => {
|
|
@@ -30,6 +31,7 @@ const useRemoteControl = (bluetoothDevice) => {
|
|
|
30
31
|
const homeAssistantConnections = useSCContextSelector(
|
|
31
32
|
(state) => state.iot.homeassistant.connections
|
|
32
33
|
);
|
|
34
|
+
const { incrementReviewTrigger } = useInAppReview(false);
|
|
33
35
|
|
|
34
36
|
const sendRemoteCommand = useCallback(
|
|
35
37
|
async (device, action, data, userId, silent = false) => {
|
|
@@ -41,6 +43,7 @@ const useRemoteControl = (bluetoothDevice) => {
|
|
|
41
43
|
);
|
|
42
44
|
return result;
|
|
43
45
|
}
|
|
46
|
+
incrementReviewTrigger();
|
|
44
47
|
if (action.command_prefer_over_bluetooth) {
|
|
45
48
|
// custom control for i/o pin over bluetooth
|
|
46
49
|
if (bluetoothDevice?.isConnected && action.arduino_action) {
|
|
@@ -106,7 +109,7 @@ const useRemoteControl = (bluetoothDevice) => {
|
|
|
106
109
|
|
|
107
110
|
return await sendCommandOverInternet(device, action, data, 'internet');
|
|
108
111
|
},
|
|
109
|
-
[bluetoothDevice, homeAssistantConnections]
|
|
112
|
+
[bluetoothDevice, homeAssistantConnections, incrementReviewTrigger]
|
|
110
113
|
);
|
|
111
114
|
|
|
112
115
|
return sendRemoteCommand;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { useIsFocused } from '@react-navigation/native';
|
|
10
|
+
|
|
11
|
+
import { API } from '../../configs';
|
|
12
|
+
import { Action } from '../../context/actionType';
|
|
13
|
+
import { SCContext, useSCContextSelector } from '../../context';
|
|
14
|
+
import { axiosGet } from '../../utils/Apis/axios';
|
|
15
|
+
import { unwatchMultiDataChips, watchMultiDataChips } from '../../iot/Monitor';
|
|
16
|
+
import { SCConfig } from '../../configs';
|
|
17
|
+
|
|
18
|
+
const useWatchSharedChips = ({ dashboardId, filterChipIds, ready = true }) => {
|
|
19
|
+
const isFocused = useIsFocused();
|
|
20
|
+
const { setAction } = useContext(SCContext);
|
|
21
|
+
const [chipIds, setChipIds] = useState([]);
|
|
22
|
+
|
|
23
|
+
const chipIdsToWatchRef = useRef([]);
|
|
24
|
+
const intervalWatchRef = useRef(null);
|
|
25
|
+
|
|
26
|
+
const isNetworkConnected = useSCContextSelector(
|
|
27
|
+
(state) => state.app.isNetworkConnected
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const handleUpdateConfigsById = useCallback(
|
|
31
|
+
(chips) => {
|
|
32
|
+
const configsById = chips.reduce((acc, chip) => {
|
|
33
|
+
chip.sensors.forEach((sensor) => {
|
|
34
|
+
sensor.configs.forEach((config) => {
|
|
35
|
+
acc[config.id] = config;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return acc;
|
|
39
|
+
}, {});
|
|
40
|
+
setAction(Action.UPDATE_CONFIGS_BY_ID, configsById);
|
|
41
|
+
},
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
[]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handleUpdateSharedChipsById = useCallback(
|
|
47
|
+
(chips) => {
|
|
48
|
+
const sharedChipsById = chips.reduce((acc, chip) => {
|
|
49
|
+
acc[chip.id] = chip;
|
|
50
|
+
return acc;
|
|
51
|
+
}, {});
|
|
52
|
+
setAction(Action.UPDATE_SHARED_CHIPS_BY_ID, sharedChipsById);
|
|
53
|
+
},
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
[]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isNetworkConnected) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const getGateways = async () => {
|
|
63
|
+
const { success, data } = await axiosGet(API.CHIP.SHARED_CONFIGURATION, {
|
|
64
|
+
params: {
|
|
65
|
+
unit: dashboardId,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (success) {
|
|
69
|
+
setChipIds(data.map((chip) => chip.id));
|
|
70
|
+
handleUpdateConfigsById(data);
|
|
71
|
+
handleUpdateSharedChipsById(data);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
ready && getGateways();
|
|
75
|
+
}, [
|
|
76
|
+
dashboardId,
|
|
77
|
+
ready,
|
|
78
|
+
isNetworkConnected,
|
|
79
|
+
handleUpdateConfigsById,
|
|
80
|
+
handleUpdateSharedChipsById,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const chipIdsToWatch = useMemo(() => {
|
|
84
|
+
if (Array.isArray(filterChipIds)) {
|
|
85
|
+
return chipIds.filter((chip) => filterChipIds.includes(chip));
|
|
86
|
+
}
|
|
87
|
+
return chipIds;
|
|
88
|
+
}, [chipIds, filterChipIds]);
|
|
89
|
+
|
|
90
|
+
const cleanupWatch = useCallback(() => {
|
|
91
|
+
if (intervalWatchRef.current) {
|
|
92
|
+
clearInterval(intervalWatchRef.current);
|
|
93
|
+
intervalWatchRef.current = null;
|
|
94
|
+
}
|
|
95
|
+
if (chipIdsToWatchRef.current.length) {
|
|
96
|
+
unwatchMultiDataChips(chipIdsToWatchRef.current);
|
|
97
|
+
chipIdsToWatchRef.current = [];
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!isFocused) {
|
|
103
|
+
cleanupWatch();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!chipIdsToWatch.length) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!chipIdsToWatchRef.current.length) {
|
|
111
|
+
watchMultiDataChips(chipIdsToWatch);
|
|
112
|
+
chipIdsToWatchRef.current = chipIdsToWatch;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (intervalWatchRef.current) {
|
|
116
|
+
clearInterval(intervalWatchRef.current);
|
|
117
|
+
}
|
|
118
|
+
intervalWatchRef.current = setInterval(() => {
|
|
119
|
+
watchMultiDataChips(chipIdsToWatch);
|
|
120
|
+
}, SCConfig.intervalWatchervalWatchDataChipTime);
|
|
121
|
+
}, [isFocused, chipIdsToWatch, cleanupWatch]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
return () => {
|
|
125
|
+
cleanupWatch();
|
|
126
|
+
};
|
|
127
|
+
}, [cleanupWatch]);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default useWatchSharedChips;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import InAppReview from 'react-native-in-app-review';
|
|
3
|
+
import { getObject, storeObject } from '../../../utils/Storage';
|
|
4
|
+
|
|
5
|
+
import useInAppReview from '../useInAppReview';
|
|
6
|
+
|
|
7
|
+
jest.mock('../../../utils/Storage', () => {
|
|
8
|
+
return {
|
|
9
|
+
...jest.requireActual('../../../utils/Storage'),
|
|
10
|
+
getObject: jest.fn(),
|
|
11
|
+
storeObject: jest.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Test useInAppReview', () => {
|
|
16
|
+
const mockOnFailed = jest.fn();
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
Date.now = jest.fn(() => new Date('2025-09-09T10:00:00.00Z'));
|
|
20
|
+
getObject.mockImplementation(() => ({ allow: true }));
|
|
21
|
+
storeObject.mockClear();
|
|
22
|
+
mockOnFailed.mockClear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('test calls RequestInAppReview and updates cache when allowed', async () => {
|
|
26
|
+
await act(async () => {
|
|
27
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
28
|
+
});
|
|
29
|
+
expect(storeObject).toHaveBeenCalledWith('in_app_review', {
|
|
30
|
+
allow: true,
|
|
31
|
+
count: 1,
|
|
32
|
+
last: new Date('2025-09-09T10:00:00.00Z'),
|
|
33
|
+
});
|
|
34
|
+
expect(InAppReview.RequestInAppReview).toHaveBeenCalled();
|
|
35
|
+
expect(mockOnFailed).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('test updates cache even when RequestInAppReview rejects', async () => {
|
|
39
|
+
InAppReview.RequestInAppReview.mockRejectedValueOnce(new Error('fail'));
|
|
40
|
+
await act(async () => {
|
|
41
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
42
|
+
});
|
|
43
|
+
expect(storeObject).toHaveBeenCalledWith('in_app_review', {
|
|
44
|
+
allow: true,
|
|
45
|
+
count: 1,
|
|
46
|
+
last: new Date('2025-09-09T10:00:00.00Z'),
|
|
47
|
+
});
|
|
48
|
+
expect(InAppReview.RequestInAppReview).toHaveBeenCalled();
|
|
49
|
+
expect(mockOnFailed).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('test does nothing when InAppReview is not available', async () => {
|
|
53
|
+
InAppReview.isAvailable.mockReturnValueOnce(false);
|
|
54
|
+
await act(async () => {
|
|
55
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
56
|
+
});
|
|
57
|
+
expect(storeObject).toHaveBeenCalledWith('in_app_review', {
|
|
58
|
+
allow: true,
|
|
59
|
+
count: 1,
|
|
60
|
+
last: new Date('2025-09-09T10:00:00.00Z'),
|
|
61
|
+
});
|
|
62
|
+
expect(InAppReview.RequestInAppReview).not.toHaveBeenCalled();
|
|
63
|
+
expect(mockOnFailed).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('test does nothing when max prompts reached', async () => {
|
|
67
|
+
getObject.mockImplementation(() => ({ allow: true, count: 3 }));
|
|
68
|
+
await act(async () => {
|
|
69
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
70
|
+
});
|
|
71
|
+
expect(storeObject).not.toHaveBeenCalled();
|
|
72
|
+
expect(InAppReview.RequestInAppReview).not.toHaveBeenCalled();
|
|
73
|
+
expect(mockOnFailed).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('test does nothing when still in retry interval', async () => {
|
|
77
|
+
getObject.mockImplementation(() => ({
|
|
78
|
+
allow: true,
|
|
79
|
+
count: 0,
|
|
80
|
+
last: new Date('2025-09-06T10:00:01.00Z'),
|
|
81
|
+
}));
|
|
82
|
+
await act(async () => {
|
|
83
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
84
|
+
});
|
|
85
|
+
expect(storeObject).not.toHaveBeenCalled();
|
|
86
|
+
expect(InAppReview.RequestInAppReview).not.toHaveBeenCalled();
|
|
87
|
+
expect(mockOnFailed).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('test does nothing when first time and allow = false', async () => {
|
|
91
|
+
getObject.mockImplementation(() => ({}));
|
|
92
|
+
await act(async () => {
|
|
93
|
+
renderHook(() => useInAppReview(true, mockOnFailed));
|
|
94
|
+
});
|
|
95
|
+
expect(storeObject).not.toHaveBeenCalled();
|
|
96
|
+
expect(InAppReview.RequestInAppReview).not.toHaveBeenCalled();
|
|
97
|
+
expect(mockOnFailed).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useIsFocused } from '@react-navigation/native';
|
|
2
|
+
import { useCallback, useEffect } from 'react';
|
|
3
|
+
import InAppReview from 'react-native-in-app-review';
|
|
4
|
+
|
|
5
|
+
import { getObject, storeObject } from '../../utils/Storage';
|
|
6
|
+
|
|
7
|
+
const KEY_CACHE = 'in_app_review';
|
|
8
|
+
|
|
9
|
+
const TRIGGER_LIMIT = 10;
|
|
10
|
+
const MAX_PROMPTS = 3;
|
|
11
|
+
const INTERVAL_RETRY = 3 * 24 * 3600 * 1000;
|
|
12
|
+
|
|
13
|
+
const useInAppReview = (ask, onFailed) => {
|
|
14
|
+
const isFocused = useIsFocused();
|
|
15
|
+
|
|
16
|
+
const allowInAppReview = useCallback(async () => {
|
|
17
|
+
const review = await getObject(KEY_CACHE, {});
|
|
18
|
+
await storeObject(KEY_CACHE, { ...review, allow: true });
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const incrementReviewTrigger = useCallback(async () => {
|
|
22
|
+
const review = await getObject(KEY_CACHE, {});
|
|
23
|
+
const trigger = (review.trigger ?? 0) + 1;
|
|
24
|
+
await storeObject(KEY_CACHE, {
|
|
25
|
+
...review,
|
|
26
|
+
allow: trigger >= TRIGGER_LIMIT,
|
|
27
|
+
trigger,
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const askReview = useCallback(async () => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const review = await getObject(KEY_CACHE, {});
|
|
34
|
+
const { allow = false, count = 0, last = 0 } = review;
|
|
35
|
+
if (!allow || count >= MAX_PROMPTS || now - last < INTERVAL_RETRY) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await storeObject(KEY_CACHE, {
|
|
40
|
+
...review,
|
|
41
|
+
count: count + 1,
|
|
42
|
+
last: now,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!InAppReview.isAvailable()) {
|
|
46
|
+
onFailed && onFailed();
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await InAppReview.RequestInAppReview();
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
onFailed && onFailed();
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}, [onFailed]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
ask && isFocused && askReview();
|
|
61
|
+
}, [ask, isFocused, askReview]);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
allowInAppReview,
|
|
65
|
+
askReview,
|
|
66
|
+
incrementReviewTrigger,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default useInAppReview;
|