diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0fd35faf7528..18fb67319176 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -106,7 +106,7 @@ "expo-updates": "~57.0.19", "expo-video": "~57.0.3", "expo-web-browser": "~57.0.2", - "expo-widgets": "~57.0.15", + "expo-widgets": "57.0.15", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.3", diff --git a/apps/mobile/src/widgets/AgentActivity.test.ts b/apps/mobile/src/widgets/AgentActivity.test.ts index dc9cd0e22117..467f290a1575 100644 --- a/apps/mobile/src/widgets/AgentActivity.test.ts +++ b/apps/mobile/src/widgets/AgentActivity.test.ts @@ -10,6 +10,7 @@ vi.mock("@expo/ui/swift-ui", () => ({ })); vi.mock("@expo/ui/swift-ui/modifiers", () => ({ + activityBackgroundTint: (value: unknown) => value, font: (value: unknown) => value, foregroundStyle: (value: unknown) => value, frame: (value: unknown) => value, diff --git a/apps/mobile/src/widgets/AgentActivity.tsx b/apps/mobile/src/widgets/AgentActivity.tsx index 88c85648271f..3e9b417c5aae 100644 --- a/apps/mobile/src/widgets/AgentActivity.tsx +++ b/apps/mobile/src/widgets/AgentActivity.tsx @@ -1,6 +1,7 @@ import { HStack, Image, Spacer, Text, VStack, ZStack } from "@expo/ui/swift-ui"; import type { ComponentProps } from "react"; import { + activityBackgroundTint, font, foregroundStyle, frame, @@ -56,14 +57,13 @@ export function AgentActivity( ): LiveActivityLayout { "widget"; - // Use SwiftUI's semantic label colors rather than fixed hex keyed off the - // device color scheme. A Live Activity banner always renders over a dark - // system material regardless of the device's light/dark setting, so - // scheme-derived dark text read as unreadable dark-on-dark on the lock - // screen. Semantic colors adapt to whatever material the OS places them on: - // the dark LA banner and the (light or dark) home-screen widget alike. - const primaryForeground = "primary"; - const secondaryForeground = "secondary"; + // Hierarchical styles inherit the system's foreground treatment, including + // tinted and vibrant presentations, rather than resolving to a label color. + type Foreground = Parameters[0]; + const primaryForeground = { type: "hierarchical", style: "primary" } as const; + const secondaryForeground = { type: "hierarchical", style: "secondary" } as const; + const monochrome = + environment.widgetRenderingMode === "accented" || environment.widgetRenderingMode === "vibrant"; // Status tints mirror the web sidebar's pills // (apps/web/src/components/Sidebar.logic.ts resolveThreadStatusPill): amber @@ -72,10 +72,13 @@ export function AgentActivity( // Mac notification center) renders it on a light one — so pick the web // palette's light (-600) or dark (-300) variant off the color scheme. const isLightScheme = environment.colorScheme === "light"; - const phaseTint = (phase: AgentActivityPhase | undefined): string => { + const phaseTint = (phase: AgentActivityPhase | undefined): Foreground => { if (environment.isLuminanceReduced) { return secondaryForeground; } + if (monochrome) { + return primaryForeground; + } switch (phase) { case "waiting_for_approval": return isLightScheme ? "#d97706" : "#fcd34d"; // amber-600 / amber-300 @@ -179,7 +182,7 @@ export function AgentActivity( // SF Symbols, like the logo, ignore frame/foregroundStyle applied directly to // the image; size + tint them through a container the resizable symbol fills. - const renderGlyph = (systemName: SFName, size: number, color: string) => ( + const renderGlyph = (systemName: SFName, size: number, color: Foreground) => ( @@ -229,7 +232,7 @@ export function AgentActivity( // frame the resizable image fills and tint it through the container's // foreground style, which the template image inherits. The 3:2 frame matches // the glyph's aspect ratio so it never distorts. - const renderLogo = (height: number, color: string) => ( + const renderLogo = (height: number, color: Foreground) => ( @@ -240,7 +243,12 @@ export function AgentActivity( {/* Logo pinned to the leading edge; the status texts centered across the full width (ZStack so the logo doesn't skew the centering). No footer — diff --git a/patches/expo-widgets@57.0.15.patch b/patches/expo-widgets@57.0.15.patch new file mode 100644 index 000000000000..f50a3fa4f661 --- /dev/null +++ b/patches/expo-widgets@57.0.15.patch @@ -0,0 +1,148 @@ +diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts +index 15e026868625ce20a3e359e5cc8e5c7e937c4650..f5cc83174ede4fb6e01cbce494f61e088eb4eed8 100644 +--- a/build/Widgets.types.d.ts ++++ b/build/Widgets.types.d.ts +@@ -90,6 +90,10 @@ export type LiveActivityEnvironment = { + * The color scheme of the activity's environment. + */ + colorScheme: 'light' | 'dark'; ++ /** The rendering mode supplied by WidgetKit for this presentation. */ ++ widgetRenderingMode?: WidgetRenderingMode; ++ /** Whether the presentation OS supports Liquid Glass. @platform iOS 26+ */ ++ isLiquidGlassAvailable?: boolean; + /** + * Whether the activity is displayed in a context with reduced luminance. + * @platform iOS 16+ +diff --git a/ios/Widgets/Utils.swift b/ios/Widgets/Utils.swift +index 2b4c044b597de7e0fab42dbc47af14d8bb521838..801f0aed65ea2659f221d477dd841787d9146c26 100644 +--- a/ios/Widgets/Utils.swift ++++ b/ios/Widgets/Utils.swift +@@ -91,6 +91,10 @@ func getLiveActivityEnvironment(for environment: EnvironmentValues, in context: + "colorScheme": "\(environment.colorScheme)" + ] + ++ if #available(iOS 26.0, *) { ++ env["isLiquidGlassAvailable"] = true ++ } ++ env["widgetRenderingMode"] = environment.widgetRenderingMode.description + env["isLuminanceReduced"] = environment.isLuminanceReduced + env["isActivityFullscreen"] = environment.isActivityFullscreen + if #available(iOS 16.2, *) { +diff --git a/ios/Widgets/WidgetLiveActivity.swift b/ios/Widgets/WidgetLiveActivity.swift +index 24d4571a52d3ebe9d785b5d049d8e134a149d513..7051c23b17ac91552e5b5c54606c57086c0ebbea 100644 +--- a/ios/Widgets/WidgetLiveActivity.swift ++++ b/ios/Widgets/WidgetLiveActivity.swift +@@ -20,53 +20,41 @@ struct LiveActivityAttributes: ActivityAttributes { + + @available(iOS 16.1, *) + public struct WidgetLiveActivity: Widget { +- @Environment(\.self) var env +- + let widgetContext: AppContext = AppContext() + + public init() {} + + public var body: some WidgetConfiguration { + ActivityConfiguration(for: LiveActivityAttributes.self) { context in +- let nodes = getLiveActivityNodes( +- forName: context.state.name, +- props: context.state.props, +- environment: getLiveActivityEnvironment(for: env, in: context) +- ) + // Only apply widgetURL when the activity has one: a hierarchy with more than one + // widgetURL modifier is undefined behavior, and layouts can set their own through + // the widgetURL modifier from @expo/ui. +- let banner = LiveActivityBannerView(context: context, nodes: nodes) ++ let banner = LiveActivityBannerView(context: context) + if let url = context.attributes.url.flatMap(URL.init(string:)) { + banner.widgetURL(url) + } else { + banner + } + } dynamicIsland: { context in +- let nodes = getLiveActivityNodes( +- forName: context.state.name, +- props: context.state.props, +- environment: getLiveActivityEnvironment(for: env, in: context) +- ) + let island = DynamicIsland { + DynamicIslandExpandedRegion(.center) { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter") ++ LiveActivitySectionView(context: context, sectionName: "expandedCenter") + } + DynamicIslandExpandedRegion(.leading) { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading") ++ LiveActivitySectionView(context: context, sectionName: "expandedLeading") + } + DynamicIslandExpandedRegion(.trailing) { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing") ++ LiveActivitySectionView(context: context, sectionName: "expandedTrailing") + } + DynamicIslandExpandedRegion(.bottom) { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom") ++ LiveActivitySectionView(context: context, sectionName: "expandedBottom") + } + } compactLeading: { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading") ++ LiveActivitySectionView(context: context, sectionName: "compactLeading") + } compactTrailing: { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing") ++ LiveActivitySectionView(context: context, sectionName: "compactTrailing") + } minimal: { +- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal") ++ LiveActivitySectionView(context: context, sectionName: "minimal") + } + if let url = context.attributes.url.flatMap(URL.init(string:)) { + return island.widgetURL(url) +@@ -79,11 +67,16 @@ public struct WidgetLiveActivity: Widget { + + @available(iOS 16.1, *) + private struct LiveActivitySectionView: View { ++ @Environment(\.self) var env + let context: ActivityViewContext +- let nodes: [String: Any] + let sectionName: String + + var body: some View { ++ let nodes = getLiveActivityNodes( ++ forName: context.state.name, ++ props: context.state.props, ++ environment: getLiveActivityEnvironment(for: env, in: context) ++ ) + if let node = nodes[sectionName] as? [String: Any] { + WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node) + } else { +@@ -94,10 +87,17 @@ private struct LiveActivitySectionView: View { + + @available(iOS 16.1, *) + private struct LiveActivityBannerView: View { ++ // Read inside the rendered view: Widget-level values do not include the ++ // presentation's color scheme, rendering mode, or reduced-luminance state. ++ @Environment(\.self) var env + var context: ActivityViewContext +- let nodes: [String: Any] + + var body: some View { ++ let nodes = getLiveActivityNodes( ++ forName: context.state.name, ++ props: context.state.props, ++ environment: getLiveActivityEnvironment(for: env, in: context) ++ ) + if #available(iOS 18.0, *) { + LiveActivityBanner(context: context, nodes: nodes) + } else if let node = nodes["banner"] as? [String: Any] { +diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts +index fee5c7475b52218fa0e192a1d347dd32cb0fe1f8..1d3a097b45d8eb5d13e5cedd26f0414caa83a5d6 100644 +--- a/src/Widgets.types.ts ++++ b/src/Widgets.types.ts +@@ -104,6 +104,10 @@ export type LiveActivityEnvironment = { + * The color scheme of the activity's environment. + */ + colorScheme: 'light' | 'dark'; ++ /** The rendering mode supplied by WidgetKit for this presentation. */ ++ widgetRenderingMode?: WidgetRenderingMode; ++ /** Whether the presentation OS supports Liquid Glass. @platform iOS 26+ */ ++ isLiquidGlassAvailable?: boolean; + /** + * Whether the activity is displayed in a context with reduced luminance. + * @platform iOS 16+ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8dbaf396924..06b0578ae057 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,7 @@ patchedDependencies: effect@4.0.0-rc.112: 8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2 expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45 + expo-widgets@57.0.15: 319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6 react-native-gesture-handler@2.32.0: 96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 @@ -414,8 +415,8 @@ importers: specifier: ~57.0.2 version: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: - specifier: ~57.0.15 - version: 57.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: 57.0.15 + version: 57.0.15(patch_hash=319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -16861,7 +16862,7 @@ snapshots: optionalDependencies: '@babel/runtime': 7.29.7 expo: 57.0.18(fc5a731e35a0144aab60c7305f29cbed) - expo-widgets: 57.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-widgets: 57.0.15(patch_hash=319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -18179,7 +18180,7 @@ snapshots: expo: 57.0.18(fc5a731e35a0144aab60c7305f29cbed) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@57.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-widgets@57.0.15(patch_hash=319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6)(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/plist': 0.8.1 '@expo/ui': 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8a4bf972f82f..e2c683e9bab3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -176,6 +176,7 @@ patchedDependencies: effect@4.0.0-rc.112: patches/effect@4.0.0-rc.112.patch expo-audio@57.0.4: patches/expo-audio@57.0.4.patch expo-sharing@57.0.17: patches/expo-sharing@57.0.17.patch + expo-widgets@57.0.15: patches/expo-widgets@57.0.15.patch react-native-gesture-handler@2.32.0: patches/react-native-gesture-handler@2.32.0.patch react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch