@eohjsc/react-native-smart-city 0.7.16 → 0.7.18

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.7.16",
4
+ "version": "0.7.18",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -109,6 +109,63 @@ describe('Test SharedUnit', () => {
109
109
  });
110
110
  });
111
111
 
112
+ it('Test remove pin and start shared unit', async () => {
113
+ const navigation = useNavigation();
114
+ item.is_pin = true;
115
+ item.is_star = true;
116
+ const mockedRenewItem = jest.fn();
117
+ await act(async () => {
118
+ tree = await create(
119
+ wrapComponent(item, navigation, undefined, mockedRenewItem)
120
+ );
121
+ });
122
+ const instance = tree.root;
123
+
124
+ const iconAddPinSharedUnit = instance.findAll(
125
+ (el) =>
126
+ el.props.accessibilityLabel ===
127
+ AccessibilityLabel.ICON_ADD_PIN_SHARED_UNIT + '-69'
128
+ );
129
+ const iconAddStarSharedUnit = instance.findAll(
130
+ (el) =>
131
+ el.props.accessibilityLabel ===
132
+ AccessibilityLabel.ICON_ADD_STAR_SHARED_UNIT + '-69'
133
+ );
134
+
135
+ const iconRemovePinSharedUnit = instance.find(
136
+ (el) =>
137
+ el.props.accessibilityLabel ===
138
+ AccessibilityLabel.ICON_REMOVE_PIN_SHARED_UNIT + '-69'
139
+ );
140
+ mock.onPost(API.UNIT.UNPIN_UNIT(3)).reply(200);
141
+ await act(async () => {
142
+ iconRemovePinSharedUnit.props.onPress();
143
+ });
144
+ mock.onPost(API.UNIT.UNPIN_UNIT(3)).reply(400);
145
+ await act(async () => {
146
+ iconRemovePinSharedUnit.props.onPress();
147
+ });
148
+ expect(mockedRenewItem).toHaveBeenCalledTimes(1);
149
+
150
+ const iconRemoveStarSharedUnit = instance.find(
151
+ (el) =>
152
+ el.props.accessibilityLabel ===
153
+ AccessibilityLabel.ICON_REMOVE_STAR_SHARED_UNIT + '-69'
154
+ );
155
+ mock.onPost(API.UNIT.UNSTAR_UNIT(3)).reply(200);
156
+ await act(async () => {
157
+ iconRemoveStarSharedUnit.props.onPress();
158
+ });
159
+ mock.onPost(API.UNIT.UNSTAR_UNIT(3)).reply(400);
160
+ await act(async () => {
161
+ iconRemoveStarSharedUnit.props.onPress();
162
+ });
163
+
164
+ expect(mockedRenewItem).toHaveBeenCalledTimes(2);
165
+ expect(iconAddPinSharedUnit).toHaveLength(0);
166
+ expect(iconAddStarSharedUnit).toHaveLength(0);
167
+ });
168
+
112
169
  // it('test create SharedUnit unit without unit owner', async () => {
113
170
  // const navigation = useNavigation();
114
171
  // delete unit.owner_name;
@@ -251,6 +251,10 @@ export default {
251
251
  AUTOMATE_TITLE_NOTIFY: 'AUTOMATE_TITLE_NOTIFY',
252
252
  AUTOMATE_MESSAGE_NOTIFY: 'AUTOMATE_MESSAGE_NOTIFY',
253
253
  AUTOMATE_INPUT_DELAY: 'AUTOMATE_INPUT_DELAY',
254
+ AUTOMATE_SHOW_LOCAL_CONTROL: 'AUTOMATE_SHOW_LOCAL_CONTROL',
255
+ AUTOMATE_DISABLE_LOCAL_CONTROL: 'AUTOMATE_DISABLE_LOCAL_CONTROL',
256
+ AUTOMATE_ENABLE_LOCAL_CONTROL: 'AUTOMATE_ENABLE_LOCAL_CONTROL',
257
+
254
258
  // Parking input maunaly spot
255
259
  PARKING_SPOT_INFO_BUTTON: 'PARKING_SPOT_INFO_BUTTON',
256
260
  PARKING_SPOT_CONFIRM_SPOT: 'PARKING_SPOT_CONFIRM_SPOT',
@@ -5,6 +5,7 @@ import { BackHandler, Platform } from 'react-native';
5
5
  export const useBlockBack = (actionBack) => {
6
6
  const navigation = useNavigation();
7
7
  const isListening = useRef(false);
8
+ const isBeforeRemoveHandled = useRef(false);
8
9
 
9
10
  const blockBack = useCallback(() => {
10
11
  actionBack && actionBack();
@@ -13,8 +14,15 @@ export const useBlockBack = (actionBack) => {
13
14
 
14
15
  const blockBeforeRemove = useCallback(
15
16
  (e) => {
16
- e.preventDefault();
17
- blockBack();
17
+ if (isBeforeRemoveHandled.current) {
18
+ return;
19
+ }
20
+ const { type } = e.data.action;
21
+ if (type === 'GO_BACK' || type === 'POP') {
22
+ isBeforeRemoveHandled.current = true;
23
+ e.preventDefault();
24
+ blockBack();
25
+ }
18
26
  },
19
27
  [blockBack]
20
28
  );
@@ -183,8 +183,8 @@ export default StyleSheet.create({
183
183
  justifyContent: 'center',
184
184
  alignItems: 'center',
185
185
  },
186
- width40: {
187
- width: '40%',
186
+ width50: {
187
+ width: '50%',
188
188
  },
189
189
  popoverStyle: {
190
190
  width: '100%',
@@ -42,11 +42,16 @@ describe('Test ScriptDetail', () => {
42
42
  const mockAddListener = useNavigation().addListener;
43
43
  const mockedNavigate = useNavigation().navigate;
44
44
  const mockedDispatch = useNavigation().dispatch;
45
+ const mockedGetState = useNavigation().getState;
46
+
45
47
  beforeEach(() => {
46
48
  mockGoBack.mockClear();
47
49
  mockAddListener.mockClear();
48
50
  mockedNavigate.mockClear();
49
51
  mockedDispatch.mockClear();
52
+ mockedGetState.mockReturnValue({
53
+ routes: [{ name: 'Home' }],
54
+ });
50
55
  global.mockedNavigate.mockClear();
51
56
  mock.reset();
52
57
  route = {
@@ -111,12 +116,15 @@ describe('Test ScriptDetail', () => {
111
116
  };
112
117
  });
113
118
 
119
+ afterEach(() => {
120
+ jest.useRealTimers();
121
+ });
122
+
114
123
  const expectMockedDispatch = (screen, screenParams) => {
115
- const mockState = {
116
- routes: [{ name: 'Home' }],
117
- };
118
- const updatedState = mockedDispatch.mock.calls[0][0](mockState);
119
- expect(updatedState).toEqual({
124
+ act(() => {
125
+ jest.runAllTimers();
126
+ });
127
+ expect(mockedDispatch).toHaveBeenCalledWith({
120
128
  type: 'RESET',
121
129
  payload: {
122
130
  index: 2,
@@ -136,6 +144,12 @@ describe('Test ScriptDetail', () => {
136
144
  },
137
145
  });
138
146
  expect(mockedDispatch).toHaveBeenCalledTimes(1);
147
+ mockedGetState.mockReturnValue({
148
+ routes: [
149
+ { name: 'Home' },
150
+ { name: Routes.ScriptDetail, params: route.params },
151
+ ],
152
+ });
139
153
  mockedDispatch.mockClear();
140
154
  };
141
155
 
@@ -359,6 +373,7 @@ describe('Test ScriptDetail', () => {
359
373
  });
360
374
 
361
375
  it('test press add action', async () => {
376
+ jest.useFakeTimers();
362
377
  data = [
363
378
  {
364
379
  id: 1,
@@ -425,6 +440,50 @@ describe('Test ScriptDetail', () => {
425
440
  expectMockedDispatch(Routes.SetupScriptDelay, {
426
441
  automate: route.params.preAutomate,
427
442
  });
443
+ });
444
+
445
+ it('test press add email and sms', async () => {
446
+ jest.useFakeTimers();
447
+ data = [
448
+ {
449
+ id: 1,
450
+ unit: 1,
451
+ script_items: null,
452
+ },
453
+ ];
454
+ mock.onGet(API.AUTOMATE.SCRIPT(1)).reply(200, data);
455
+ const auth = {
456
+ account: {
457
+ user: {
458
+ permissions: {
459
+ max_actions_per_automation: 2,
460
+ },
461
+ },
462
+ },
463
+ };
464
+ await act(async () => {
465
+ tree = await create(wrapComponent(route, { auth }));
466
+ });
467
+ const instance = tree.root;
468
+ const button = instance.find(
469
+ (el) =>
470
+ el.props.accessibilityLabel ===
471
+ AccessibilityLabel.BUTTON_ADD_SCRIPT_ACTION &&
472
+ el.type === TouchableOpacity
473
+ );
474
+ await act(async () => {
475
+ await button.props.onPress();
476
+ });
477
+ const texts = instance.findByType(AddActionScript);
478
+ expect(texts.props.isVisible).toBeTruthy();
479
+
480
+ const listScriptActions = instance.findAll(
481
+ (el) =>
482
+ el.props.accessibilityLabel ===
483
+ AccessibilityLabel.AUTOMATE_LIST_SCRIPT_ACTION &&
484
+ el.type === TouchableOpacity
485
+ );
486
+ expect(listScriptActions).toHaveLength(5);
428
487
  await act(async () => {
429
488
  await listScriptActions[3].props.onPress();
430
489
  });
@@ -433,6 +492,103 @@ describe('Test ScriptDetail', () => {
433
492
  automate: route.params.preAutomate,
434
493
  unitId: route.params.preAutomate.unit,
435
494
  });
495
+ await act(async () => {
496
+ await listScriptActions[4].props.onPress();
497
+ });
498
+
499
+ expectMockedDispatch(Routes.SetupScriptSms, {
500
+ automate: route.params.preAutomate,
501
+ unitId: route.params.preAutomate.unit,
502
+ });
503
+ });
504
+
505
+ it('test press add action when has closeScreen is present in stacks', async () => {
506
+ jest.useFakeTimers();
507
+ data = [
508
+ {
509
+ id: 1,
510
+ unit: 1,
511
+ script_items: null,
512
+ },
513
+ ];
514
+ mock.onGet(API.AUTOMATE.SCRIPT(1)).reply(200, data);
515
+ const auth = {
516
+ account: {
517
+ user: {
518
+ permissions: {
519
+ max_actions_per_automation: 2,
520
+ },
521
+ },
522
+ },
523
+ };
524
+ route.params.closeScreen = Routes.UnitDetail;
525
+ await act(async () => {
526
+ tree = await create(wrapComponent(route, { auth }));
527
+ });
528
+ const instance = tree.root;
529
+ const button = instance.find(
530
+ (el) =>
531
+ el.props.accessibilityLabel ===
532
+ AccessibilityLabel.BUTTON_ADD_SCRIPT_ACTION &&
533
+ el.type === TouchableOpacity
534
+ );
535
+ await act(async () => {
536
+ await button.props.onPress();
537
+ });
538
+ const texts = instance.findByType(AddActionScript);
539
+ expect(texts.props.isVisible).toBeTruthy();
540
+
541
+ const listScriptActions = instance.findAll(
542
+ (el) =>
543
+ el.props.accessibilityLabel ===
544
+ AccessibilityLabel.AUTOMATE_LIST_SCRIPT_ACTION &&
545
+ el.type === TouchableOpacity
546
+ );
547
+ expect(listScriptActions).toHaveLength(5);
548
+
549
+ mockedGetState.mockReturnValue({
550
+ routes: [
551
+ { name: 'Home' },
552
+ { name: Routes.UnitDetail, params: { unitId: 1 } },
553
+ { name: Routes.ScriptDetail, params: route.params },
554
+ ],
555
+ });
556
+ await act(async () => {
557
+ await listScriptActions[0].props.onPress();
558
+ });
559
+ await act(async () => {
560
+ jest.runAllTimers();
561
+ });
562
+ expect(mockedDispatch).toHaveBeenCalledWith({
563
+ type: 'RESET',
564
+ payload: {
565
+ index: 3,
566
+ routes: [
567
+ {
568
+ name: 'Home',
569
+ },
570
+ {
571
+ name: Routes.UnitDetail,
572
+ params: { unitId: 1 },
573
+ },
574
+ {
575
+ name: Routes.ScriptDetail,
576
+ params: route.params,
577
+ },
578
+ {
579
+ name: Routes.SelectControlDevices,
580
+ params: {
581
+ unitId: route.params.preAutomate.unit,
582
+ automateId: route.params.preAutomate.id,
583
+ numberActionCanAdd: 2,
584
+ closeScreen: undefined,
585
+ routeName: null,
586
+ },
587
+ },
588
+ ],
589
+ },
590
+ });
591
+ expect(mockedDispatch).toHaveBeenCalledTimes(1);
436
592
  });
437
593
 
438
594
  it('test press disable script', async () => {
@@ -573,6 +729,7 @@ describe('Test ScriptDetail', () => {
573
729
  _testGoToActivityLog(3, AUTOMATE_TYPE.VALUE_CHANGE, 'automate', undefined);
574
730
 
575
731
  it('Test render textCondition value change >', async () => {
732
+ jest.useFakeTimers();
576
733
  const automate = {
577
734
  can_edit: true,
578
735
  type: AUTOMATE_TYPE.VALUE_CHANGE,
@@ -613,33 +770,10 @@ describe('Test ScriptDetail', () => {
613
770
  menuActionMore.props.listMenuItem[2].doAction();
614
771
  });
615
772
 
616
- const mockState = {
617
- routes: [{ name: 'Home' }],
618
- };
619
- const updatedState = mockedDispatch.mock.calls[0][0](mockState);
620
- expect(updatedState).toEqual({
621
- type: 'RESET',
622
- payload: {
623
- index: 2,
624
- routes: [
625
- {
626
- name: 'Home',
627
- },
628
- {
629
- name: Routes.ScriptDetail,
630
- params: route.params,
631
- },
632
- {
633
- name: Routes.AddUnknownTypeSmart,
634
- params: {
635
- automate: automate,
636
- closeScreen: undefined,
637
- },
638
- },
639
- ],
640
- },
773
+ expectMockedDispatch(Routes.AddUnknownTypeSmart, {
774
+ automate: automate,
775
+ closeScreen: undefined,
641
776
  });
642
- expect(mockedDispatch).toHaveBeenCalledTimes(1);
643
777
 
644
778
  expect(menuActionMore.props.listMenuItem[0].text).toEqual('Device display');
645
779
  await act(async () => {
@@ -755,7 +889,10 @@ describe('Test ScriptDetail', () => {
755
889
 
756
890
  // beforeRemove test when swipe to back
757
891
  const navigation = useNavigation();
758
- const beforeRemoveEvent = { preventDefault: jest.fn() };
892
+ const beforeRemoveEvent = {
893
+ preventDefault: jest.fn(),
894
+ data: { action: { type: 'GO_BACK' } },
895
+ };
759
896
  const beforeRemoveListener = navigation.addListener.mock.calls.find(
760
897
  ([event]) => event === 'beforeRemove'
761
898
  )[1];
@@ -771,6 +908,34 @@ describe('Test ScriptDetail', () => {
771
908
  });
772
909
  });
773
910
 
911
+ it('test skip block back when navigate on ios', async () => {
912
+ Platform.OS = 'ios';
913
+ route.params.closeScreen = Routes.UnitDetail;
914
+ route.params.preAutomate.unit = 2;
915
+
916
+ await act(async () => {
917
+ await create(wrapComponent(route));
918
+ });
919
+
920
+ // beforeRemove test when navigate
921
+ const navigation = useNavigation();
922
+ const beforeRemoveEvent = {
923
+ preventDefault: jest.fn(),
924
+ data: { action: { type: 'NAVIGATE' } },
925
+ };
926
+ const beforeRemoveListener = navigation.addListener.mock.calls.find(
927
+ ([event]) => event === 'beforeRemove'
928
+ )[1];
929
+
930
+ await act(async () => {
931
+ beforeRemoveListener(beforeRemoveEvent);
932
+ });
933
+
934
+ expect(mockAddListener).toHaveBeenCalled();
935
+ expect(beforeRemoveEvent.preventDefault).not.toHaveBeenCalled();
936
+ expect(mockedNavigate).not.toHaveBeenCalled();
937
+ });
938
+
774
939
  it('test navigate to UnitDetail on event hardwareBackPress', async () => {
775
940
  Platform.OS = 'android';
776
941
  jest.spyOn(BackHandler, 'addEventListener');
@@ -797,4 +962,64 @@ describe('Test ScriptDetail', () => {
797
962
  unitId: 2,
798
963
  });
799
964
  });
965
+
966
+ it('test enable local control', async () => {
967
+ mock.onPost(API.AUTOMATE.ENABLE_LOCAL_CONTROL(1)).reply(200);
968
+ mock
969
+ .onGet(API.DEV_MODE.GATEWAY.SHARED())
970
+ .reply(200, [{ name: 'Chip', id: 1 }]);
971
+
972
+ await act(async () => {
973
+ tree = await create(wrapComponent(route));
974
+ });
975
+ const instance = tree.root;
976
+ const show_local_control = instance.find(
977
+ (el) =>
978
+ el.props.accessibilityLabel ===
979
+ AccessibilityLabel.AUTOMATE_SHOW_LOCAL_CONTROL &&
980
+ el.type === TouchableOpacity
981
+ );
982
+ await act(async () => {
983
+ await show_local_control.props.onPress();
984
+ });
985
+ const disable = instance.find(
986
+ (el) =>
987
+ el.props.accessibilityLabel ===
988
+ AccessibilityLabel.AUTOMATE_ENABLE_LOCAL_CONTROL &&
989
+ el.type === TouchableOpacity
990
+ );
991
+ await act(async () => {
992
+ await disable.props.onPress();
993
+ });
994
+
995
+ expect(mock.history.post[0].url).toEqual(
996
+ API.AUTOMATE.ENABLE_LOCAL_CONTROL(1)
997
+ );
998
+ });
999
+
1000
+ it('test disable local control', async () => {
1001
+ await act(async () => {
1002
+ tree = await create(wrapComponent(route));
1003
+ });
1004
+ const instance = tree.root;
1005
+ const show_local_control = instance.find(
1006
+ (el) =>
1007
+ el.props.accessibilityLabel ===
1008
+ AccessibilityLabel.AUTOMATE_SHOW_LOCAL_CONTROL &&
1009
+ el.type === TouchableOpacity
1010
+ );
1011
+ await act(async () => {
1012
+ await show_local_control.props.onPress();
1013
+ });
1014
+ const disable = instance.find(
1015
+ (el) =>
1016
+ el.props.accessibilityLabel ===
1017
+ AccessibilityLabel.AUTOMATE_DISABLE_LOCAL_CONTROL &&
1018
+ el.type === TouchableOpacity
1019
+ );
1020
+ await act(async () => {
1021
+ await disable.props.onPress();
1022
+ });
1023
+ expect(mock.history.post.length).toBe(0);
1024
+ });
800
1025
  });
@@ -46,7 +46,7 @@ import { Card } from '../../../commons/CardShadow';
46
46
  const PreventDoubleTouch = withPreventDoubleClick(TouchableOpacity);
47
47
 
48
48
  const ScriptDetail = ({ route }) => {
49
- const { dispatch, navigate, goBack } = useNavigation();
49
+ const { dispatch, navigate, goBack, getState } = useNavigation();
50
50
  const { params = {} } = route;
51
51
  const refMenuAction = useRef();
52
52
  const { childRef, showingPopover, showPopoverWithRef, hidePopover } =
@@ -114,38 +114,41 @@ const ScriptDetail = ({ route }) => {
114
114
 
115
115
  const handleNavigate = useCallback(
116
116
  (screen, screenParams) => {
117
- dispatch((state) => {
118
- const index = state.routes.findIndex((r) => r.name === closeScreen);
119
- let routes = [];
120
- if (!closeScreen) {
121
- routes = state.routes;
122
- } else if (index >= 0) {
123
- routes = state.routes.slice(0, index + 1);
124
- }
125
- const hasScriptDetail = routes.some(
126
- (r) => r.name === Routes.ScriptDetail
127
- );
128
-
129
- if (!hasScriptDetail) {
130
- routes.push({
131
- name: Routes.ScriptDetail,
132
- params: route.params,
133
- });
134
- }
117
+ const state = getState();
118
+ const index = state.routes.findIndex((r) => r.name === closeScreen);
119
+ let routes = [];
120
+ if (!closeScreen) {
121
+ routes = state.routes;
122
+ } else if (index >= 0) {
123
+ routes = state.routes.slice(0, index + 1);
124
+ }
125
+ const hasScriptDetail = routes.some(
126
+ (r) => r.name === Routes.ScriptDetail
127
+ );
135
128
 
129
+ if (!hasScriptDetail) {
136
130
  routes.push({
137
- name: screen,
138
- params: screenParams,
131
+ name: Routes.ScriptDetail,
132
+ params: route.params,
139
133
  });
134
+ }
140
135
 
141
- return CommonActions.reset({
142
- ...state,
143
- routes: routes,
144
- index: routes.length - 1,
145
- });
136
+ routes.push({
137
+ name: screen,
138
+ params: screenParams,
146
139
  });
140
+
141
+ setTimeout(() => {
142
+ dispatch(
143
+ CommonActions.reset({
144
+ routes: routes,
145
+ index: routes.length - 1,
146
+ })
147
+ );
148
+ }, 500); // Workaround: Fix crash on iOS when resetting the stack with dispatch.
149
+ // Delay added to ensure navigation state updates before dispatching reset.
147
150
  },
148
- [closeScreen, dispatch, route.params]
151
+ [closeScreen, dispatch, getState, route.params]
149
152
  );
150
153
 
151
154
  const handleUpdateAutomate = useCallback(async () => {
@@ -267,7 +270,10 @@ const ScriptDetail = ({ route }) => {
267
270
  closeScreen === Routes.MultiUnits ||
268
271
  closeScreen === Routes.Automate
269
272
  ) {
270
- navigate(closeScreen, {});
273
+ navigate({
274
+ name: closeScreen,
275
+ merge: true,
276
+ });
271
277
  } else {
272
278
  goBack();
273
279
  }
@@ -356,6 +362,10 @@ const ScriptDetail = ({ route }) => {
356
362
 
357
363
  const onPressSelectChip = useCallback(
358
364
  async (enable_local_control, item) => {
365
+ if (!enable_local_control && !local_control.chip_id_local_control) {
366
+ setIsShowLocalControl(false);
367
+ return;
368
+ }
359
369
  const { success } = await axiosPost(
360
370
  API.AUTOMATE.ENABLE_LOCAL_CONTROL(automateId),
361
371
  {
@@ -372,20 +382,23 @@ const ScriptDetail = ({ route }) => {
372
382
  });
373
383
  }
374
384
  },
375
- [automateId]
385
+ [automateId, local_control.chip_id_local_control]
376
386
  );
377
387
 
378
388
  const renderLocalControl = useMemo(() => {
379
389
  if (!!can_edit && type !== AUTOMATE_TYPE.ONE_TAP) {
380
390
  return (
381
391
  <View style={styles.row}>
382
- <Text type="H3" semibold style={styles.width40}>
392
+ <Text type="H3" semibold style={styles.width50}>
383
393
  {t('local_control')}
384
394
  </Text>
385
395
  <Card style={styles.card}>
386
396
  <TouchableOpacity
387
397
  style={styles.localControl}
388
398
  onPress={onShowLocalControl}
399
+ accessibilityLabel={
400
+ AccessibilityLabel.AUTOMATE_SHOW_LOCAL_CONTROL
401
+ }
389
402
  >
390
403
  <Text numberOfLines={1}>
391
404
  {local_control.is_local_control
@@ -534,27 +547,35 @@ const ScriptDetail = ({ route }) => {
534
547
  onBackButtonPress={onCloseLocalControl}
535
548
  onBackdropPress={onCloseLocalControl}
536
549
  >
537
- <View style={styles.popoverStyle}>
550
+ <View key={'localControl'} style={styles.popoverStyle}>
538
551
  <TouchableOpacity
539
552
  style={styles.textDisable}
553
+ key={'listChip'}
540
554
  onPress={() => {
541
555
  onPressSelectChip(false, {
542
556
  id: local_control.chip_id_local_control,
543
557
  name: local_control.chip_local_control,
544
558
  });
545
559
  }}
560
+ accessibilityLabel={
561
+ AccessibilityLabel.AUTOMATE_DISABLE_LOCAL_CONTROL
562
+ }
546
563
  >
547
564
  <Text>{t('disable')}</Text>
548
565
  {!local_control.is_local_control && (
549
566
  <IconOutline style={styles.checked} name={'check'} size={20} />
550
567
  )}
551
568
  </TouchableOpacity>
552
- {listChipShared.map((item) => (
569
+ {listChipShared.map((item, index) => (
553
570
  <TouchableOpacity
571
+ key={'listChip' + index} // Add key fix warning
554
572
  style={styles.listChip}
555
573
  onPress={() => {
556
574
  onPressSelectChip(true, item);
557
575
  }}
576
+ accessibilityLabel={
577
+ AccessibilityLabel.AUTOMATE_ENABLE_LOCAL_CONTROL
578
+ }
558
579
  >
559
580
  <Text>{item.name}</Text>
560
581
  {local_control.chip_id_local_control === item.id &&
@@ -1081,7 +1081,7 @@ export default {
1081
1081
  automate: 'Tự động',
1082
1082
  smart: 'Thông minh',
1083
1083
  enable_this_script: 'Kích hoạt kịch bản này',
1084
- local_control: 'Điều khiển cục bộ',
1084
+ local_control: 'Điều khiển tại biên',
1085
1085
  choose_gateway: 'Chọn gateway',
1086
1086
  this_script_has_been_disabled: 'Kịch bản này đã bị vô hiệu hoá',
1087
1087
  disable: 'Vô hiệu hóa',
@@ -1090,7 +1090,7 @@ export default {
1090
1090
  sms_alarm: 'Cảnh báo SMS',
1091
1091
  message_sms:
1092
1092
  'Nội dung cảnh báo: “Đang có tín hiệu báo cháy từ lầu 1, vui lòng kiểm tra.”',
1093
- only_in_local_control: 'Chỉ hoạt động ở chế độ điều khiển cục bộ',
1093
+ only_in_local_control: 'Chỉ hoạt động ở chế độ điều khiển tại biên',
1094
1094
  send_app_notification: 'Gửi thông báo qua ứng dụng',
1095
1095
  delay_the_action: 'Trì hoãn hành động',
1096
1096
  wait: 'Chờ đợi',