From 4ed34cb242134bd15411e4254d3bcefde53934ff Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:54:01 -0700 Subject: [PATCH] Fix frozen unknown device ID for new accounts (#11449) --- CLAUDE.md | 10 +++++++++- src/analytics/identifiers/device.ts | 21 +++++++++++++++++++++ src/analytics/index.tsx | 5 ++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 066eaad914..fbe87d5287 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,6 +224,7 @@ return conditional styles inline in a style array: `web({cursor: 'pointer'})`, - Prefer prop destructuring via parameters over a const within the component. - Prefer inline types over `Props` types or interfaces. - Set reasonable defaults for optional props. +- Prefer the implicit global `React` for types over `type` imports. ```tsx import {Fragment} from 'react' @@ -232,7 +233,13 @@ import {Trans} from '@lingui/react/macro' import {Text} from '#/components/Typography' -function MyComponent({items = []}: {items?: string[]}) { +function MyComponent({ + items = [], + children, +}: { + items?: string[] + children: React.ReactNode +}) { return ( <> @@ -247,6 +254,7 @@ function MyComponent({items = []}: {items?: string[]}) { {item} ))} + {children} ) diff --git a/src/analytics/identifiers/device.ts b/src/analytics/identifiers/device.ts index ac50143548..3d9254483d 100644 --- a/src/analytics/identifiers/device.ts +++ b/src/analytics/identifiers/device.ts @@ -1,3 +1,4 @@ +import {useSyncExternalStore} from 'react' import uuid from 'react-native-uuid' import AsyncStorage from '@react-native-async-storage/async-storage' @@ -24,3 +25,23 @@ export function getDeviceIdOrThrow() { } return id } + +function subscribeToDeviceId(onChange: () => void) { + const sub = device.addOnValueChangedListener(['deviceId'], onChange) + return () => sub.remove() +} + +/** + * Reads the device ID for use during render. The app awaits + * `getAndMigrateDeviceId` before booting (see `setupDeviceId` in + * `analytics/index.tsx`), so this is normally set on first read. + * + * Subscribing rather than reading storage directly means a late write - a + * caller that mounts before the migration resolves - still propagates, instead + * of leaving consumers pinned to `undefined` for the lifetime of the component. + * `useSyncExternalStore` re-evaluates the snapshot every render, so there's no + * gap between the initial read and the subscription. + */ +export function useDeviceId() { + return useSyncExternalStore(subscribeToDeviceId, getDeviceId) +} diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index 51438c2d7b..07af41473e 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -20,6 +20,7 @@ import { getAndMigrateDeviceId, getDeviceId, getInitialSessionId, + useDeviceId, useSessionId, } from '#/analytics/identifiers' import { @@ -178,6 +179,7 @@ export function AnalyticsContext({ ) } } + const deviceId = useDeviceId() ?? 'unknown' const sessionId = useSessionId() const geolocation = useGeolocationServiceResponse() const parentContext = useContext(Context) @@ -197,6 +199,7 @@ export function AnalyticsContext({ ...metadata, base: { ...parentContext.metadata.base, + deviceId, sessionId, isBetaUser, }, @@ -217,7 +220,7 @@ export function AnalyticsContext({ }, } return context - }, [parentContext, metadata, sessionId, isBetaUser, geolocation]) + }, [parentContext, metadata, deviceId, sessionId, isBetaUser, geolocation]) return {children} }