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}
}