Skip to content

Commit d6001bf

Browse files
authored
Merge pull request #678 from seamapi/kadir/cx-191-snackbar-feedback-when-locking-and-unlocking
feat: Add snackbars for toggling lock status
2 parents cd18ae1 + 3cbb808 commit d6001bf

File tree

4 files changed

+178
-100
lines changed

4 files changed

+178
-100
lines changed

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

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { useState } from 'react'
2+
13
import { useDevice } from 'lib/seam/devices/use-device.js'
24
import { isLockDevice, type LockDevice } from 'lib/seam/locks/lock-device.js'
35
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
46
import { Button } from 'lib/ui/Button.js'
57
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
8+
import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'
69
import { TextButton } from 'lib/ui/TextButton.js'
710

811
export function AccessCodeDevice({
@@ -49,40 +52,71 @@ function Content(props: {
4952
onSelectDevice: (deviceId: string) => void
5053
}): JSX.Element {
5154
const { device, disableLockUnlock, onSelectDevice } = props
52-
const toggleLock = useToggleLock()
55+
const [snackbarVisible, setSnackbarVisible] = useState(false)
56+
const [snackbarVariant, setSnackbarVariant] =
57+
useState<SnackbarVariant>('success')
58+
59+
const toggleLock = useToggleLock({
60+
onSuccess: () => {
61+
setSnackbarVisible(true)
62+
setSnackbarVariant('success')
63+
},
64+
onError: () => {
65+
setSnackbarVisible(true)
66+
setSnackbarVariant('error')
67+
},
68+
})
5369

5470
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
5571

5672
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>
73+
<>
74+
<Snackbar
75+
variant={snackbarVariant}
76+
visible={snackbarVisible}
77+
onClose={() => {
78+
setSnackbarVisible(false)
79+
}}
80+
message={
81+
snackbarVariant === 'success'
82+
? t.successfullyUpdated
83+
: t.failedToUpdate
84+
}
85+
autoDismiss
86+
/>
87+
88+
<div className='seam-access-code-device'>
89+
<div className='seam-device-image'>
90+
<DeviceImage device={device} />
91+
</div>
92+
<div className='seam-body'>
93+
<div>{device.properties.name}</div>
94+
<TextButton
95+
onClick={() => {
96+
onSelectDevice(device.device_id)
97+
}}
98+
>
99+
{t.deviceDetails}
100+
</TextButton>
101+
</div>
102+
{!disableLockUnlock && device.properties.online && (
103+
<Button
104+
onClick={() => {
105+
toggleLock.mutate(device)
106+
}}
107+
>
108+
{toggleLockLabel}
109+
</Button>
110+
)}
70111
</div>
71-
{!disableLockUnlock && device.properties.online && (
72-
<Button
73-
onClick={() => {
74-
toggleLock.mutate(device)
75-
}}
76-
>
77-
{toggleLockLabel}
78-
</Button>
79-
)}
80-
</div>
112+
</>
81113
)
82114
}
83115

84116
const t = {
85117
deviceDetails: 'Device details',
86118
unlock: 'Unlock',
87119
lock: 'Lock',
120+
successfullyUpdated: 'Lock status has been successfully updated',
121+
failedToUpdate: 'Failed to update lock status',
88122
}

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

Lines changed: 105 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import classNames from 'classnames'
2+
import { useState } from 'react'
23

34
import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
45
import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
@@ -16,6 +17,7 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
1617
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
1718
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
1819
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
20+
import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'
1921
import { useToggle } from 'lib/ui/use-toggle.js'
2022

2123
interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps {
@@ -38,11 +40,25 @@ export function LockDeviceDetails({
3840
onEditName,
3941
}: LockDeviceDetailsProps): JSX.Element | null {
4042
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
41-
const toggleLock = useToggleLock()
4243
const { accessCodes } = useAccessCodes({
4344
device_id: device.device_id,
4445
})
4546

47+
const [snackbarVisible, setSnackbarVisible] = useState(false)
48+
const [snackbarVariant, setSnackbarVariant] =
49+
useState<SnackbarVariant>('success')
50+
51+
const toggleLock = useToggleLock({
52+
onSuccess: () => {
53+
setSnackbarVisible(true)
54+
setSnackbarVariant('success')
55+
},
56+
onError: () => {
57+
setSnackbarVisible(true)
58+
setSnackbarVariant('error')
59+
},
60+
})
61+
4662
const lockStatus = device.properties.locked ? t.locked : t.unlocked
4763
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
4864

@@ -88,87 +104,103 @@ export function LockDeviceDetails({
88104
]
89105

90106
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} />
107+
<>
108+
<Snackbar
109+
variant={snackbarVariant}
110+
visible={snackbarVisible}
111+
onClose={() => {
112+
setSnackbarVisible(false)
113+
}}
114+
message={
115+
snackbarVariant === 'success'
116+
? t.successfullyUpdated
117+
: t.failedToUpdate
118+
}
119+
autoDismiss
120+
/>
121+
122+
<div className={classNames('seam-device-details', className)}>
123+
<ContentHeader title='Device' onBack={onBack} />
124+
<div className='seam-body'>
125+
<div className='seam-summary'>
126+
<div className='seam-content'>
127+
<div className='seam-image'>
128+
<DeviceImage device={device} />
129+
</div>
130+
<div className='seam-info'>
131+
<span className='seam-label'>{t.device}</span>
132+
<EditableDeviceName
133+
tagName='h4'
134+
value={device.properties.name}
135+
className='seam-device-name'
136+
onEdit={onEditName}
137+
/>
138+
<div className='seam-properties'>
139+
<span className='seam-label'>{t.status}:</span>{' '}
140+
<OnlineStatus device={device} />
141+
{device.properties.online && (
142+
<>
143+
<span className='seam-label'>{t.power}:</span>{' '}
144+
<BatteryStatusIndicator device={device} />
145+
</>
146+
)}
147+
<DeviceModel device={device} />
148+
</div>
117149
</div>
118150
</div>
151+
<Alerts alerts={alerts} className='seam-alerts-space-top' />
119152
</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 />
153+
<div className='seam-box'>
154+
<div
155+
className='seam-content seam-access-codes'
156+
onClick={toggleAccessCodesOpen}
157+
>
158+
<span className='seam-value'>
159+
{accessCodeCount} {t.accessCodes}
160+
</span>
161+
<ChevronRightIcon />
162+
</div>
131163
</div>
132-
</div>
133164

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-
)}
165+
<div className='seam-box'>
166+
{device.properties.locked && device.properties.online && (
167+
<div className='seam-content seam-lock-status'>
168+
<div>
169+
<span className='seam-label'>{t.lockStatus}</span>
170+
<span className='seam-value'>{lockStatus}</span>
171+
</div>
172+
<div className='seam-right'>
173+
{!disableLockUnlock &&
174+
device.capabilities_supported.includes('lock') && (
175+
<Button
176+
size='small'
177+
onClick={() => {
178+
toggleLock.mutate(device)
179+
}}
180+
>
181+
{toggleLockLabel}
182+
</Button>
183+
)}
184+
</div>
153185
</div>
154-
</div>
155-
)}
186+
)}
156187

157-
<AccessCodeLength
158-
supportedCodeLengths={
159-
device.properties?.supported_code_lengths ?? []
188+
<AccessCodeLength
189+
supportedCodeLengths={
190+
device.properties?.supported_code_lengths ?? []
191+
}
192+
/>
193+
</div>
194+
<DeviceInfo
195+
device={device}
196+
disableConnectedAccountInformation={
197+
disableConnectedAccountInformation
160198
}
199+
disableResourceIds={disableResourceIds}
161200
/>
162201
</div>
163-
<DeviceInfo
164-
device={device}
165-
disableConnectedAccountInformation={
166-
disableConnectedAccountInformation
167-
}
168-
disableResourceIds={disableResourceIds}
169-
/>
170202
</div>
171-
</div>
203+
</>
172204
)
173205
}
174206

@@ -208,4 +240,6 @@ const t = {
208240
lockStatus: 'Lock status',
209241
status: 'Status',
210242
power: 'Power',
243+
successfullyUpdated: 'Lock status has been successfully updated',
244+
failedToUpdate: 'Failed to update lock status',
211245
}

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { CheckGreenIcon } from 'lib/icons/CheckGreen.js'
55
import { CloseWhiteIcon } from 'lib/icons/CloseWhite.js'
66
import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js'
77

8-
type SnackbarVariant = 'success' | 'error'
8+
export 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)