Skip to content

Commit 55fc5c2

Browse files
authored
Merge branch 'main' into missing-device-details
2 parents a65d661 + 0ee1e3f commit 55fc5c2

File tree

5 files changed

+350
-43
lines changed

5 files changed

+350
-43
lines changed

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

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import { NestedAccessCodeTable } from 'lib/seam/components/AccessCodeTable/Acces
77
import type { NestedSpecificDeviceDetailsProps } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
88
import { DeviceInfo } from 'lib/seam/components/DeviceDetails/DeviceInfo.js'
99
import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js'
10+
import { LockDeviceLockButtons } from 'lib/seam/components/DeviceDetails/LockDeviceLockButtons.js'
1011
import { deviceErrorFilter, deviceWarningFilter } from 'lib/seam/filters.js'
1112
import type { LockDevice } from 'lib/seam/locks/lock-device.js'
12-
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
1313
import { Alerts } from 'lib/ui/Alert/Alerts.js'
14-
import { Button } from 'lib/ui/Button.js'
1514
import { BatteryStatusIndicator } from 'lib/ui/device/BatteryStatusIndicator.js'
1615
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
1716
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
@@ -48,20 +47,6 @@ export function LockDeviceDetails({
4847
const [snackbarVariant, setSnackbarVariant] =
4948
useState<SnackbarVariant>('success')
5049

51-
const toggleLock = useToggleLock({
52-
onSuccess: () => {
53-
setSnackbarVisible(true)
54-
setSnackbarVariant('success')
55-
},
56-
onError: () => {
57-
setSnackbarVisible(true)
58-
setSnackbarVariant('error')
59-
},
60-
})
61-
62-
const lockStatus = device.properties.locked ? t.locked : t.unlocked
63-
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
64-
6550
const accessCodeCount = accessCodes?.length ?? 0
6651

6752
if (accessCodesOpen) {
@@ -161,28 +146,12 @@ export function LockDeviceDetails({
161146
</div>
162147

163148
<div className='seam-box'>
164-
{device.properties.locked && device.properties.online && (
165-
<div className='seam-content seam-lock-status'>
166-
<div>
167-
<span className='seam-label'>{t.lockStatus}</span>
168-
<span className='seam-value'>{lockStatus}</span>
169-
</div>
170-
<div className='seam-right'>
171-
{!disableLockUnlock &&
172-
device.capabilities_supported.includes('lock') && (
173-
<Button
174-
size='small'
175-
onClick={() => {
176-
toggleLock.mutate(device)
177-
}}
178-
>
179-
{toggleLockLabel}
180-
</Button>
181-
)}
182-
</div>
183-
</div>
184-
)}
185-
149+
<LockDeviceLockButtons
150+
setSnackbarVisible={setSnackbarVisible}
151+
setSnackbarVariant={setSnackbarVariant}
152+
device={device}
153+
disableLockUnlock={disableLockUnlock}
154+
/>
186155
<AccessCodeLength
187156
supportedCodeLengths={
188157
device.properties?.supported_code_lengths ?? []
@@ -228,14 +197,9 @@ function AccessCodeLength(props: {
228197

229198
const t = {
230199
device: 'Device',
231-
unlock: 'Unlock',
232-
lock: 'Lock',
233-
locked: 'Locked',
234-
unlocked: 'Unlocked',
235200
accessCodes: 'access codes',
236201
codeLength: 'Code length',
237202
digits: 'digits',
238-
lockStatus: 'Lock status',
239203
status: 'Status',
240204
power: 'Power',
241205
successfullyUpdated: 'Lock status has been successfully updated',
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import type { LockDevice } from 'lib/seam/locks/lock-device.js'
2+
import { useLock } from 'lib/seam/locks/use-lock-door.js'
3+
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
4+
import { useUnlock } from 'lib/seam/locks/use-unlock-door.js'
5+
import { Button } from 'lib/ui/Button.js'
6+
import type { SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'
7+
8+
interface LockDeviceLockButtonsProps {
9+
device: LockDevice
10+
disableLockUnlock: boolean
11+
setSnackbarVisible: (visible: boolean) => void
12+
setSnackbarVariant: (variant: SnackbarVariant) => void
13+
}
14+
15+
export function LockDeviceLockButtons({
16+
device,
17+
setSnackbarVariant,
18+
setSnackbarVisible,
19+
disableLockUnlock,
20+
}: LockDeviceLockButtonsProps): JSX.Element | null {
21+
const lockStatus = device.properties.locked ? t.locked : t.unlocked
22+
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock
23+
24+
const toggleLock = useToggleLock({
25+
onSuccess: () => {
26+
setSnackbarVisible(true)
27+
setSnackbarVariant('success')
28+
},
29+
onError: () => {
30+
setSnackbarVisible(true)
31+
setSnackbarVariant('error')
32+
},
33+
})
34+
35+
const lock = useLock({
36+
onSuccess: () => {
37+
setSnackbarVisible(true)
38+
setSnackbarVariant('success')
39+
},
40+
onError: () => {
41+
setSnackbarVisible(true)
42+
setSnackbarVariant('error')
43+
},
44+
})
45+
46+
const unlock = useUnlock({
47+
onSuccess: () => {
48+
setSnackbarVisible(true)
49+
setSnackbarVariant('success')
50+
},
51+
onError: () => {
52+
setSnackbarVisible(true)
53+
setSnackbarVariant('error')
54+
},
55+
})
56+
57+
if (disableLockUnlock) {
58+
return null
59+
}
60+
61+
if (
62+
device.can_remotely_lock === true &&
63+
device.can_remotely_unlock === true
64+
) {
65+
return (
66+
<div className='seam-content seam-lock-status'>
67+
<div>
68+
<span className='seam-label'>{t.lockStatus}</span>
69+
<span className='seam-value'>{lockStatus}</span>
70+
</div>
71+
<div className='seam-right'>
72+
<Button
73+
size='small'
74+
onClick={() => {
75+
toggleLock.mutate(device)
76+
}}
77+
>
78+
{toggleLockLabel}
79+
</Button>
80+
</div>
81+
</div>
82+
)
83+
}
84+
85+
if (device.can_remotely_lock === true) {
86+
return (
87+
<div className='seam-content seam-lock-status'>
88+
<div>
89+
<span className='seam-label'>{t.lockStatus}</span>
90+
<span className='seam-value'>{lockStatus}</span>
91+
</div>
92+
<div className='seam-right'>
93+
<Button
94+
size='small'
95+
onClick={() => {
96+
lock.mutate(device)
97+
}}
98+
>
99+
{t.lock}
100+
</Button>
101+
</div>
102+
</div>
103+
)
104+
}
105+
106+
if (device.can_remotely_unlock === true) {
107+
return (
108+
<div className='seam-content seam-lock-status'>
109+
<div>
110+
<span className='seam-label'>{t.lockStatus}</span>
111+
<span className='seam-value'>{lockStatus}</span>
112+
</div>
113+
<div className='seam-right'>
114+
<Button
115+
size='small'
116+
onClick={() => {
117+
unlock.mutate(device)
118+
}}
119+
>
120+
{t.unlock}
121+
</Button>
122+
</div>
123+
</div>
124+
)
125+
}
126+
127+
return null
128+
}
129+
130+
const t = {
131+
unlock: 'Unlock',
132+
lock: 'Lock',
133+
locked: 'Locked',
134+
unlocked: 'Unlocked',
135+
lockStatus: 'Lock status',
136+
statusUnknown: 'Unknown',
137+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type {
2+
SeamActionAttemptFailedError,
3+
SeamActionAttemptTimeoutError,
4+
SeamHttpApiError,
5+
} from '@seamapi/http/connect'
6+
import { NullSeamClientError, useSeamClient } from '@seamapi/react-query'
7+
import type { ActionAttempt, Device } from '@seamapi/types/connect'
8+
import {
9+
useMutation,
10+
type UseMutationResult,
11+
useQueryClient,
12+
} from '@tanstack/react-query'
13+
14+
export type UseLockData = undefined
15+
16+
export type UseLockMutationVariables = Pick<Device, 'device_id'> & {
17+
properties: Required<Pick<Device['properties'], 'locked'>>
18+
}
19+
20+
type LockActionAttempt = Extract<ActionAttempt, { action_type: 'LOCK_DOOR' }>
21+
22+
type MutationError =
23+
| SeamHttpApiError
24+
| SeamActionAttemptFailedError<LockActionAttempt>
25+
| SeamActionAttemptTimeoutError<LockActionAttempt>
26+
27+
interface UseLockParams {
28+
onError?: () => void
29+
onSuccess?: () => void
30+
}
31+
32+
export function useLock(
33+
params: UseLockParams = {}
34+
): UseMutationResult<UseLockData, MutationError, UseLockMutationVariables> {
35+
const { client } = useSeamClient()
36+
const queryClient = useQueryClient()
37+
38+
return useMutation<UseLockData, MutationError, UseLockMutationVariables>({
39+
mutationFn: async (variables) => {
40+
const {
41+
device_id: deviceId,
42+
properties: { locked },
43+
} = variables
44+
if (client === null) throw new NullSeamClientError()
45+
if (locked == null) return
46+
await client.locks.lockDoor({ device_id: deviceId })
47+
},
48+
onMutate: (variables) => {
49+
queryClient.setQueryData<Device[]>(['devices', 'list', {}], (devices) => {
50+
if (devices == null) {
51+
return devices
52+
}
53+
54+
return devices.map((device) => {
55+
if (
56+
device.device_id !== variables.device_id ||
57+
device.properties.locked == null
58+
) {
59+
return device
60+
}
61+
62+
return {
63+
...device,
64+
properties: {
65+
...device.properties,
66+
locked: !variables.properties.locked,
67+
},
68+
}
69+
})
70+
})
71+
72+
queryClient.setQueryData<Device>(
73+
['devices', 'get', { device_id: variables.device_id }],
74+
(device) => {
75+
if (device?.properties.locked == null) return device
76+
77+
return {
78+
...device,
79+
properties: {
80+
...device.properties,
81+
locked: !variables.properties.locked,
82+
},
83+
}
84+
}
85+
)
86+
},
87+
onError: async (_error, variables) => {
88+
params.onError?.()
89+
90+
await queryClient.invalidateQueries({
91+
queryKey: ['devices', 'list'],
92+
})
93+
await queryClient.invalidateQueries({
94+
queryKey: ['devices', 'get', { device_id: variables.device_id }],
95+
})
96+
},
97+
onSuccess() {
98+
params.onSuccess?.()
99+
},
100+
})
101+
}

0 commit comments

Comments
 (0)