authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-13 16:22:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-13 16:22:39-07:00
log9086a1f7174c9a66c29c96bf0e3a2ddc33c5a507
tree1a3fb231809aff110e66788bf2363fc8a6147476
parent7b61099886f3aac139ad43fb73873c73ba7baf4f
signature Signed by PGP key B5690EEEBB952194

fix(mobile): adopt system glass for Live Activities (#11604)


6 files changed, 176 insertions(+), 17 deletions(-)

apps/mobile/package.json+1-1
......@@ -106,7 +106,7 @@
106106 "expo-updates": "~57.0.19",
107107 "expo-video": "~57.0.3",
108108 "expo-web-browser": "~57.0.2",
109 "expo-widgets": "~57.0.15",
109 "expo-widgets": "57.0.15",
110110 "react": "19.2.3",
111111 "react-dom": "19.2.3",
112112 "react-native": "0.86.3",
apps/mobile/src/widgets/AgentActivity.test.ts+1
......@@ -10,6 +10,7 @@ vi.mock("@expo/ui/swift-ui", () => ({
1010}));
1111
1212vi.mock("@expo/ui/swift-ui/modifiers", () => ({
13 activityBackgroundTint: (value: unknown) => value,
1314 font: (value: unknown) => value,
1415 foregroundStyle: (value: unknown) => value,
1516 frame: (value: unknown) => value,
apps/mobile/src/widgets/AgentActivity.tsx+20-12
......@@ -1,6 +1,7 @@
11import { HStack, Image, Spacer, Text, VStack, ZStack } from "@expo/ui/swift-ui";
22import type { ComponentProps } from "react";
33import {
4 activityBackgroundTint,
45 font,
56 foregroundStyle,
67 frame,
......@@ -56,14 +57,13 @@ export function AgentActivity(
5657): LiveActivityLayout {
5758 "widget";
5859
59 // Use SwiftUI's semantic label colors rather than fixed hex keyed off the
60 // device color scheme. A Live Activity banner always renders over a dark
61 // system material regardless of the device's light/dark setting, so
62 // scheme-derived dark text read as unreadable dark-on-dark on the lock
63 // screen. Semantic colors adapt to whatever material the OS places them on:
64 // the dark LA banner and the (light or dark) home-screen widget alike.
65 const primaryForeground = "primary";
66 const secondaryForeground = "secondary";
60 // Hierarchical styles inherit the system's foreground treatment, including
61 // tinted and vibrant presentations, rather than resolving to a label color.
62 type Foreground = Parameters<typeof foregroundStyle>[0];
63 const primaryForeground = { type: "hierarchical", style: "primary" } as const;
64 const secondaryForeground = { type: "hierarchical", style: "secondary" } as const;
65 const monochrome =
66 environment.widgetRenderingMode === "accented" || environment.widgetRenderingMode === "vibrant";
6767
6868 // Status tints mirror the web sidebar's pills
6969 // (apps/web/src/components/Sidebar.logic.ts resolveThreadStatusPill): amber
......@@ -72,10 +72,13 @@ export function AgentActivity(
7272 // Mac notification center) renders it on a light one — so pick the web
7373 // palette's light (-600) or dark (-300) variant off the color scheme.
7474 const isLightScheme = environment.colorScheme === "light";
75 const phaseTint = (phase: AgentActivityPhase | undefined): string => {
75 const phaseTint = (phase: AgentActivityPhase | undefined): Foreground => {
7676 if (environment.isLuminanceReduced) {
7777 return secondaryForeground;
7878 }
79 if (monochrome) {
80 return primaryForeground;
81 }
7982 switch (phase) {
8083 case "waiting_for_approval":
8184 return isLightScheme ? "#d97706" : "#fcd34d"; // amber-600 / amber-300
......@@ -179,7 +182,7 @@ export function AgentActivity(
179182
180183 // SF Symbols, like the logo, ignore frame/foregroundStyle applied directly to
181184 // the image; size + tint them through a container the resizable symbol fills.
182 const renderGlyph = (systemName: SFName, size: number, color: string) => (
185 const renderGlyph = (systemName: SFName, size: number, color: Foreground) => (
183186 <HStack modifiers={[frame({ width: size, height: size }), foregroundStyle(color)]}>
184187 <Image systemName={systemName} modifiers={[resizable()]} />
185188 </HStack>
......@@ -229,7 +232,7 @@ export function AgentActivity(
229232 // frame the resizable image fills and tint it through the container's
230233 // foreground style, which the template image inherits. The 3:2 frame matches
231234 // the glyph's aspect ratio so it never distorts.
232 const renderLogo = (height: number, color: string) => (
235 const renderLogo = (height: number, color: Foreground) => (
233236 <HStack modifiers={[frame({ width: height * 1.5, height }), foregroundStyle(color)]}>
234237 <Image assetName="T3Mark" modifiers={[resizable()]} />
235238 </HStack>
......@@ -240,7 +243,12 @@ export function AgentActivity(
240243 <VStack
241244 alignment="leading"
242245 spacing={6}
243 modifiers={deepLink ? [padding({ all: 14 }), widgetURL(deepLink)] : [padding({ all: 14 })]}
246 modifiers={[
247 padding({ all: 14 }),
248 // A clear tint reveals iOS 26's glass material; older hosts keep the standard surface.
249 activityBackgroundTint(environment.isLiquidGlassAvailable ? "clear" : null),
250 ...(deepLink ? [widgetURL(deepLink)] : []),
251 ]}
244252 >
245253 {/* Logo pinned to the leading edge; the status texts centered across the
246254 full width (ZStack so the logo doesn't skew the centering). No footer —
patches/expo-widgets@57.0.15.patch created+148
......@@ -0,0 +1,148 @@
1diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts
2index 15e026868625ce20a3e359e5cc8e5c7e937c4650..f5cc83174ede4fb6e01cbce494f61e088eb4eed8 100644
3--- a/build/Widgets.types.d.ts
4+++ b/build/Widgets.types.d.ts
5@@ -90,6 +90,10 @@ export type LiveActivityEnvironment = {
6 * The color scheme of the activity's environment.
7 */
8 colorScheme: 'light' | 'dark';
9+ /** The rendering mode supplied by WidgetKit for this presentation. */
10+ widgetRenderingMode?: WidgetRenderingMode;
11+ /** Whether the presentation OS supports Liquid Glass. @platform iOS 26+ */
12+ isLiquidGlassAvailable?: boolean;
13 /**
14 * Whether the activity is displayed in a context with reduced luminance.
15 * @platform iOS 16+
16diff --git a/ios/Widgets/Utils.swift b/ios/Widgets/Utils.swift
17index 2b4c044b597de7e0fab42dbc47af14d8bb521838..801f0aed65ea2659f221d477dd841787d9146c26 100644
18--- a/ios/Widgets/Utils.swift
19+++ b/ios/Widgets/Utils.swift
20@@ -91,6 +91,10 @@ func getLiveActivityEnvironment(for environment: EnvironmentValues, in context:
21 "colorScheme": "\(environment.colorScheme)"
22 ]
23
24+ if #available(iOS 26.0, *) {
25+ env["isLiquidGlassAvailable"] = true
26+ }
27+ env["widgetRenderingMode"] = environment.widgetRenderingMode.description
28 env["isLuminanceReduced"] = environment.isLuminanceReduced
29 env["isActivityFullscreen"] = environment.isActivityFullscreen
30 if #available(iOS 16.2, *) {
31diff --git a/ios/Widgets/WidgetLiveActivity.swift b/ios/Widgets/WidgetLiveActivity.swift
32index 24d4571a52d3ebe9d785b5d049d8e134a149d513..7051c23b17ac91552e5b5c54606c57086c0ebbea 100644
33--- a/ios/Widgets/WidgetLiveActivity.swift
34+++ b/ios/Widgets/WidgetLiveActivity.swift
35@@ -20,53 +20,41 @@ struct LiveActivityAttributes: ActivityAttributes {
36
37 @available(iOS 16.1, *)
38 public struct WidgetLiveActivity: Widget {
39- @Environment(\.self) var env
40-
41 let widgetContext: AppContext = AppContext()
42
43 public init() {}
44
45 public var body: some WidgetConfiguration {
46 ActivityConfiguration(for: LiveActivityAttributes.self) { context in
47- let nodes = getLiveActivityNodes(
48- forName: context.state.name,
49- props: context.state.props,
50- environment: getLiveActivityEnvironment(for: env, in: context)
51- )
52 // Only apply widgetURL when the activity has one: a hierarchy with more than one
53 // widgetURL modifier is undefined behavior, and layouts can set their own through
54 // the widgetURL modifier from @expo/ui.
55- let banner = LiveActivityBannerView(context: context, nodes: nodes)
56+ let banner = LiveActivityBannerView(context: context)
57 if let url = context.attributes.url.flatMap(URL.init(string:)) {
58 banner.widgetURL(url)
59 } else {
60 banner
61 }
62 } dynamicIsland: { context in
63- let nodes = getLiveActivityNodes(
64- forName: context.state.name,
65- props: context.state.props,
66- environment: getLiveActivityEnvironment(for: env, in: context)
67- )
68 let island = DynamicIsland {
69 DynamicIslandExpandedRegion(.center) {
70- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter")
71+ LiveActivitySectionView(context: context, sectionName: "expandedCenter")
72 }
73 DynamicIslandExpandedRegion(.leading) {
74- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading")
75+ LiveActivitySectionView(context: context, sectionName: "expandedLeading")
76 }
77 DynamicIslandExpandedRegion(.trailing) {
78- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing")
79+ LiveActivitySectionView(context: context, sectionName: "expandedTrailing")
80 }
81 DynamicIslandExpandedRegion(.bottom) {
82- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom")
83+ LiveActivitySectionView(context: context, sectionName: "expandedBottom")
84 }
85 } compactLeading: {
86- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading")
87+ LiveActivitySectionView(context: context, sectionName: "compactLeading")
88 } compactTrailing: {
89- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing")
90+ LiveActivitySectionView(context: context, sectionName: "compactTrailing")
91 } minimal: {
92- LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal")
93+ LiveActivitySectionView(context: context, sectionName: "minimal")
94 }
95 if let url = context.attributes.url.flatMap(URL.init(string:)) {
96 return island.widgetURL(url)
97@@ -79,11 +67,16 @@ public struct WidgetLiveActivity: Widget {
98
99 @available(iOS 16.1, *)
100 private struct LiveActivitySectionView: View {
101+ @Environment(\.self) var env
102 let context: ActivityViewContext<LiveActivityAttributes>
103- let nodes: [String: Any]
104 let sectionName: String
105
106 var body: some View {
107+ let nodes = getLiveActivityNodes(
108+ forName: context.state.name,
109+ props: context.state.props,
110+ environment: getLiveActivityEnvironment(for: env, in: context)
111+ )
112 if let node = nodes[sectionName] as? [String: Any] {
113 WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
114 } else {
115@@ -94,10 +87,17 @@ private struct LiveActivitySectionView: View {
116
117 @available(iOS 16.1, *)
118 private struct LiveActivityBannerView: View {
119+ // Read inside the rendered view: Widget-level values do not include the
120+ // presentation's color scheme, rendering mode, or reduced-luminance state.
121+ @Environment(\.self) var env
122 var context: ActivityViewContext<LiveActivityAttributes>
123- let nodes: [String: Any]
124
125 var body: some View {
126+ let nodes = getLiveActivityNodes(
127+ forName: context.state.name,
128+ props: context.state.props,
129+ environment: getLiveActivityEnvironment(for: env, in: context)
130+ )
131 if #available(iOS 18.0, *) {
132 LiveActivityBanner(context: context, nodes: nodes)
133 } else if let node = nodes["banner"] as? [String: Any] {
134diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts
135index fee5c7475b52218fa0e192a1d347dd32cb0fe1f8..1d3a097b45d8eb5d13e5cedd26f0414caa83a5d6 100644
136--- a/src/Widgets.types.ts
137+++ b/src/Widgets.types.ts
138@@ -104,6 +104,10 @@ export type LiveActivityEnvironment = {
139 * The color scheme of the activity's environment.
140 */
141 colorScheme: 'light' | 'dark';
142+ /** The rendering mode supplied by WidgetKit for this presentation. */
143+ widgetRenderingMode?: WidgetRenderingMode;
144+ /** Whether the presentation OS supports Liquid Glass. @platform iOS 26+ */
145+ isLiquidGlassAvailable?: boolean;
146 /**
147 * Whether the activity is displayed in a context with reduced luminance.
148 * @platform iOS 16+
pnpm-lock.yaml+5-4
......@@ -100,6 +100,7 @@ patchedDependencies:
100100 effect@4.0.0-rc.112: 8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2
101101 expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a
102102 expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45
103 expo-widgets@57.0.15: 319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6
103104 react-native-gesture-handler@2.32.0: 96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398
104105 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787
105106 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675
......@@ -414,8 +415,8 @@ importers:
414415 specifier: ~57.0.2
415416 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))
416417 expo-widgets:
417 specifier: ~57.0.15
418 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)
418 specifier: 57.0.15
419 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)
419420 react:
420421 specifier: 19.2.3
421422 version: 19.2.3
......@@ -16861,7 +16862,7 @@ snapshots:
1686116862 optionalDependencies:
1686216863 '@babel/runtime': 7.29.7
1686316864 expo: 57.0.18(fc5a731e35a0144aab60c7305f29cbed)
16864 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)
16865 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)
1686516866 transitivePeerDependencies:
1686616867 - '@babel/core'
1686716868 - supports-color
......@@ -18179,7 +18180,7 @@ snapshots:
1817918180 expo: 57.0.18(fc5a731e35a0144aab60c7305f29cbed)
1818018181 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)
1818118182
18182 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):
18183 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):
1818318184 dependencies:
1818418185 '@expo/plist': 0.8.1
1818518186 '@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)
pnpm-workspace.yaml+1
......@@ -176,6 +176,7 @@ patchedDependencies:
176176 effect@4.0.0-rc.112: patches/effect@4.0.0-rc.112.patch
177177 expo-audio@57.0.4: patches/expo-audio@57.0.4.patch
178178 expo-sharing@57.0.17: patches/expo-sharing@57.0.17.patch
179 expo-widgets@57.0.15: patches/expo-widgets@57.0.15.patch
179180 react-native-gesture-handler@2.32.0: patches/react-native-gesture-handler@2.32.0.patch
180181 react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch
181182 react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch