diff --git a/docs/pages/eas/ai/mcp.mdx b/docs/pages/eas/ai/mcp.mdx index e1bd248d71d9d7..a5615d66af354c 100644 --- a/docs/pages/eas/ai/mcp.mdx +++ b/docs/pages/eas/ai/mcp.mdx @@ -86,7 +86,7 @@ Expo MCP Server supports integration with various AI-assisted tools. Use the gen - + After installation, run `/mcp` in your Claude Code session to authenticate. @@ -96,7 +96,7 @@ After installation, run `/mcp` in your Claude Code session to authenticate. Click the following link to install the MCP server for Cursor: - + Install MCP Server - + The above command adds the MCP server to your Codex configuration file and prompts you to authenticate with your Expo account. diff --git a/docs/pages/versions/unversioned/sdk/splash-screen.mdx b/docs/pages/versions/unversioned/sdk/splash-screen.mdx index 30818a98397181..f422ffb13561c6 100644 --- a/docs/pages/versions/unversioned/sdk/splash-screen.mdx +++ b/docs/pages/versions/unversioned/sdk/splash-screen.mdx @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control > From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.** -Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/). +Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen). ## Installation diff --git a/docs/pages/versions/v54.0.0/sdk/splash-screen.mdx b/docs/pages/versions/v54.0.0/sdk/splash-screen.mdx index f515d9f9596e61..21b78f0b45919f 100644 --- a/docs/pages/versions/v54.0.0/sdk/splash-screen.mdx +++ b/docs/pages/versions/v54.0.0/sdk/splash-screen.mdx @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control > From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.** -Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/). +Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen). ## Installation diff --git a/docs/pages/versions/v55.0.0/sdk/splash-screen.mdx b/docs/pages/versions/v55.0.0/sdk/splash-screen.mdx index 30818a98397181..f422ffb13561c6 100644 --- a/docs/pages/versions/v55.0.0/sdk/splash-screen.mdx +++ b/docs/pages/versions/v55.0.0/sdk/splash-screen.mdx @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control > From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.** -Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/). +Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen). ## Installation diff --git a/docs/pages/versions/v56.0.0/sdk/splash-screen.mdx b/docs/pages/versions/v56.0.0/sdk/splash-screen.mdx index 7a4503b5ce1074..8b1e80303fceb9 100644 --- a/docs/pages/versions/v56.0.0/sdk/splash-screen.mdx +++ b/docs/pages/versions/v56.0.0/sdk/splash-screen.mdx @@ -20,7 +20,7 @@ The `SplashScreen` module from the `expo-splash-screen` library provides control > From **SDK 52**, due to changes supporting the latest Android splash screen API, Expo Go and development builds cannot fully replicate the splash screen experience your users will see in your [standalone app](/more/glossary-of-terms/#standalone-app). Expo Go will show your app icon instead of the splash screen, and the splash screen on development builds will not reflect all properties set in the config plugin. **It is highly recommended that you test your splash screen on a release build to ensure it looks as expected.** -Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen), or [quickly generate an icon and splash screen using your browser](https://buildicon.netlify.app/). +Also, see the guide on [creating a splash screen image](/develop/user-interface/splash-screen-and-app-icon/#splash-screen). ## Installation diff --git a/packages/expo-observe/build/integrations/expo-router/init.d.ts.map b/packages/expo-observe/build/integrations/expo-router/init.d.ts.map index fe7361f862cfe5..5c2aa98a63e772 100644 --- a/packages/expo-observe/build/integrations/expo-router/init.d.ts.map +++ b/packages/expo-observe/build/integrations/expo-router/init.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAS1D,eAAO,MAAM,aAAa,eAAoB,CAAC;AAE/C,wBAAgB,qBAAqB,SAGpC;AAED,KAAK,gBAAgB,GAAG,WAAW,CAAC,OAAO,cAAc,CAAC,CAAC,2BAA2B,CAAC,CAAC;AAExF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,gBAAgB,GACjC,MAAM,IAAI,CAiIZ"} \ No newline at end of file +{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/init.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAS1D,eAAO,MAAM,aAAa,eAAoB,CAAC;AAE/C,wBAAgB,qBAAqB,SAGpC;AAED,KAAK,gBAAgB,GAAG,WAAW,CAAC,OAAO,cAAc,CAAC,CAAC,2BAA2B,CAAC,CAAC;AAExF,wBAAgB,aAAa,CAC3B,OAAO,EAAE,wBAAwB,EACjC,gBAAgB,EAAE,gBAAgB,GACjC,MAAM,IAAI,CA2GZ"} \ No newline at end of file diff --git a/packages/expo-observe/build/integrations/expo-router/useObserveForRouter.d.ts.map b/packages/expo-observe/build/integrations/expo-router/useObserveForRouter.d.ts.map index b55e1a23f732ee..59411b1f0d2f7f 100644 --- a/packages/expo-observe/build/integrations/expo-router/useObserveForRouter.d.ts.map +++ b/packages/expo-observe/build/integrations/expo-router/useObserveForRouter.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"useObserveForRouter.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"AAAA,OAAO,UAAqC,MAAM,kBAAkB,CAAC;AAUrE,KAAK,eAAe,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,IAAI,eAAe,GAAG,IAAI,CA8G5D"} \ No newline at end of file +{"version":3,"file":"useObserveForRouter.d.ts","sourceRoot":"","sources":["../../../src/integrations/expo-router/useObserveForRouter.ts"],"names":[],"mappings":"AAAA,OAAO,UAAqC,MAAM,kBAAkB,CAAC;AAUrE,KAAK,eAAe,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,IAAI,eAAe,GAAG,IAAI,CAiH5D"} \ No newline at end of file diff --git a/packages/expo-observe/src/integrations/expo-router/init.ts b/packages/expo-observe/src/integrations/expo-router/init.ts index 2596e075e76173..54ad51e4c691e5 100644 --- a/packages/expo-observe/src/integrations/expo-router/init.ts +++ b/packages/expo-observe/src/integrations/expo-router/init.ts @@ -64,88 +64,66 @@ export function initListeners( const isInitial = !storage.renderedScreensIds.has(e.screenId); storage.renderedScreensIds.add(e.screenId); const name = isInitial ? 'cold_ttr' : 'warm_ttr'; - const mainSessionId = (await AppMetrics.getMainSession())?.id; - if (!mainSessionId) { - return; - } - const routePattern = buildRoutePattern(e.segments); + // Resolve which clock anchors the TTR span. Initial focus diffs against + // app launch; subsequent focuses diff against the last dispatched action. + let dispatchTime: number; + let isAppLaunch: boolean; if (!storage.hasRecordedInitialTtr) { - // Stored in seconds to match the OTel `unit = "s"` convention - const appLaunchTtrSeconds = (now - appLaunchTime) / 1000; storage.hasRecordedInitialTtr = true; - // Seed dispatchTime so a later markInteractive on the initial screen can - // diff against app launch — symmetric with the navigated-focus branch - // below, which seeds dispatchTime from the last pending action. - storage.screenTimes[e.screenId] = { - ...storage.screenTimes[e.screenId], - dispatchTime: appLaunchTime, - isAppLaunch: true, - ...(hasPendingInteractive ? { lastInteractiveCall: now } : {}), - }; - AppMetrics.addCustomMetricToSession({ - sessionId: mainSessionId, - timestamp, - category: 'navigation', - name, - routeName: routePattern, - value: appLaunchTtrSeconds, - params: { isAppLaunch: true, routeParams: e.params, url: e.pathname }, - }); - if (hasPendingInteractive) { - // `markInteractive` ran before this focus, so emit TTI = TTR — the - // screen wasn't interactive any earlier than it was focused. - storage.interactiveScreensIds.add(e.screenId); - await emitTTI({ - sessionId: mainSessionId, - timestamp, - routeName: routePattern, - value: appLaunchTtrSeconds, - isAppLaunch: true, - routeParams: e.params, - url: e.pathname, - }); - } - return; + dispatchTime = appLaunchTime; + isAppLaunch = true; + } else { + const last = storage.pendingActions[storage.pendingActions.length - 1]; + if (!last) return; + dispatchTime = last.dispatchTime; + isAppLaunch = false; + storage.pendingActions.length = 0; } - if (storage.pendingActions.length === 0) return; - - const last = storage.pendingActions[storage.pendingActions.length - 1]; - if (last) { - const dispatchTime = last.dispatchTime; - const ttrSeconds = (now - dispatchTime) / 1000; - storage.screenTimes[e.screenId] = { - ...storage.screenTimes[e.screenId], - dispatchTime, - isAppLaunch: false, - ...(hasPendingInteractive ? { lastInteractiveCall: now } : {}), - }; + // Stored in seconds to match the OTel `unit = "s"` convention + const ttrSeconds = (now - dispatchTime) / 1000; + + // Write all storage updates BEFORE awaiting anything so a concurrent + // markInteractive sees the seeded dispatchTime and emits its own TTI + // instead of racing the deferred-TTI branch below. + storage.screenTimes[e.screenId] = { + ...storage.screenTimes[e.screenId], + dispatchTime, + isAppLaunch, + ...(hasPendingInteractive ? { lastInteractiveCall: now } : {}), + }; + if (hasPendingInteractive) { + // `markInteractive` ran before this focus, so emit TTI = TTR — the + // screen wasn't interactive any earlier than it was focused. + storage.interactiveScreensIds.add(e.screenId); + } - AppMetrics.addCustomMetricToSession({ + // All async work happens after storage is updated. + const mainSessionId = (await AppMetrics.getMainSession())?.id; + if (!mainSessionId) return; + + AppMetrics.addCustomMetricToSession({ + sessionId: mainSessionId, + timestamp, + category: 'navigation', + name, + routeName: routePattern, + value: ttrSeconds, + params: { isAppLaunch, routeParams: e.params, url: e.pathname }, + }); + if (hasPendingInteractive) { + await emitTTI({ sessionId: mainSessionId, timestamp, - category: 'navigation', - name, routeName: routePattern, value: ttrSeconds, - params: { isAppLaunch: false, routeParams: e.params, url: e.pathname }, + isAppLaunch, + routeParams: e.params, + url: e.pathname, }); - if (hasPendingInteractive) { - storage.interactiveScreensIds.add(e.screenId); - await emitTTI({ - sessionId: mainSessionId, - timestamp, - routeName: routePattern, - value: ttrSeconds, - isAppLaunch: false, - routeParams: e.params, - url: e.pathname, - }); - } } - storage.pendingActions.length = 0; }); cleanup.add(unsubscribeFocus); diff --git a/packages/expo-observe/src/integrations/expo-router/useObserveForRouter.ts b/packages/expo-observe/src/integrations/expo-router/useObserveForRouter.ts index eb3c24ca4c69ca..1034e5333d14f3 100644 --- a/packages/expo-observe/src/integrations/expo-router/useObserveForRouter.ts +++ b/packages/expo-observe/src/integrations/expo-router/useObserveForRouter.ts @@ -81,17 +81,20 @@ export function useObserveForRouter(): MarkInteractive | null { return; } + // TTI is recorded once per screen ID for the lifetime of the storage — + // re-focusing the same screen (A → B → A) must not produce a second metric + const wasAlreadyInteractive = storage.interactiveScreensIds.has(screenId); + storage.interactiveScreensIds.add(screenId); + + // All async work happens after storage is updated, so a concurrent + // pageFocused observes our writes before its own awaited writes. AppMetrics.markInteractive({ ...(attributes ?? {}), routeName: routePattern, params: { ...(attributes?.params ?? {}), url: pathname }, }); - // TTI is recorded once per screen ID for the lifetime of the storage — - // re-focusing the same screen (A → B → A) must not produce a second metric - if (storage.interactiveScreensIds.has(screenId)) return; - storage.interactiveScreensIds.add(screenId); - + if (wasAlreadyInteractive) return; if (!currentScreenData?.dispatchTime) { // `pageFocused` hasn't recorded the dispatch yet. The focus listener // will see `lastInteractiveCall` set with no `dispatchTime` and emit diff --git a/packages/expo-ui/CHANGELOG.md b/packages/expo-ui/CHANGELOG.md index e27eb0f0c1ce5f..cf2641a050a576 100644 --- a/packages/expo-ui/CHANGELOG.md +++ b/packages/expo-ui/CHANGELOG.md @@ -8,6 +8,7 @@ - [jetpack-compose] Added `NavigationBar` and `NavigationBarItem` components. - [iOS] Added support for custom SF Symbols in the SwiftUI `Image` component. ([#46183](https://github.com/expo/expo/pull/46183) by [@cinques](https://github.com/cinques)) +- [iOS] Added `presentationBackground` SwiftUI modifier and applied it in `community/bottom-sheet`. ([#46285](https://github.com/expo/expo/pull/46285) by [@duyanhv](https://github.com/duyanhv)) - [swift-ui] Added `` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo)) - [universal] Added `` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo))