Skip to content

Commit 503a81f

Browse files
committed
feat: Add snackbars for toggling lock status
1 parent 01768db commit 503a81f

File tree

5 files changed

+177
-99
lines changed

5 files changed

+177
-99
lines changed

src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useDevice } from 'lib/seam/devices/use-device.js'
22
import { isLockDevice, type LockDevice } from 'lib/seam/locks/lock-device.js'
33
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
4+
import { useToggleLockSnackbar } from 'lib/seam/locks/use-toggle-lock-snackbar.js'
45
import { Button } from 'lib/ui/Button.js'
56
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
67
import { TextButton } from 'lib/ui/TextButton.js'
@@ -49,35 +50,48 @@ function Content(props: {
4950
onSelectDevice: (deviceId: string) => void
5051
}): JSX.Element {
5152
const { device, disableLockUnlock, onSelectDevice } = props
52-
const toggleLock = useToggleLock()
53+
54+
const { renderSnackbar, showToggleSnackbar } = useToggleLockSnackbar()
55+
const toggleLock = useToggleLock({
56+
onSuccess: () => {
57+
showToggleSnackbar('success')
58+
},
59+
onError: () => {
60+
showToggleSnackbar('error')
61+
},
62+
})
5363

5464
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
5565

5666
return (
57-
<div className='seam-access-code-device'>
58-
<div className='seam-device-image'>
59-
<DeviceImage device={device} />
60-
</div>
61-
<div className='seam-body'>
62-
<div>{device.properties.name}</div>
63-
<TextButton
64-
onClick={() => {
65-
onSelectDevice(device.device_id)
66-
}}
67-
>
68-
{t.deviceDetails}
69-
</TextButton>
67+
<>
68+
{renderSnackbar()}
69+
70+
<div className='seam-access-code-device'>
71+
<div className='seam-device-image'>
72+
<DeviceImage device={device} />
73+
</div>
74+
<div className='seam-body'>
75+
<div>{device.properties.name}</div>
76+
<TextButton
77+
onClick={() => {
78+
onSelectDevice(device.device_id)
79+
}}
80+
>
81+
{t.deviceDetails}
82+
</TextButton>
83+
</div>
84+
{!disableLockUnlock && device.properties.online && (
85+
<Button
86+
onClick={() => {
87+
toggleLock.mutate(device)
88+
}}
89+
>
90+
{toggleLockLabel}
91+
</Button>
92+
)}
7093
</div>
71-
{!disableLockUnlock && device.properties.online && (
72-
<Button
73-
onClick={() => {
74-
toggleLock.mutate(device)
75-
}}
76-
>
77-
{toggleLockLabel}
78-
</Button>
79-
)}
80-
</div>
94+
</>
8195
)
8296
}
8397

src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx

Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js'
99
import { deviceErrorFilter, deviceWarningFilter } from 'lib/seam/filters.js'
1010
import type { LockDevice } from 'lib/seam/locks/lock-device.js'
1111
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
12+
import { useToggleLockSnackbar } from 'lib/seam/locks/use-toggle-lock-snackbar.js'
1213
import { Alerts } from 'lib/ui/Alert/Alerts.js'
1314
import { Button } from 'lib/ui/Button.js'
1415
import { BatteryStatusIndicator } from 'lib/ui/device/BatteryStatusIndicator.js'
@@ -38,11 +39,20 @@ export function LockDeviceDetails({
3839
onEditName,
3940
}: LockDeviceDetailsProps): JSX.Element | null {
4041
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
41-
const toggleLock = useToggleLock()
4242
const { accessCodes } = useAccessCodes({
4343
device_id: device.device_id,
4444
})
4545

46+
const { renderSnackbar, showToggleSnackbar } = useToggleLockSnackbar()
47+
const toggleLock = useToggleLock({
48+
onSuccess: () => {
49+
showToggleSnackbar('success')
50+
},
51+
onError: () => {
52+
showToggleSnackbar('error')
53+
},
54+
})
55+
4656
const lockStatus = device.properties.locked ? t.locked : t.unlocked
4757
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
4858

@@ -88,87 +98,91 @@ export function LockDeviceDetails({
8898
]
8999

90100
return (
91-
<div className={classNames('seam-device-details', className)}>
92-
<ContentHeader title='Device' onBack={onBack} />
93-
<div className='seam-body'>
94-
<div className='seam-summary'>
95-
<div className='seam-content'>
96-
<div className='seam-image'>
97-
<DeviceImage device={device} />
98-
</div>
99-
<div className='seam-info'>
100-
<span className='seam-label'>{t.device}</span>
101-
<EditableDeviceName
102-
tagName='h4'
103-
value={device.properties.name}
104-
className='seam-device-name'
105-
onEdit={onEditName}
106-
/>
107-
<div className='seam-properties'>
108-
<span className='seam-label'>{t.status}:</span>{' '}
109-
<OnlineStatus device={device} />
110-
{device.properties.online && (
111-
<>
112-
<span className='seam-label'>{t.power}:</span>{' '}
113-
<BatteryStatusIndicator device={device} />
114-
</>
115-
)}
116-
<DeviceModel device={device} />
101+
<>
102+
{renderSnackbar()}
103+
104+
<div className={classNames('seam-device-details', className)}>
105+
<ContentHeader title='Device' onBack={onBack} />
106+
<div className='seam-body'>
107+
<div className='seam-summary'>
108+
<div className='seam-content'>
109+
<div className='seam-image'>
110+
<DeviceImage device={device} />
111+
</div>
112+
<div className='seam-info'>
113+
<span className='seam-label'>{t.device}</span>
114+
<EditableDeviceName
115+
tagName='h4'
116+
value={device.properties.name}
117+
className='seam-device-name'
118+
onEdit={onEditName}
119+
/>
120+
<div className='seam-properties'>
121+
<span className='seam-label'>{t.status}:</span>{' '}
122+
<OnlineStatus device={device} />
123+
{device.properties.online && (
124+
<>
125+
<span className='seam-label'>{t.power}:</span>{' '}
126+
<BatteryStatusIndicator device={device} />
127+
</>
128+
)}
129+
<DeviceModel device={device} />
130+
</div>
117131
</div>
118132
</div>
133+
<Alerts alerts={alerts} className='seam-alerts-space-top' />
119134
</div>
120-
<Alerts alerts={alerts} className='seam-alerts-space-top' />
121-
</div>
122-
<div className='seam-box'>
123-
<div
124-
className='seam-content seam-access-codes'
125-
onClick={toggleAccessCodesOpen}
126-
>
127-
<span className='seam-value'>
128-
{accessCodeCount} {t.accessCodes}
129-
</span>
130-
<ChevronRightIcon />
135+
<div className='seam-box'>
136+
<div
137+
className='seam-content seam-access-codes'
138+
onClick={toggleAccessCodesOpen}
139+
>
140+
<span className='seam-value'>
141+
{accessCodeCount} {t.accessCodes}
142+
</span>
143+
<ChevronRightIcon />
144+
</div>
131145
</div>
132-
</div>
133146

134-
<div className='seam-box'>
135-
{device.properties.locked && device.properties.online && (
136-
<div className='seam-content seam-lock-status'>
137-
<div>
138-
<span className='seam-label'>{t.lockStatus}</span>
139-
<span className='seam-value'>{lockStatus}</span>
140-
</div>
141-
<div className='seam-right'>
142-
{!disableLockUnlock &&
143-
device.capabilities_supported.includes('lock') && (
144-
<Button
145-
size='small'
146-
onClick={() => {
147-
toggleLock.mutate(device)
148-
}}
149-
>
150-
{toggleLockLabel}
151-
</Button>
152-
)}
147+
<div className='seam-box'>
148+
{device.properties.locked && device.properties.online && (
149+
<div className='seam-content seam-lock-status'>
150+
<div>
151+
<span className='seam-label'>{t.lockStatus}</span>
152+
<span className='seam-value'>{lockStatus}</span>
153+
</div>
154+
<div className='seam-right'>
155+
{!disableLockUnlock &&
156+
device.capabilities_supported.includes('lock') && (
157+
<Button
158+
size='small'
159+
onClick={() => {
160+
toggleLock.mutate(device)
161+
}}
162+
>
163+
{toggleLockLabel}
164+
</Button>
165+
)}
166+
</div>
153167
</div>
154-
</div>
155-
)}
168+
)}
156169

157-
<AccessCodeLength
158-
supportedCodeLengths={
159-
device.properties?.supported_code_lengths ?? []
170+
<AccessCodeLength
171+
supportedCodeLengths={
172+
device.properties?.supported_code_lengths ?? []
173+
}
174+
/>
175+
</div>
176+
<DeviceInfo
177+
device={device}
178+
disableConnectedAccountInformation={
179+
disableConnectedAccountInformation
160180
}
181+
disableResourceIds={disableResourceIds}
161182
/>
162183
</div>
163-
<DeviceInfo
164-
device={device}
165-
disableConnectedAccountInformation={
166-
disableConnectedAccountInformation
167-
}
168-
disableResourceIds={disableResourceIds}
169-
/>
170184
</div>
171-
</div>
185+
</>
172186
)
173187
}
174188

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useCallback, useState } from 'react'
2+
3+
import { Snackbar, type SnackbarProps } from 'lib/ui/Snackbar/Snackbar.js'
4+
5+
export interface UseToggleLockSnackbarContext {
6+
showToggleSnackbar: (type: SnackbarProps['variant']) => void
7+
renderSnackbar: () => JSX.Element
8+
}
9+
10+
export function useToggleLockSnackbar(): UseToggleLockSnackbarContext {
11+
const [visible, setVisible] = useState(false)
12+
const [variant, setVariant] = useState<SnackbarProps['variant']>('success')
13+
14+
return {
15+
showToggleSnackbar: useCallback((type) => {
16+
setVariant(type)
17+
setVisible(true)
18+
}, []),
19+
renderSnackbar: useCallback(() => {
20+
return (
21+
<Snackbar
22+
variant={variant}
23+
visible={visible}
24+
onClose={() => {
25+
setVisible(false)
26+
}}
27+
message={
28+
variant === 'success' ? t.successfullyUpdated : t.failedToUpdate
29+
}
30+
autoDismiss
31+
/>
32+
)
33+
}, [visible, variant]),
34+
}
35+
}
36+
37+
const t = {
38+
successfullyUpdated: 'Lock status has been successfully updated',
39+
failedToUpdate: 'Failed to update lock status',
40+
}

src/lib/seam/locks/use-toggle-lock.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import {
1212

1313
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
1414

15-
export type UseToggleLockParams = never
16-
1715
export type UseToggleLockData = undefined
1816

1917
export type UseToggleLockMutationVariables = Pick<Device, 'device_id'> & {
@@ -29,7 +27,14 @@ type MutationError =
2927
| SeamActionAttemptFailedError<ToggleLockActionAttempt>
3028
| SeamActionAttemptTimeoutError<ToggleLockActionAttempt>
3129

32-
export function useToggleLock(): UseMutationResult<
30+
interface UseToggleLockParams {
31+
onError?: () => void
32+
onSuccess?: () => void
33+
}
34+
35+
export function useToggleLock(
36+
params: UseToggleLockParams = {}
37+
): UseMutationResult<
3338
UseToggleLockData,
3439
MutationError,
3540
UseToggleLockMutationVariables
@@ -92,12 +97,17 @@ export function useToggleLock(): UseMutationResult<
9297
)
9398
},
9499
onError: async (_error, variables) => {
100+
params.onError?.()
101+
95102
await queryClient.invalidateQueries({
96103
queryKey: ['devices', 'list'],
97104
})
98105
await queryClient.invalidateQueries({
99106
queryKey: ['devices', 'get', { device_id: variables.device_id }],
100107
})
101108
},
109+
onSuccess() {
110+
params.onSuccess?.()
111+
},
102112
})
103113
}

src/lib/ui/Snackbar/Snackbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js'
77

88
type SnackbarVariant = 'success' | 'error'
99

10-
interface SnackbarProps {
10+
export interface SnackbarProps {
1111
message: string
1212
variant: SnackbarVariant
1313
visible: boolean

0 commit comments

Comments
 (0)