| 1 | diff --git a/build/Widgets.types.d.ts b/build/Widgets.types.d.ts |
| 2 | index c9f4dfe1f10a3a65ab72bd7e639f6f6d148e5f95..dc0792e1cd7b6762394de5e3fefa0863a253f70c 100644 |
| 3 | --- a/build/Widgets.types.d.ts |
| 4 | +++ b/build/Widgets.types.d.ts |
| 5 | @@ -93,6 +93,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+ |
| 16 | diff --git a/ios/LiveActivityFactory.swift b/ios/LiveActivityFactory.swift |
| 17 | index 29e4f1566d188665e301d32bc1ad85640f83e68c..7143f9a1c06245d16649b981d7f5cde1d8868217 100644 |
| 18 | --- a/ios/LiveActivityFactory.swift |
| 19 | +++ b/ios/LiveActivityFactory.swift |
| 20 | @@ -19,6 +19,14 @@ final class LiveActivityFactory: SharedObject { |
| 21 | throw LiveActivitiesNotSupportedException() |
| 22 | } |
| 23 | |
| 24 | + // iOS ends an activity after 8 hours but leaves it frozen on the Lock Screen for up to |
| 25 | + // 4 more. getInstances() can't see it, so a caller starting a replacement would leave |
| 26 | + // both cards visible. Dismiss the ended ones now. |
| 27 | + for activity in Activity<LiveActivityAttributes>.activities |
| 28 | + where activity.content.state.name == name && activity.activityState == .ended { |
| 29 | + Task { await activity.end(nil, dismissalPolicy: .immediate) } |
| 30 | + } |
| 31 | + |
| 32 | do { |
| 33 | let initialState = LiveActivityAttributes.ContentState(name: name, props: props) |
| 34 | let activity = try Activity.request( |
| 35 | diff --git a/ios/Widgets/Utils.swift b/ios/Widgets/Utils.swift |
| 36 | index 76b2199c2d3f879463fed5d2bd49038b0428a2e7..6cb52c3de4ad9696bc0c51ad62401cce96f405e7 100644 |
| 37 | --- a/ios/Widgets/Utils.swift |
| 38 | +++ b/ios/Widgets/Utils.swift |
| 39 | @@ -125,6 +125,10 @@ func getLiveActivityEnvironment(for environment: EnvironmentValues, in context: |
| 40 | "isStale": context.isStale |
| 41 | ] |
| 42 | |
| 43 | + if #available(iOS 26.0, *) { |
| 44 | + env["isLiquidGlassAvailable"] = true |
| 45 | + } |
| 46 | + env["widgetRenderingMode"] = environment.widgetRenderingMode.description |
| 47 | if #available(iOS 18.0, *) { |
| 48 | env["isActivityUpdateReduced"] = environment.isActivityUpdateReduced |
| 49 | env["activityFamily"] = "\(environment.activityFamily)" |
| 50 | diff --git a/ios/Widgets/WidgetLiveActivity.swift b/ios/Widgets/WidgetLiveActivity.swift |
| 51 | index 4d135c4e4eb1f846315654ed9d835a935d120d42..dae47445b27516fa2464a25b68bddc8f6dec3e1e 100644 |
| 52 | --- a/ios/Widgets/WidgetLiveActivity.swift |
| 53 | +++ b/ios/Widgets/WidgetLiveActivity.swift |
| 54 | @@ -19,53 +19,41 @@ struct LiveActivityAttributes: ActivityAttributes { |
| 55 | } |
| 56 | |
| 57 | public struct WidgetLiveActivity: Widget { |
| 58 | - @Environment(\.self) var env |
| 59 | - |
| 60 | let widgetContext: AppContext = AppContext() |
| 61 | |
| 62 | public init() {} |
| 63 | |
| 64 | public var body: some WidgetConfiguration { |
| 65 | ActivityConfiguration(for: LiveActivityAttributes.self) { context in |
| 66 | - let nodes = getLiveActivityNodes( |
| 67 | - forName: context.state.name, |
| 68 | - props: context.state.props, |
| 69 | - environment: getLiveActivityEnvironment(for: env, in: context) |
| 70 | - ) |
| 71 | // Only apply widgetURL when the activity has one: a hierarchy with more than one |
| 72 | // widgetURL modifier is undefined behavior, and layouts can set their own through |
| 73 | // the widgetURL modifier from @expo/ui. |
| 74 | - let banner = LiveActivityBannerView(context: context, nodes: nodes) |
| 75 | + let banner = LiveActivityBannerView(context: context) |
| 76 | if let url = context.attributes.url.flatMap(URL.init(string:)) { |
| 77 | banner.widgetURL(url) |
| 78 | } else { |
| 79 | banner |
| 80 | } |
| 81 | } dynamicIsland: { context in |
| 82 | - let nodes = getLiveActivityNodes( |
| 83 | - forName: context.state.name, |
| 84 | - props: context.state.props, |
| 85 | - environment: getLiveActivityEnvironment(for: env, in: context) |
| 86 | - ) |
| 87 | let island = DynamicIsland { |
| 88 | DynamicIslandExpandedRegion(.center) { |
| 89 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter") |
| 90 | + LiveActivitySectionView(context: context, sectionName: "expandedCenter") |
| 91 | } |
| 92 | DynamicIslandExpandedRegion(.leading) { |
| 93 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading") |
| 94 | + LiveActivitySectionView(context: context, sectionName: "expandedLeading") |
| 95 | } |
| 96 | DynamicIslandExpandedRegion(.trailing) { |
| 97 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing") |
| 98 | + LiveActivitySectionView(context: context, sectionName: "expandedTrailing") |
| 99 | } |
| 100 | DynamicIslandExpandedRegion(.bottom) { |
| 101 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom") |
| 102 | + LiveActivitySectionView(context: context, sectionName: "expandedBottom") |
| 103 | } |
| 104 | } compactLeading: { |
| 105 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading") |
| 106 | + LiveActivitySectionView(context: context, sectionName: "compactLeading") |
| 107 | } compactTrailing: { |
| 108 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing") |
| 109 | + LiveActivitySectionView(context: context, sectionName: "compactTrailing") |
| 110 | } minimal: { |
| 111 | - LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal") |
| 112 | + LiveActivitySectionView(context: context, sectionName: "minimal") |
| 113 | } |
| 114 | if let url = context.attributes.url.flatMap(URL.init(string:)) { |
| 115 | return island.widgetURL(url) |
| 116 | @@ -77,11 +65,16 @@ public struct WidgetLiveActivity: Widget { |
| 117 | } |
| 118 | |
| 119 | private struct LiveActivitySectionView: View { |
| 120 | + @Environment(\.self) var env |
| 121 | let context: ActivityViewContext<LiveActivityAttributes> |
| 122 | - let nodes: [String: Any] |
| 123 | let sectionName: String |
| 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 let node = nodes[sectionName] as? [String: Any] { |
| 132 | WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node) |
| 133 | } else { |
| 134 | @@ -91,10 +84,17 @@ private struct LiveActivitySectionView: View { |
| 135 | } |
| 136 | |
| 137 | private struct LiveActivityBannerView: View { |
| 138 | + // Read inside the rendered view: Widget-level values do not include the |
| 139 | + // presentation's color scheme, rendering mode, or reduced-luminance state. |
| 140 | + @Environment(\.self) var env |
| 141 | var context: ActivityViewContext<LiveActivityAttributes> |
| 142 | - let nodes: [String: Any] |
| 143 | |
| 144 | var body: some View { |
| 145 | + let nodes = getLiveActivityNodes( |
| 146 | + forName: context.state.name, |
| 147 | + props: context.state.props, |
| 148 | + environment: getLiveActivityEnvironment(for: env, in: context) |
| 149 | + ) |
| 150 | if #available(iOS 18.0, *) { |
| 151 | LiveActivityBanner(context: context, nodes: nodes) |
| 152 | } else if let node = nodes["banner"] as? [String: Any] { |
| 153 | diff --git a/scripts/build-layout-registry.mjs b/scripts/build-layout-registry.mjs |
| 154 | index d52a60e342c8e0a8a1d78f7174da5e60e6bd1c53..fa9bd77c5c04d459038a5b4a00e1391780b406fa 100644 |
| 155 | --- a/scripts/build-layout-registry.mjs |
| 156 | +++ b/scripts/build-layout-registry.mjs |
| 157 | @@ -154,6 +154,8 @@ function createLayoutRegistry(expectedWidgets, capturedLayouts) { |
| 158 | return registry; |
| 159 | } |
| 160 | |
| 161 | -if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { |
| 162 | +// Compare real paths: pnpm exposes the package through a symlink, so the |
| 163 | +// invoked path and import.meta.url differ while naming the same file. |
| 164 | +if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) { |
| 165 | await main(); |
| 166 | } |
| 167 | diff --git a/src/Widgets.types.ts b/src/Widgets.types.ts |
| 168 | index 07706139379384dae27befef4e3e5ae09d04a0db..924fa0029243664e369ce08c81e504b6300bfc7f 100644 |
| 169 | --- a/src/Widgets.types.ts |
| 170 | +++ b/src/Widgets.types.ts |
| 171 | @@ -107,6 +107,10 @@ export type LiveActivityEnvironment = { |
| 172 | * The color scheme of the activity's environment. |
| 173 | */ |
| 174 | colorScheme: 'light' | 'dark'; |
| 175 | + /** The rendering mode supplied by WidgetKit for this presentation. */ |
| 176 | + widgetRenderingMode?: WidgetRenderingMode; |
| 177 | + /** Whether the presentation OS supports Liquid Glass. @platform iOS 26+ */ |
| 178 | + isLiquidGlassAvailable?: boolean; |
| 179 | /** |
| 180 | * Whether the activity is displayed in a context with reduced luminance. |
| 181 | * @platform iOS 16+ |