feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
124
apps/mobile-rn/src/lib/device-context.tsx
Normal file
124
apps/mobile-rn/src/lib/device-context.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { AppState, type AppStateStatus } from 'react-native'
|
||||
import { useAuth } from './auth-context'
|
||||
import { getMobileRuntimeConfig } from './native-config'
|
||||
import { supabase } from './supabase'
|
||||
import {
|
||||
registerCurrentDevice,
|
||||
type RegisteredDevice,
|
||||
} from '../features/devices/device-service'
|
||||
|
||||
interface DeviceContextValue {
|
||||
currentDevice: RegisteredDevice | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refreshCurrentDevice: () => Promise<void>
|
||||
}
|
||||
|
||||
const DeviceContext = createContext<DeviceContextValue | null>(null)
|
||||
const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000
|
||||
|
||||
export function DeviceProvider({ children }: { children: ReactNode }): React.ReactElement {
|
||||
const { user, purgeLocalSession } = useAuth()
|
||||
const [currentDevice, setCurrentDevice] = useState<RegisteredDevice | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const generationRef = useRef(0)
|
||||
const lastHeartbeatRef = useRef(0)
|
||||
const revocationHandledRef = useRef(false)
|
||||
|
||||
const handleRevocation = useCallback(async (): Promise<void> => {
|
||||
if (revocationHandledRef.current) return
|
||||
revocationHandledRef.current = true
|
||||
await purgeLocalSession()
|
||||
}, [purgeLocalSession])
|
||||
|
||||
const refreshCurrentDevice = useCallback(async (): Promise<void> => {
|
||||
const userId = user?.id
|
||||
const generation = ++generationRef.current
|
||||
if (!userId) {
|
||||
setCurrentDevice(null)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const device = await registerCurrentDevice(userId, getMobileRuntimeConfig())
|
||||
if (generation !== generationRef.current) return
|
||||
setCurrentDevice(device)
|
||||
setError(null)
|
||||
lastHeartbeatRef.current = Date.now()
|
||||
if (device.revokedAt !== null) await handleRevocation()
|
||||
} catch {
|
||||
if (generation !== generationRef.current) return
|
||||
setError('device_registration_failed')
|
||||
} finally {
|
||||
if (generation === generationRef.current) setLoading(false)
|
||||
}
|
||||
}, [handleRevocation, user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
revocationHandledRef.current = false
|
||||
void refreshCurrentDevice()
|
||||
return () => { generationRef.current += 1 }
|
||||
}, [refreshCurrentDevice, user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id || !currentDevice?.id) return undefined
|
||||
const channel = supabase
|
||||
.channel(`mobile-current-device:${currentDevice.id}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'devices',
|
||||
filter: `id=eq.${currentDevice.id}`,
|
||||
},
|
||||
(payload) => {
|
||||
const revokedAt = (payload.new as { revoked_at?: unknown }).revoked_at
|
||||
if (typeof revokedAt === 'string' && Number.isFinite(Date.parse(revokedAt))) {
|
||||
void handleRevocation().catch(() => undefined)
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
return () => { void supabase.removeChannel(channel) }
|
||||
}, [currentDevice?.id, handleRevocation, user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', (state: AppStateStatus) => {
|
||||
if (
|
||||
state === 'active'
|
||||
&& user?.id
|
||||
&& Date.now() - lastHeartbeatRef.current >= HEARTBEAT_INTERVAL_MS
|
||||
) void refreshCurrentDevice()
|
||||
})
|
||||
return () => subscription.remove()
|
||||
}, [refreshCurrentDevice, user?.id])
|
||||
|
||||
const value = useMemo<DeviceContextValue>(() => ({
|
||||
currentDevice,
|
||||
loading,
|
||||
error,
|
||||
refreshCurrentDevice,
|
||||
}), [currentDevice, error, loading, refreshCurrentDevice])
|
||||
|
||||
return <DeviceContext.Provider value={value}>{children}</DeviceContext.Provider>
|
||||
}
|
||||
|
||||
export function useDevice(): DeviceContextValue {
|
||||
const value = useContext(DeviceContext)
|
||||
if (!value) throw new Error('useDevice must be used inside DeviceProvider')
|
||||
return value
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue