@eohjsc/react-native-smart-city 0.3.87 → 0.3.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eohjsc/react-native-smart-city",
3
3
  "title": "React Native Smart Home",
4
- "version": "0.3.87",
4
+ "version": "0.3.88",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -17,7 +17,7 @@ describe('Test MenuActionAddNew', () => {
17
17
  let wrapper;
18
18
 
19
19
  // flat list issue
20
- it.skip('onItemClick MenuActionAddNew', async () => {
20
+ it('onItemClick MenuActionAddNew', async () => {
21
21
  const mockFunc = jest.fn();
22
22
  await act(async () => {
23
23
  wrapper = create(wrapComponent(dataActions));
@@ -9,7 +9,7 @@ describe('Test MenuActionList', () => {
9
9
  const listItem = [item(1)];
10
10
  let wrapper;
11
11
 
12
- it.skip('onItemClick MenuActionList', async () => {
12
+ it('onItemClick MenuActionList', async () => {
13
13
  const mockOnItemClick = jest.fn();
14
14
  const mockHideModal = jest.fn();
15
15
 
@@ -20,6 +20,8 @@ const ModalPopupCT = ({
20
20
  isVisible = false,
21
21
  onPressConfirm,
22
22
  onPressCancel,
23
+ textShowAlert,
24
+ footer,
23
25
  }) => {
24
26
  const [isChecked, setIsChecked] = useState(false);
25
27
  const isUnHideButton = useMemo(() => {
@@ -59,7 +61,7 @@ const ModalPopupCT = ({
59
61
  <View style={styles.viewIconAlert}>{iconAlert}</View>
60
62
  <View style={styles.viewTextAlert}>
61
63
  <Text type="H4" color={Colors.Gray9}>
62
- {t('text_alert_modal_popup_ct')}
64
+ {textShowAlert || t('text_alert_modal_popup_ct')}
63
65
  </Text>
64
66
  </View>
65
67
  </View>
@@ -94,41 +96,45 @@ const ModalPopupCT = ({
94
96
  </View>
95
97
  )}
96
98
  </View>
97
- <View style={styles.footer}>
98
- <View style={styles.buttonBottom}>
99
- <TouchableOpacity
100
- onPress={onPressCancel}
101
- style={styles.buttonCancel}
102
- >
103
- <Text
104
- type="H4"
105
- bold
106
- color={Colors.Gray9}
107
- style={styles.paddingBottom3}
108
- >
109
- {t('cancel')}
110
- </Text>
111
- </TouchableOpacity>
112
- <View style={styles.buttonConfirm}>
99
+ {footer || (
100
+ <View style={styles.footer}>
101
+ <View style={styles.buttonBottom}>
113
102
  <TouchableOpacity
114
- onPress={onPressConfirm}
115
- disabled={isUnHideButton ? !isUnHideButton : !isChecked}
103
+ onPress={onPressCancel}
104
+ style={styles.buttonCancel}
116
105
  >
117
106
  <Text
118
107
  type="H4"
119
108
  bold
120
- color={Colors.White}
109
+ color={Colors.Gray9}
121
110
  style={styles.paddingBottom3}
122
111
  >
123
- {t((type === 'reboot' && 'reboot') || 'delete')}
112
+ {t('cancel')}
124
113
  </Text>
125
114
  </TouchableOpacity>
126
- <View
127
- style={isUnHideButton ? {} : !isChecked && styles.disableButton}
128
- />
115
+ <View style={styles.buttonConfirm}>
116
+ <TouchableOpacity
117
+ onPress={onPressConfirm}
118
+ disabled={isUnHideButton ? !isUnHideButton : !isChecked}
119
+ >
120
+ <Text
121
+ type="H4"
122
+ bold
123
+ color={Colors.White}
124
+ style={styles.paddingBottom3}
125
+ >
126
+ {t((type === 'reboot' && 'reboot') || 'delete')}
127
+ </Text>
128
+ </TouchableOpacity>
129
+ <View
130
+ style={
131
+ isUnHideButton ? {} : !isChecked && styles.disableButton
132
+ }
133
+ />
134
+ </View>
129
135
  </View>
130
136
  </View>
131
- </View>
137
+ )}
132
138
  </View>
133
139
  </ModalCustom>
134
140
  );
@@ -4,7 +4,7 @@ const API = {
4
4
  UNIT: {
5
5
  MY_UNITS: () => '/property_manager/units/mine/',
6
6
  SHARED_UNITS: () => '/property_manager/shared_units/',
7
- CREATE_UNIT: () => '/property_manager/units/',
7
+ CREATE_UNIT: () => '/property_manager/iot_dashboard/dev_mode/units/',
8
8
  UNIT_DETAIL: (id) => `/property_manager/units/${id}/`,
9
9
  UNITS_PUBLIC: () => '/property_manager/units/public/',
10
10
  UNIT_SUMMARY: (id) => `/property_manager/units/${id}/summary/`,
@@ -675,4 +675,7 @@ export default {
675
675
  ROW_ITEM_DEVMOD: 'ROW_ITEM_DEVMOD',
676
676
  BUTTON_UNDERSTAND: 'BUTTON_UNDERSTAND',
677
677
  MODAL_POPUP_CUSTOM: 'MODAL_POPUP_CUSTOM',
678
+
679
+ //MemberInfo
680
+ CHANGE_ROLE: 'CHANGE_ROLE',
678
681
  };
@@ -41,10 +41,11 @@ const AddLocationMaps = memo(() => {
41
41
 
42
42
  const onDone = useCallback(() => {
43
43
  navigate(Routes.AddSubUnit, {
44
- location: input,
44
+ location: searchedLocation,
45
45
  isAddUnit: true,
46
46
  });
47
- }, [input, navigate]);
47
+ }, [searchedLocation, navigate]);
48
+
48
49
  const onBack = useCallback(() => {
49
50
  goBack();
50
51
  }, [goBack]);
@@ -95,7 +96,7 @@ const AddLocationMaps = memo(() => {
95
96
  }, []);
96
97
 
97
98
  const onPressRowLocation = useCallback(async (item) => {
98
- setInput(item.description);
99
+ setInput(item);
99
100
  setSearchData([]);
100
101
  const body = {
101
102
  params: {
@@ -111,7 +112,7 @@ const AddLocationMaps = memo(() => {
111
112
  if (success) {
112
113
  const { location } = data.result.geometry;
113
114
  setSearchedLocation({
114
- description: item.description,
115
+ description: item?.description,
115
116
  latitude: location.lat,
116
117
  longitude: location.lng,
117
118
  });
@@ -17,13 +17,15 @@ jest.mock('@react-navigation/native', () => {
17
17
  });
18
18
  describe('Test useGateway', () => {
19
19
  let hook;
20
+
20
21
  beforeEach(async () => {
22
+ mock.reset();
21
23
  await act(() => {
22
24
  hook = renderHook(() => useGateway());
23
25
  });
24
26
  });
27
+
25
28
  afterEach(() => {
26
- mock.reset();
27
29
  Toast.show.mockClear();
28
30
  });
29
31
 
@@ -46,9 +48,17 @@ describe('Test useGateway', () => {
46
48
  .reply(200, { results: [{ id: 1, name: 'chip 1' }] });
47
49
 
48
50
  await act(async () => {
49
- await hook.result.current.fetchDataGateways();
51
+ await hook.result.current.fetchDataGateways(1, 1);
50
52
  });
51
53
  expect(hook.result.current.gateways).toEqual([{ id: 1, name: 'chip 1' }]);
54
+
55
+ await act(async () => {
56
+ await hook.result.current.fetchDataGateways(2, 1);
57
+ });
58
+ expect(hook.result.current.gateways).toEqual([
59
+ { id: 1, name: 'chip 1' },
60
+ { id: 1, name: 'chip 1' },
61
+ ]);
52
62
  });
53
63
 
54
64
  it('test useGateway fetchDevicesGateway isInterval', async () => {
@@ -103,4 +113,18 @@ describe('Test useGateway', () => {
103
113
  visibilityTime: 1000,
104
114
  });
105
115
  });
116
+
117
+ it('test fetchActionConfigDevice', async () => {
118
+ mock.onDelete(API.DEV_MODE.ZIGBEE.CONFIG_MAP(1, 1)).reply(200);
119
+ await act(async () => {
120
+ await hook.result.current.fetchActionConfigDevice(1, 1, true, false);
121
+ });
122
+ expect(hook.result.current.detailDeviceZigbee).toEqual({});
123
+
124
+ mock.onDelete(API.DEV_MODE.ZIGBEE.ACTION(1, 1)).reply(200);
125
+ await act(async () => {
126
+ await hook.result.current.fetchActionConfigDevice(1, 1, true, true);
127
+ });
128
+ expect(hook.result.current.detailDeviceZigbee).toEqual({});
129
+ });
106
130
  });
@@ -68,7 +68,9 @@ export const useGateway = () => {
68
68
  const config_read = data
69
69
  ?.filter(
70
70
  (item) =>
71
- item?.config?.is_read && item?.value_type !== VIRTUAL_TYPE
71
+ !item?.config.is_write &&
72
+ item?.config?.is_read &&
73
+ item?.value_type !== VIRTUAL_TYPE
72
74
  )
73
75
  ?.map((item) => {
74
76
  return {
@@ -86,12 +88,10 @@ export const useGateway = () => {
86
88
  });
87
89
  const config_virtual = data
88
90
  ?.filter((item) => item?.value_type === VIRTUAL_TYPE)
89
- ?.map((item) => {
90
- return {
91
- ...item,
92
- pin_number: item?.pin_number.toString(),
93
- };
94
- });
91
+ ?.map((item) => ({
92
+ ...item,
93
+ pin_number: item?.pin_number.toString(),
94
+ }));
95
95
 
96
96
  setDetailDeviceInternal((prev) => ({
97
97
  ...prev,
@@ -164,8 +164,10 @@ export const useGateway = () => {
164
164
  );
165
165
  success && setGatewayDevices((prev) => ({ ...prev, modbus: [] }));
166
166
  }
167
- success && ToastBottomHelper.success(t('delete_successfully'));
168
- success && navigation.pop(numberGoBack);
167
+ if (success) {
168
+ ToastBottomHelper.success(t('delete_successfully'));
169
+ navigation.pop(numberGoBack);
170
+ }
169
171
  },
170
172
  [navigation]
171
173
  );
@@ -9,7 +9,7 @@ import { HeaderCustom } from '../../commons/Header';
9
9
  import RowMemberInfo from '../GuestInfo/components/RowGuestInfo';
10
10
  import { useIsOwnerOfUnit } from '../../hooks/Common';
11
11
  import { axiosGet } from '../../utils/Apis/axios';
12
- import { AlertAction } from '../../commons';
12
+ import { AlertAction, ViewButtonBottom } from '../../commons';
13
13
  import { useStateAlertAction, useDataMember } from './hooks';
14
14
  import ItemChangeRole from './Components/ItemChangeRole';
15
15
  import MemberSvg from '../../Images/Common/member.svg';
@@ -18,6 +18,7 @@ import styles from './Styles/inforMemberUnitStyles';
18
18
  import { useNavigation, useIsFocused } from '@react-navigation/native';
19
19
  import Routes from '../../utils/Route';
20
20
  import { AccessibilityLabel } from '../../configs/Constants';
21
+ import ModalPopupCT from '../../commons/ModalPopupCT';
21
22
 
22
23
  const InfoMemberUnit = memo(({ route }) => {
23
24
  const t = useTranslations();
@@ -28,6 +29,7 @@ const InfoMemberUnit = memo(({ route }) => {
28
29
  };
29
30
  const { navigate } = useNavigation();
30
31
  const [isLoading, setIsLoading] = useState(true);
32
+ const [isShowWarning, setIsShowWarning] = useState(false);
31
33
  const [memberInfo, setMemberInfo] = useState({});
32
34
  const [itemSelected, setItemSelected] = useState({});
33
35
  const isFocused = useIsFocused();
@@ -56,10 +58,7 @@ const InfoMemberUnit = memo(({ route }) => {
56
58
  if (stateAlertAction?.is_change) {
57
59
  hideAlertAction();
58
60
  if (itemSelected?.role?.is_owner) {
59
- navigate(Routes.EnterPassword, {
60
- dataParams: { unit_id: unit?.id, member },
61
- type: 'infoMemberUnit',
62
- });
61
+ setIsShowWarning(true);
63
62
  }
64
63
  } else {
65
64
  removeMember(
@@ -72,14 +71,19 @@ const InfoMemberUnit = memo(({ route }) => {
72
71
  }, [
73
72
  hideAlertAction,
74
73
  itemSelected?.role?.is_owner,
75
- member,
76
- navigate,
77
74
  removeMember,
78
75
  stateAlertAction?.is_change,
79
76
  stateAlertAction?.member,
80
- unit?.id,
81
77
  ]);
82
78
 
79
+ const handleChangeOwner = useCallback(() => {
80
+ setIsShowWarning(false);
81
+ navigate(Routes.EnterPassword, {
82
+ dataParams: { unit_id: unit?.id, member },
83
+ type: 'infoMemberUnit',
84
+ });
85
+ }, [member, navigate, unit?.id]);
86
+
83
87
  const fetchMemberInfo = useCallback(async () => {
84
88
  setIsLoading(true);
85
89
  const { success, data } = await axiosGet(
@@ -158,6 +162,22 @@ const InfoMemberUnit = memo(({ route }) => {
158
162
  return <></>;
159
163
  }, [isOwner, navigate, unit?.id, memberInfo]);
160
164
 
165
+ const footerWarning = useMemo(() => {
166
+ return (
167
+ <>
168
+ <View style={styles.line} />
169
+ <ViewButtonBottom
170
+ leftTitle={t('cancel')}
171
+ rightTitle={t('change')}
172
+ styleButton={styles.styleButton}
173
+ styleButtonLeftText={styles.textButton}
174
+ onLeftClick={() => setIsShowWarning(false)}
175
+ onRightClick={handleChangeOwner}
176
+ />
177
+ </>
178
+ );
179
+ }, [handleChangeOwner, t]);
180
+
161
181
  useEffect(() => {
162
182
  if (isIdentityOwner) {
163
183
  setItemSelected(itemsRoleModal[0]);
@@ -223,6 +243,7 @@ const InfoMemberUnit = memo(({ route }) => {
223
243
  <TouchableOpacity
224
244
  onPress={onPressChangeRole}
225
245
  style={styles.role}
246
+ accessibilityLabel={AccessibilityLabel.CHANGE_ROLE}
226
247
  >
227
248
  <View style={styles.leftRole}>
228
249
  <Text type="Body" color={Colors.Gray7}>
@@ -271,6 +292,18 @@ const InfoMemberUnit = memo(({ route }) => {
271
292
  >
272
293
  <ChangeRoleContent />
273
294
  </AlertAction>
295
+
296
+ <ModalPopupCT
297
+ isVisible={isShowWarning}
298
+ title={t('transfer_ownership')}
299
+ subTitle={t('text_change_owner')}
300
+ isShowAlert
301
+ isShowUnderstand={false}
302
+ textShowAlert={t('text_alert_change_owner')}
303
+ styleWrapButton={styles.wrapButton}
304
+ styleRightButton={styles.button}
305
+ footer={footerWarning}
306
+ />
274
307
  </>
275
308
  );
276
309
  });
@@ -89,4 +89,15 @@ export default StyleSheet.create({
89
89
  removeButtonStyle: {
90
90
  color: Colors.Red,
91
91
  },
92
+ line: {
93
+ borderTopWidth: 1,
94
+ borderTopColor: Colors.Neutral.Neutral3,
95
+ },
96
+ styleButton: {
97
+ paddingTop: 24,
98
+ paddingBottom: 0,
99
+ },
100
+ textButton: {
101
+ color: Colors.Gray9,
102
+ },
92
103
  });
@@ -7,10 +7,13 @@ import InfoMemberUnit from '../InfoMemberUnit';
7
7
  import { HeaderCustom } from '../../../commons/Header';
8
8
  import { SCProvider } from '../../../context';
9
9
  import { mockSCStore } from '../../../context/mockStore';
10
- import { AlertAction } from '../../../commons';
10
+ import { AlertAction, ViewButtonBottom } from '../../../commons';
11
11
  import API from '../../../configs/API';
12
12
  import { AccessibilityLabel } from '../../../configs/Constants';
13
13
  import api from '../../../utils/Apis/axios';
14
+ import ItemChangeRole from '../Components/ItemChangeRole';
15
+ import ModalPopupCT from '../../../commons/ModalPopupCT';
16
+ import Routes from '../../../utils/Route';
14
17
 
15
18
  const mock = new MockAdapter(api.axiosInstance);
16
19
 
@@ -53,6 +56,7 @@ describe('Test InfoMemberUnit', () => {
53
56
  unit: {
54
57
  id: 1,
55
58
  name: 'unit',
59
+ user_id: 1,
56
60
  },
57
61
  member: {
58
62
  id: 1,
@@ -107,7 +111,7 @@ describe('Test InfoMemberUnit', () => {
107
111
 
108
112
  it('render InfoMemberUnit delete member', async () => {
109
113
  mock.onGet(API.SHARE.UNIT_MEMBER_INFO(1, 1)).reply(200, {
110
- id: 1,
114
+ id: 2,
111
115
  identity: 'owner',
112
116
  phone_number: '88888',
113
117
  name: 'user',
@@ -131,4 +135,60 @@ describe('Test InfoMemberUnit', () => {
131
135
  });
132
136
  expect(mockGoBack).toBeCalled();
133
137
  });
138
+
139
+ it('test change owner', async () => {
140
+ mock.onGet(API.SHARE.UNIT_MEMBER_INFO(1, 1)).reply(200, {
141
+ id: 2,
142
+ identity: 'owner',
143
+ phone_number: '88888',
144
+ name: 'user',
145
+ email: 'abc@gmail.com',
146
+ });
147
+ await act(async () => {
148
+ tree = await create(wrapComponent(route));
149
+ });
150
+ const instance = tree.root;
151
+
152
+ const touchChangeRole = instance.findByProps({
153
+ accessibilityLabel: AccessibilityLabel.CHANGE_ROLE,
154
+ });
155
+ await act(async () => {
156
+ await touchChangeRole.props.onPress();
157
+ });
158
+
159
+ const alertAction = instance.findByType(AlertAction);
160
+ expect(alertAction.props.visible).toEqual(true);
161
+
162
+ const itemChangeRole = instance.findAllByType(ItemChangeRole);
163
+ expect(itemChangeRole).toHaveLength(2);
164
+
165
+ const modalPopupCT = instance.findByType(ModalPopupCT);
166
+ await act(async () => {
167
+ await itemChangeRole[0].props.onPress();
168
+ });
169
+ await act(async () => {
170
+ await alertAction.props.rightButtonClick();
171
+ });
172
+ expect(alertAction.props.visible).toEqual(false);
173
+ expect(modalPopupCT.props.isVisible).toEqual(true);
174
+
175
+ const viewButtonBottom = instance.findAllByType(ViewButtonBottom);
176
+ expect(viewButtonBottom).toHaveLength(2);
177
+
178
+ await act(async () => {
179
+ await viewButtonBottom[1].props.onRightClick();
180
+ });
181
+ expect(modalPopupCT.props.isVisible).toEqual(false);
182
+ expect(mockedNavigate).toHaveBeenCalledWith(Routes.EnterPassword, {
183
+ dataParams: {
184
+ unit_id: 1,
185
+ member: {
186
+ id: 1,
187
+ name: 'user1',
188
+ share_id: 1,
189
+ },
190
+ },
191
+ type: 'infoMemberUnit',
192
+ });
193
+ });
134
194
  });
@@ -29,7 +29,7 @@ const AddSubUnit = ({ route }) => {
29
29
  unit,
30
30
  addType,
31
31
  isAddUnit,
32
- location = '',
32
+ location = {},
33
33
  isInsideUnit,
34
34
  } = route?.params;
35
35
  const [roomName, setRoomName] = useState('');
@@ -50,8 +50,10 @@ const AddSubUnit = ({ route }) => {
50
50
  awaitCreate.current = true;
51
51
  const dataObj = {
52
52
  name: roomName,
53
- address: location,
53
+ address: location.description,
54
54
  background: wallpaper,
55
+ lat: location.latitude,
56
+ lng: location.longitude,
55
57
  };
56
58
  const formData = createFormData(dataObj, ['background']);
57
59
  const { success, data } = await axiosPost(
@@ -161,7 +163,7 @@ const AddSubUnit = ({ route }) => {
161
163
 
162
164
  const validateData = useMemo(() => {
163
165
  if (isAddUnit) {
164
- return roomName === '' || wallpaper === '' || location === '';
166
+ return roomName === '' || wallpaper === '' || location.description === '';
165
167
  } else {
166
168
  return roomName === '' || wallpaper === '';
167
169
  }
@@ -212,9 +214,10 @@ const AddSubUnit = ({ route }) => {
212
214
  <Text style={styles.addWallpaper}>{t('geolocation')}</Text>
213
215
  <Text
214
216
  style={styles.textLocation}
215
- color={location && Colors.Primary}
217
+ color={location.description && Colors.Primary}
216
218
  >
217
- {location || t('text_explain_add_geolocation')}
219
+ {location.description ||
220
+ t('text_explain_add_geolocation')}
218
221
  </Text>
219
222
  </View>
220
223
  </TouchableWithoutFeedback>
@@ -14,6 +14,7 @@ export default StyleSheet.create({
14
14
  },
15
15
  bottomButton: {
16
16
  backgroundColor: Colors.White,
17
+ marginBottom: 100,
17
18
  },
18
19
  wrapMarker: {
19
20
  zIndex: 3,
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-hooks';
3
+ import { SCProvider } from '../../../../context';
4
+ import { mockSCStore } from '../../../../context/mockStore';
5
+ import { useUnitConnectRemoteDevices } from '../useUnitConnectRemoteDevices';
6
+
7
+ const mockSetAction = jest.fn();
8
+
9
+ const wrapper = ({ children }) => <SCProvider>{children}</SCProvider>;
10
+
11
+ jest.mock('react', () => {
12
+ return {
13
+ ...jest.requireActual('react'),
14
+ useContext: () => ({
15
+ setAction: mockSetAction,
16
+ stateData: mockSCStore({
17
+ app: {
18
+ isNetworkConnected: true,
19
+ },
20
+ bluetooth: {
21
+ isEnabled: true,
22
+ },
23
+ iot: {
24
+ lgthinq: {
25
+ unitConnected: [
26
+ {
27
+ id: 1,
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ }),
33
+ }),
34
+ };
35
+ });
36
+
37
+ describe('Test useUnitConnectRemoteDevices', () => {
38
+ const unit = {
39
+ id: 1,
40
+ remote_control_options: {
41
+ lg_thinq: [
42
+ {
43
+ id: 1,
44
+ lg_devices: [{ device_id: 1, configs: [{ id: 1, name: 'name' }] }],
45
+ },
46
+ ],
47
+ bluetooth: [1],
48
+ googlehome: [1],
49
+ },
50
+ };
51
+ it('test handleLgThinqConnect', async () => {
52
+ renderHook(() => useUnitConnectRemoteDevices(unit), {
53
+ wrapper,
54
+ });
55
+ expect(mockSetAction).not.toBeCalled();
56
+ });
57
+ });
@@ -1125,5 +1125,8 @@
1125
1125
  "data_address": "Data address",
1126
1126
  "methods": "{number} methods",
1127
1127
  "io_method": "Input/Output method",
1128
+ "transfer_ownership": "Transfer ownership",
1129
+ "text_change_owner": "Ownership permissions of this unit to the user you have selected.",
1130
+ "text_alert_change_owner": "You will be removed from this Unit after transfer ownership.",
1128
1131
  "the_system_has_been_upgraded" : "To provide a better user experience, [Era] will perform a system upgrade.The upgrade will take about 10 minutes.\n\n Some device control functions will be interrupted during the upgrade.[Era] would like to ask for your understanding for this inconvenience.\n\nBest regards."
1129
1132
  }
@@ -1124,6 +1124,9 @@
1124
1124
  "data_address": "Địa chỉ dữ liệu",
1125
1125
  "methods": "{number} phương thức",
1126
1126
  "io_method": "Giao thức Input/Output",
1127
+ "transfer_ownership": "Chuyển quyền sở hữu",
1128
+ "text_change_owner": "Quyền sở hữu đơn vị này cho người dùng bạn đã chọn.",
1129
+ "text_alert_change_owner": "Bạn sẽ bị xóa khỏi Unit này sau khi chuyển quyền sở hữu.",
1127
1130
  "the_system_has_been_upgraded" : "Để mang lại trải nghiệm tốt hơn cho người dùng, [Era] sẽ tiến hành nâng cấp hệ thống.Việc nâng cấp sẽ diễn ra trong khoảng 10 phút.\n\nMột số chức năng điều khiển thiết bị sẽ bị gián đoạn trong thời gian nâng cấp.[Era] kính mong quý khách hàng thông cảm cho sự bất tiện này.\n\nTrân trọng "
1128
1131
 
1129
1132
  }