diff --git a/native/profile/linked-devices-bottom-sheet.react.js b/native/profile/linked-devices-bottom-sheet.react.js
--- a/native/profile/linked-devices-bottom-sheet.react.js
+++ b/native/profile/linked-devices-bottom-sheet.react.js
@@ -2,7 +2,7 @@
import invariant from 'invariant';
import * as React from 'react';
-import { View, Text } from 'react-native';
+import { View, Text, ActivityIndicator } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDeviceListUpdate } from 'lib/shared/device-list-utils.js';
@@ -18,7 +18,7 @@
import Button from '../components/button.react.js';
import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
-import { useStyles } from '../themes/colors.js';
+import { useColors, useStyles } from '../themes/colors.js';
import type { BottomSheetRef } from '../types/bottom-sheet.js';
import Alert from '../utils/alert.js';
@@ -60,38 +60,46 @@
const styles = useStyles(unboundStyles);
const insets = useSafeAreaInsets();
- const handleDeviceRemoval = React.useCallback(async () => {
- const authMetadata = await getAuthMetadata();
- const { userID } = authMetadata;
- if (!userID) {
- throw new Error('No user ID');
- }
+ // This state is on purpose never set to false, to avoid case when
+ // this state is flipped before Bottom Sheet is closed, which is
+ // terrible for UX. When trying to remove device again,
+ // a new component is rendered with the default state
+ // value `false`.
+ const [removingInProgress, setRemovingInProgress] = React.useState(false);
+ const handleDeviceRemoval = React.useCallback(async () => {
+ setRemovingInProgress(true);
try {
+ const authMetadata = await getAuthMetadata();
+ const { userID } = authMetadata;
+ if (!userID) {
+ throw new Error('No user ID');
+ }
+
await runDeviceListUpdate({
type: 'remove',
deviceID,
});
- } catch (err) {
- console.log('Primary device error:', err);
+
+ const messageContents: DeviceLogoutP2PMessage = {
+ type: userActionsP2PMessageTypes.LOG_OUT_DEVICE,
+ };
+
+ await broadcastEphemeralMessage(
+ JSON.stringify(messageContents),
+ [{ userID, deviceID }],
+ authMetadata,
+ );
+ } catch (e) {
+ console.log('Removing device failed:', e);
Alert.alert(
'Removing device failed',
'Failed to update the device list',
[{ text: 'OK' }],
);
+ } finally {
bottomSheetRef.current?.close();
}
-
- const messageContents: DeviceLogoutP2PMessage = {
- type: userActionsP2PMessageTypes.LOG_OUT_DEVICE,
- };
-
- await broadcastEphemeralMessage(
- JSON.stringify(messageContents),
- [{ userID, deviceID }],
- authMetadata,
- );
- bottomSheetRef.current?.close();
}, [
getAuthMetadata,
broadcastEphemeralMessage,
@@ -99,7 +107,7 @@
runDeviceListUpdate,
]);
- const confirmDeviceRemoval = () => {
+ const confirmDeviceRemoval = React.useCallback(() => {
Alert.alert(
'Remove device',
'Are you sure you want to remove this device?',
@@ -109,7 +117,7 @@
],
{ cancelable: true },
);
- };
+ }, [handleDeviceRemoval]);
const onLayout = React.useCallback(() => {
removeDeviceContainerRef.current?.measure(
@@ -128,17 +136,46 @@
);
}, [insets.bottom, setContentHeight]);
- let removeDeviceButton;
- if (shouldDisplayRemoveButton) {
- removeDeviceButton = (
+ const colors = useColors();
+
+ const removeDeviceButton = React.useMemo(() => {
+ if (!shouldDisplayRemoveButton) {
+ return null;
+ }
+
+ let style, content;
+ if (removingInProgress) {
+ style = [styles.buttonContainer, styles.disabledButton];
+ content = (
+
+
+
+ );
+ } else {
+ style = [styles.buttonContainer, styles.removeButton];
+ content = Remove device;
+ }
+
+ return (
);
- }
+ }, [
+ colors.panelForegroundLabel,
+ confirmDeviceRemoval,
+ removingInProgress,
+ shouldDisplayRemoveButton,
+ styles.buttonContainer,
+ styles.disabledButton,
+ styles.removeButton,
+ styles.removeButtonText,
+ styles.spinner,
+ ]);
return (
@@ -157,14 +194,24 @@
container: {
paddingHorizontal: 16,
},
- removeButtonContainer: {
- backgroundColor: 'vibrantRedButton',
+ buttonContainer: {
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
},
+ removeButton: {
+ backgroundColor: 'vibrantRedButton',
+ },
+ disabledButton: {
+ backgroundColor: 'disabledButton',
+ },
removeButtonText: {
color: 'floatingButtonLabel',
+ fontSize: 16,
+ },
+ spinner: {
+ justifyContent: 'center',
+ alignItems: 'center',
},
};