diff --git a/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx b/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx index eeb8e9425..db2330025 100644 --- a/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx +++ b/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx @@ -1,8 +1,11 @@ +import { useState } from 'react' + import { useDevice } from 'lib/seam/devices/use-device.js' import { isLockDevice, type LockDevice } from 'lib/seam/locks/lock-device.js' import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js' import { Button } from 'lib/ui/Button.js' import { DeviceImage } from 'lib/ui/device/DeviceImage.js' +import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js' import { TextButton } from 'lib/ui/TextButton.js' export function AccessCodeDevice({ @@ -49,35 +52,64 @@ function Content(props: { onSelectDevice: (deviceId: string) => void }): JSX.Element { const { device, disableLockUnlock, onSelectDevice } = props - const toggleLock = useToggleLock() + const [snackbarVisible, setSnackbarVisible] = useState(false) + const [snackbarVariant, setSnackbarVariant] = + useState('success') + + const toggleLock = useToggleLock({ + onSuccess: () => { + setSnackbarVisible(true) + setSnackbarVariant('success') + }, + onError: () => { + setSnackbarVisible(true) + setSnackbarVariant('error') + }, + }) const toggleLockLabel = device.properties.locked ? t.unlock : t.lock return ( -
-
- -
-
-
{device.properties.name}
- { - onSelectDevice(device.device_id) - }} - > - {t.deviceDetails} - + <> + { + setSnackbarVisible(false) + }} + message={ + snackbarVariant === 'success' + ? t.successfullyUpdated + : t.failedToUpdate + } + autoDismiss + /> + +
+
+ +
+
+
{device.properties.name}
+ { + onSelectDevice(device.device_id) + }} + > + {t.deviceDetails} + +
+ {!disableLockUnlock && device.properties.online && ( + + )}
- {!disableLockUnlock && device.properties.online && ( - - )} -
+ ) } @@ -85,4 +117,6 @@ const t = { deviceDetails: 'Device details', unlock: 'Unlock', lock: 'Lock', + successfullyUpdated: 'Lock status has been successfully updated', + failedToUpdate: 'Failed to update lock status', } diff --git a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx index 9e19e47d0..cfc741a6c 100644 --- a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames' +import { useState } from 'react' import { ChevronRightIcon } from 'lib/icons/ChevronRight.js' import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js' @@ -16,6 +17,7 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js' import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js' import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js' import { ContentHeader } from 'lib/ui/layout/ContentHeader.js' +import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js' import { useToggle } from 'lib/ui/use-toggle.js' interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { @@ -38,11 +40,25 @@ export function LockDeviceDetails({ onEditName, }: LockDeviceDetailsProps): JSX.Element | null { const [accessCodesOpen, toggleAccessCodesOpen] = useToggle() - const toggleLock = useToggleLock() const { accessCodes } = useAccessCodes({ device_id: device.device_id, }) + const [snackbarVisible, setSnackbarVisible] = useState(false) + const [snackbarVariant, setSnackbarVariant] = + useState('success') + + const toggleLock = useToggleLock({ + onSuccess: () => { + setSnackbarVisible(true) + setSnackbarVariant('success') + }, + onError: () => { + setSnackbarVisible(true) + setSnackbarVariant('error') + }, + }) + const lockStatus = device.properties.locked ? t.locked : t.unlocked const toggleLockLabel = device.properties.locked ? t.unlock : t.lock @@ -88,87 +104,103 @@ export function LockDeviceDetails({ ] return ( -
- -
-
-
-
- -
-
- {t.device} - -
- {t.status}:{' '} - - {device.properties.online && ( - <> - {t.power}:{' '} - - - )} - + <> + { + setSnackbarVisible(false) + }} + message={ + snackbarVariant === 'success' + ? t.successfullyUpdated + : t.failedToUpdate + } + autoDismiss + /> + +
+ +
+
+
+
+ +
+
+ {t.device} + +
+ {t.status}:{' '} + + {device.properties.online && ( + <> + {t.power}:{' '} + + + )} + +
+
- -
-
-
- - {accessCodeCount} {t.accessCodes} - - +
+
+ + {accessCodeCount} {t.accessCodes} + + +
-
-
- {device.properties.locked && device.properties.online && ( -
-
- {t.lockStatus} - {lockStatus} -
-
- {!disableLockUnlock && - device.capabilities_supported.includes('lock') && ( - - )} +
+ {device.properties.locked && device.properties.online && ( +
+
+ {t.lockStatus} + {lockStatus} +
+
+ {!disableLockUnlock && + device.capabilities_supported.includes('lock') && ( + + )} +
-
- )} + )} - +
+
-
-
+ ) } @@ -208,4 +240,6 @@ const t = { lockStatus: 'Lock status', status: 'Status', power: 'Power', + successfullyUpdated: 'Lock status has been successfully updated', + failedToUpdate: 'Failed to update lock status', } diff --git a/src/lib/seam/locks/use-toggle-lock.ts b/src/lib/seam/locks/use-toggle-lock.ts index 502c44e7a..8c4a87b15 100644 --- a/src/lib/seam/locks/use-toggle-lock.ts +++ b/src/lib/seam/locks/use-toggle-lock.ts @@ -12,8 +12,6 @@ import { import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' -export type UseToggleLockParams = never - export type UseToggleLockData = undefined export type UseToggleLockMutationVariables = Pick & { @@ -29,7 +27,14 @@ type MutationError = | SeamActionAttemptFailedError | SeamActionAttemptTimeoutError -export function useToggleLock(): UseMutationResult< +interface UseToggleLockParams { + onError?: () => void + onSuccess?: () => void +} + +export function useToggleLock( + params: UseToggleLockParams = {} +): UseMutationResult< UseToggleLockData, MutationError, UseToggleLockMutationVariables @@ -92,6 +97,8 @@ export function useToggleLock(): UseMutationResult< ) }, onError: async (_error, variables) => { + params.onError?.() + await queryClient.invalidateQueries({ queryKey: ['devices', 'list'], }) @@ -99,5 +106,8 @@ export function useToggleLock(): UseMutationResult< queryKey: ['devices', 'get', { device_id: variables.device_id }], }) }, + onSuccess() { + params.onSuccess?.() + }, }) } diff --git a/src/lib/ui/Snackbar/Snackbar.tsx b/src/lib/ui/Snackbar/Snackbar.tsx index a9f7a339b..66a49201b 100644 --- a/src/lib/ui/Snackbar/Snackbar.tsx +++ b/src/lib/ui/Snackbar/Snackbar.tsx @@ -5,9 +5,9 @@ import { CheckGreenIcon } from 'lib/icons/CheckGreen.js' import { CloseWhiteIcon } from 'lib/icons/CloseWhite.js' import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js' -type SnackbarVariant = 'success' | 'error' +export type SnackbarVariant = 'success' | 'error' -interface SnackbarProps { +export interface SnackbarProps { message: string variant: SnackbarVariant visible: boolean