@use-voltra/ios-client 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commonjs/dynamic-widget/api.js +28 -1
- package/build/commonjs/dynamic-widget/api.js.map +1 -1
- package/build/commonjs/index.js +7 -0
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/module/dynamic-widget/api.js +26 -0
- package/build/module/dynamic-widget/api.js.map +1 -1
- package/build/module/index.js +1 -1
- package/build/module/index.js.map +1 -1
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/commonjs/index.d.ts +1 -1
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/module/index.d.ts +1 -1
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
- package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
- package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
- package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/cjs/validation.js +1 -0
- package/expo-plugin/build/cjs/validation.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
- package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
- package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
- package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/esm/validation.js +2 -1
- package/expo-plugin/build/esm/validation.js.map +1 -1
- package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
- package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/types.d.ts +13 -4
- package/expo-plugin/build/types/types.d.ts.map +1 -1
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
- package/expo-plugin/build/types/validation.d.ts.map +1 -1
- package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
- package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
- package/expo-plugin/src/ios-widget/files/index.ts +14 -1
- package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
- package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
- package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
- package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
- package/expo-plugin/src/types.ts +13 -3
- package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
- package/expo-plugin/src/validation.ts +2 -0
- package/ios/Package.swift +8 -1
- package/ios/VoltraWidget.podspec +1 -1
- package/ios/app/NativeVoltra.mm +15 -2
- package/ios/app/VoltraModule.swift +11 -0
- package/ios/app/VoltraModuleImpl.swift +14 -0
- package/ios/app/VoltraViewComponentView.mm +2 -2
- package/ios/app/VoltraViewRoot.swift +6 -12
- package/ios/shared/ShortNames.swift +1 -0
- package/ios/shared/VoltraConstants.swift +4 -0
- package/ios/shared/VoltraElement.swift +43 -2
- package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
- package/ios/shared/VoltraModifierDescriptor.swift +33 -0
- package/ios/shared/VoltraWidgetDefaults.swift +24 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
- package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
- package/ios/target/VoltraHomeWidget.swift +10 -15
- package/ios/target/VoltraWidget.swift +26 -18
- package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
- package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
- package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
- package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
- package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
- package/ios/ui/Style/TextStyle.swift +4 -2
- package/ios/ui/Style/View+applyStyle.swift +1 -1
- package/ios/ui/Views/VoltraText.swift +4 -8
- package/package.json +4 -4
- package/src/dynamic-widget/api.ts +28 -0
- package/src/index.ts +6 -1
- package/src/native/NativeVoltra.ts +2 -0
|
@@ -21,7 +21,10 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
21
21
|
definitionId: String,
|
|
22
22
|
context: ActivityViewContext<Attributes>
|
|
23
23
|
) -> DynamicIsland {
|
|
24
|
-
|
|
24
|
+
// Set once on the Dynamic Island, which makes it the default for every region; the regions
|
|
25
|
+
// only drop the tree's own widgetURL so the configured link takes precedence.
|
|
26
|
+
let deepLinkURL = context.attributes.deepLinkUrl.flatMap(VoltraDeepLinkResolver.resolveUrl)
|
|
27
|
+
let dynamicIsland = DynamicIsland {
|
|
25
28
|
DynamicIslandExpandedRegion(.leading) {
|
|
26
29
|
VoltraDynamicLiveActivityDynamicIslandRegionView(
|
|
27
30
|
definitionId: definitionId,
|
|
@@ -73,6 +76,7 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
73
76
|
// DynamicIsland itself is not a View and cannot read SwiftUI Environment.
|
|
74
77
|
// Do not execute a definition against fabricated values solely to obtain a
|
|
75
78
|
// keyline tint; its region views below read the actual environment.
|
|
79
|
+
return deepLinkURL.map { dynamicIsland.widgetURL($0) } ?? dynamicIsland
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
fileprivate static func resolve<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
@@ -81,6 +85,7 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
81
85
|
activityFamily: String?,
|
|
82
86
|
colorScheme: ColorScheme? = nil,
|
|
83
87
|
locale: Locale = .current,
|
|
88
|
+
layoutDirection: LayoutDirection? = nil,
|
|
84
89
|
widgetRenderingMode: WidgetRenderingMode = .fullColor
|
|
85
90
|
) -> VoltraDynamicLiveActivityResolvedContent {
|
|
86
91
|
guard VoltraDynamicLiveActivityRegistry.shared.contains(definitionId) else {
|
|
@@ -91,6 +96,13 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
91
96
|
logFailure(definitionId: definitionId, activityName: context.attributes.name, message: "Could not encode content-state props")
|
|
92
97
|
return .empty
|
|
93
98
|
}
|
|
99
|
+
// Captured once so every region shares it and the cache key covers every locale field,
|
|
100
|
+
// including the app's `setDynamicWidgetLocale` override (ADR 0009).
|
|
101
|
+
let localeEnvironment = VoltraLocaleEnvironment.capture(
|
|
102
|
+
locale: locale,
|
|
103
|
+
isRightToLeft: layoutDirection.map { $0 == .rightToLeft },
|
|
104
|
+
appLocale: VoltraWidgetDefaults.dynamicWidgetLocale()
|
|
105
|
+
)
|
|
94
106
|
// WidgetKit asks each Dynamic Island region (and each presentation) to render
|
|
95
107
|
// independently, all with identical state/environment. Cache the resolved
|
|
96
108
|
// variants shape so those siblings share one bundle load, one JS evaluation,
|
|
@@ -103,7 +115,7 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
103
115
|
props: propsJSON,
|
|
104
116
|
activityFamily: activityFamily,
|
|
105
117
|
colorScheme: String(describing: colorScheme ?? .light),
|
|
106
|
-
locale:
|
|
118
|
+
locale: localeEnvironment,
|
|
107
119
|
widgetRenderingMode: String(describing: widgetRenderingMode),
|
|
108
120
|
isStale: context.isStale
|
|
109
121
|
)
|
|
@@ -114,7 +126,7 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
114
126
|
propsJSON: propsJSON,
|
|
115
127
|
activityFamily: activityFamily,
|
|
116
128
|
colorScheme: colorScheme,
|
|
117
|
-
|
|
129
|
+
localeEnvironment: localeEnvironment,
|
|
118
130
|
widgetRenderingMode: widgetRenderingMode
|
|
119
131
|
)
|
|
120
132
|
}
|
|
@@ -126,13 +138,13 @@ public enum VoltraDynamicLiveActivityRenderer {
|
|
|
126
138
|
propsJSON: String,
|
|
127
139
|
activityFamily: String?,
|
|
128
140
|
colorScheme: ColorScheme?,
|
|
129
|
-
|
|
141
|
+
localeEnvironment: VoltraLocaleEnvironment,
|
|
130
142
|
widgetRenderingMode: WidgetRenderingMode
|
|
131
143
|
) -> VoltraDynamicLiveActivityResolvedContent {
|
|
132
144
|
let environmentJSON = VoltraDynamicLiveActivityEnvironmentBuilder.build(
|
|
133
145
|
date: Date(),
|
|
134
146
|
colorScheme: colorScheme,
|
|
135
|
-
|
|
147
|
+
localeEnvironment: localeEnvironment,
|
|
136
148
|
widgetRenderingMode: widgetRenderingMode,
|
|
137
149
|
isStale: context.isStale,
|
|
138
150
|
activityFamily: activityFamily
|
|
@@ -176,18 +188,23 @@ private struct VoltraDynamicLiveActivityDynamicIslandRegionView<Attributes: Volt
|
|
|
176
188
|
|
|
177
189
|
@Environment(\.colorScheme) private var colorScheme
|
|
178
190
|
@Environment(\.locale) private var locale
|
|
191
|
+
@Environment(\.layoutDirection) private var layoutDirection
|
|
179
192
|
@Environment(\.widgetRenderingMode) private var widgetRenderingMode
|
|
180
193
|
|
|
181
194
|
var body: some View {
|
|
182
|
-
|
|
195
|
+
let deepLinkURL = context.attributes.deepLinkUrl.flatMap(VoltraDeepLinkResolver.resolveUrl)
|
|
196
|
+
let content = VoltraDynamicLiveActivityRenderer.resolve(
|
|
183
197
|
definitionId: definitionId,
|
|
184
198
|
context: context,
|
|
185
199
|
activityFamily: nil,
|
|
186
200
|
colorScheme: colorScheme,
|
|
187
201
|
locale: locale,
|
|
202
|
+
layoutDirection: layoutDirection,
|
|
188
203
|
widgetRenderingMode: widgetRenderingMode
|
|
189
204
|
)
|
|
190
|
-
.
|
|
205
|
+
let _ = VoltraRootModifiers.warnIfIslandIgnoresWidgetURL(in: region, root: content.root(for: region), configured: deepLinkURL)
|
|
206
|
+
content.view(for: region, activityId: context.activityID)
|
|
207
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
|
|
@@ -197,6 +214,7 @@ private struct VoltraDynamicLiveActivityLockScreenView<Attributes: VoltraDynamic
|
|
|
197
214
|
|
|
198
215
|
@Environment(\.colorScheme) private var colorScheme
|
|
199
216
|
@Environment(\.locale) private var locale
|
|
217
|
+
@Environment(\.layoutDirection) private var layoutDirection
|
|
200
218
|
@Environment(\.widgetRenderingMode) private var widgetRenderingMode
|
|
201
219
|
|
|
202
220
|
var body: some View {
|
|
@@ -206,14 +224,12 @@ private struct VoltraDynamicLiveActivityLockScreenView<Attributes: VoltraDynamic
|
|
|
206
224
|
activityFamily: nil,
|
|
207
225
|
colorScheme: colorScheme,
|
|
208
226
|
locale: locale,
|
|
227
|
+
layoutDirection: layoutDirection,
|
|
209
228
|
widgetRenderingMode: widgetRenderingMode
|
|
210
229
|
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} else {
|
|
215
|
-
content.view(for: .lockScreen, activityId: context.activityID, deepLink: context.attributes.deepLinkUrl)
|
|
216
|
-
}
|
|
230
|
+
content.view(for: .lockScreen, activityId: context.activityID)
|
|
231
|
+
.voltraWidgetURL(configured: context.attributes.deepLinkUrl.flatMap(VoltraDeepLinkResolver.resolveUrl))
|
|
232
|
+
.voltraActivityBackgroundTint(configured: content.payload?.activityBackgroundTint.flatMap { JSColorParser.parse($0) })
|
|
217
233
|
}
|
|
218
234
|
}
|
|
219
235
|
|
|
@@ -225,6 +241,7 @@ private struct VoltraDynamicLiveActivityAdaptiveLockScreenView<Attributes: Voltr
|
|
|
225
241
|
@Environment(\.activityFamily) private var activityFamily
|
|
226
242
|
@Environment(\.colorScheme) private var colorScheme
|
|
227
243
|
@Environment(\.locale) private var locale
|
|
244
|
+
@Environment(\.layoutDirection) private var layoutDirection
|
|
228
245
|
@Environment(\.widgetRenderingMode) private var widgetRenderingMode
|
|
229
246
|
|
|
230
247
|
var body: some View {
|
|
@@ -234,12 +251,14 @@ private struct VoltraDynamicLiveActivityAdaptiveLockScreenView<Attributes: Voltr
|
|
|
234
251
|
activityFamily: String(describing: activityFamily),
|
|
235
252
|
colorScheme: colorScheme,
|
|
236
253
|
locale: locale,
|
|
254
|
+
layoutDirection: layoutDirection,
|
|
237
255
|
widgetRenderingMode: widgetRenderingMode
|
|
238
256
|
)
|
|
257
|
+
// One widgetURL for the whole presentation, including the small family that shows two
|
|
258
|
+
// regions side by side.
|
|
239
259
|
familyContent(content: content)
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
}
|
|
260
|
+
.voltraWidgetURL(configured: context.attributes.deepLinkUrl.flatMap(VoltraDeepLinkResolver.resolveUrl))
|
|
261
|
+
.voltraActivityBackgroundTint(configured: content.payload?.activityBackgroundTint.flatMap { JSColorParser.parse($0) })
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
@ViewBuilder
|
|
@@ -247,7 +266,7 @@ private struct VoltraDynamicLiveActivityAdaptiveLockScreenView<Attributes: Voltr
|
|
|
247
266
|
if activityFamily == .small {
|
|
248
267
|
smallFamilyContent(content: content)
|
|
249
268
|
} else {
|
|
250
|
-
content.view(for: .lockScreen, activityId: context.activityID
|
|
269
|
+
content.view(for: .lockScreen, activityId: context.activityID)
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
|
|
@@ -256,14 +275,13 @@ private struct VoltraDynamicLiveActivityAdaptiveLockScreenView<Attributes: Voltr
|
|
|
256
275
|
if content.hasContent(for: .supplementalActivityFamiliesSmall) {
|
|
257
276
|
content.view(
|
|
258
277
|
for: .supplementalActivityFamiliesSmall,
|
|
259
|
-
activityId: context.activityID
|
|
260
|
-
deepLink: context.attributes.deepLinkUrl
|
|
278
|
+
activityId: context.activityID
|
|
261
279
|
)
|
|
262
280
|
} else if content.hasContent(for: .islandCompactLeading) || content.hasContent(for: .islandCompactTrailing) {
|
|
263
281
|
HStack(spacing: 0) {
|
|
264
|
-
content.view(for: .islandCompactLeading, activityId: context.activityID
|
|
282
|
+
content.view(for: .islandCompactLeading, activityId: context.activityID)
|
|
265
283
|
Spacer()
|
|
266
|
-
content.view(for: .islandCompactTrailing, activityId: context.activityID
|
|
284
|
+
content.view(for: .islandCompactTrailing, activityId: context.activityID)
|
|
267
285
|
}
|
|
268
286
|
.frame(maxWidth: .infinity)
|
|
269
287
|
}
|
|
@@ -279,12 +297,15 @@ private struct VoltraDynamicLiveActivityResolvedContent {
|
|
|
279
297
|
!(payload?.regions[region] ?? []).isEmpty
|
|
280
298
|
}
|
|
281
299
|
|
|
300
|
+
func root(for region: VoltraRegion) -> VoltraNode? {
|
|
301
|
+
guard let nodes = payload?.regions[region], !nodes.isEmpty else { return nil }
|
|
302
|
+
return nodes.count == 1 ? nodes[0] : .array(nodes)
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
@ViewBuilder
|
|
283
|
-
func view(for region: VoltraRegion, activityId: String
|
|
284
|
-
if let
|
|
285
|
-
let root: VoltraNode = nodes.count == 1 ? nodes[0] : .array(nodes)
|
|
306
|
+
func view(for region: VoltraRegion, activityId: String) -> some View {
|
|
307
|
+
if let root = root(for: region) {
|
|
286
308
|
Voltra(root: root, activityId: activityId)
|
|
287
|
-
.voltraIfLet(deepLink.flatMap(VoltraDeepLinkResolver.resolveUrl)) { view, url in view.widgetURL(url) }
|
|
288
309
|
}
|
|
289
310
|
}
|
|
290
311
|
}
|
|
@@ -302,7 +323,7 @@ private final class VoltraDynamicLiveActivityRenderCache: @unchecked Sendable {
|
|
|
302
323
|
let props: String
|
|
303
324
|
let activityFamily: String?
|
|
304
325
|
let colorScheme: String
|
|
305
|
-
let locale:
|
|
326
|
+
let locale: VoltraLocaleEnvironment
|
|
306
327
|
let widgetRenderingMode: String
|
|
307
328
|
let isStale: Bool
|
|
308
329
|
}
|
|
@@ -330,7 +351,7 @@ private enum VoltraDynamicLiveActivityEnvironmentBuilder {
|
|
|
330
351
|
static func build(
|
|
331
352
|
date: Date,
|
|
332
353
|
colorScheme: ColorScheme?,
|
|
333
|
-
|
|
354
|
+
localeEnvironment: VoltraLocaleEnvironment,
|
|
334
355
|
widgetRenderingMode: WidgetRenderingMode,
|
|
335
356
|
isStale: Bool,
|
|
336
357
|
activityFamily: String?
|
|
@@ -351,11 +372,12 @@ private enum VoltraDynamicLiveActivityEnvironmentBuilder {
|
|
|
351
372
|
var environment: [String: Any] = [
|
|
352
373
|
"date": Int(date.timeIntervalSince1970 * 1000),
|
|
353
374
|
"colorScheme": String(describing: colorScheme ?? .light),
|
|
354
|
-
"locale": locale.identifier,
|
|
355
375
|
"widgetRenderingMode": String(describing: widgetRenderingMode),
|
|
356
376
|
"build": build,
|
|
357
377
|
"isStale": isStale,
|
|
358
378
|
]
|
|
379
|
+
// BCP-47 `locale` and the rest of the ADR 0009 locale fields.
|
|
380
|
+
environment.merge(localeEnvironment.dictionary) { current, _ in current }
|
|
359
381
|
if let activityFamily {
|
|
360
382
|
environment["activityFamily"] = activityFamily
|
|
361
383
|
}
|
|
@@ -255,6 +255,7 @@ public enum VoltraClientWidgetEnvBuilder {
|
|
|
255
255
|
widgetRenderingMode: WidgetRenderingMode,
|
|
256
256
|
showsWidgetContainerBackground: Bool,
|
|
257
257
|
locale: Locale,
|
|
258
|
+
layoutDirection: LayoutDirection? = nil,
|
|
258
259
|
configuration: [String: String],
|
|
259
260
|
serverUpdateJSON: String? = nil
|
|
260
261
|
) -> String {
|
|
@@ -299,12 +300,20 @@ public enum VoltraClientWidgetEnvBuilder {
|
|
|
299
300
|
// sends and what WidgetScope.of(widgetId, configuration:) resolves to.
|
|
300
301
|
let instanceEntry = WidgetCanonicalConfiguration.key(configuration).map { ",\n \"instance\": \(jsonString($0))" } ?? ""
|
|
301
302
|
|
|
303
|
+
// Locale fields (ADR 0009 §1). `locale` is BCP-47 (`pl-PL`), never the ICU `pl_PL` that
|
|
304
|
+
// `Locale.identifier` gives and `Intl` rejects.
|
|
305
|
+
let localeEnvironment = VoltraLocaleEnvironment.capture(
|
|
306
|
+
locale: locale,
|
|
307
|
+
isRightToLeft: layoutDirection.map { $0 == .rightToLeft },
|
|
308
|
+
appLocale: VoltraWidgetDefaults.dynamicWidgetLocale()
|
|
309
|
+
)
|
|
310
|
+
|
|
302
311
|
return """
|
|
303
312
|
{
|
|
304
313
|
"date": \(timestampMs),
|
|
305
314
|
"widgetFamily": \(jsonString(familyString(widgetFamily))),
|
|
306
315
|
"colorScheme": \(jsonString(schemeString(colorScheme))),
|
|
307
|
-
|
|
316
|
+
\(localeEnvironment.jsonFields),
|
|
308
317
|
"widgetRenderingMode": \(jsonString(renderingModeString(widgetRenderingMode))),
|
|
309
318
|
"showsWidgetContainerBackground": \(showsWidgetContainerBackground),
|
|
310
319
|
"configuration": \(configurationJSON),
|
|
@@ -355,6 +364,7 @@ public struct VoltraClientWidgetContentView: View {
|
|
|
355
364
|
@Environment(\.widgetRenderingMode) private var widgetRenderingMode
|
|
356
365
|
@Environment(\.showsWidgetContainerBackground) private var showsWidgetContainerBackground
|
|
357
366
|
@Environment(\.locale) private var locale
|
|
367
|
+
@Environment(\.layoutDirection) private var layoutDirection
|
|
358
368
|
|
|
359
369
|
public init(entry: VoltraClientWidgetEntry, initialState: Data?) {
|
|
360
370
|
self.entry = entry
|
|
@@ -381,6 +391,7 @@ public struct VoltraClientWidgetContentView: View {
|
|
|
381
391
|
widgetRenderingMode: widgetRenderingMode,
|
|
382
392
|
showsWidgetContainerBackground: showsWidgetContainerBackground,
|
|
383
393
|
locale: locale,
|
|
394
|
+
layoutDirection: layoutDirection,
|
|
384
395
|
configuration: entry.configuration,
|
|
385
396
|
serverUpdateJSON: entry.serverUpdateJSON
|
|
386
397
|
)
|
|
@@ -284,6 +284,7 @@ public struct VoltraHomeWidgetView: View {
|
|
|
284
284
|
|
|
285
285
|
public var body: some View {
|
|
286
286
|
let mappedRenderingMode = mapWidgetRenderingMode(widgetRenderingMode)
|
|
287
|
+
let rootModifiers = VoltraRootModifiers(root: entry.rootNode)
|
|
287
288
|
|
|
288
289
|
Group {
|
|
289
290
|
if let root = entry.rootNode {
|
|
@@ -298,7 +299,7 @@ public struct VoltraHomeWidgetView: View {
|
|
|
298
299
|
)
|
|
299
300
|
)
|
|
300
301
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
301
|
-
.
|
|
302
|
+
.voltraWidgetURL(configured: configuredDeepLinkURL(entry), tree: rootModifiers) { defaultDeepLinkURL(entry) }
|
|
302
303
|
|
|
303
304
|
if showRefreshButton {
|
|
304
305
|
content.overlay(alignment: .topTrailing) {
|
|
@@ -311,7 +312,7 @@ public struct VoltraHomeWidgetView: View {
|
|
|
311
312
|
placeholderView(widgetId: entry.widgetId)
|
|
312
313
|
}
|
|
313
314
|
}
|
|
314
|
-
.
|
|
315
|
+
.voltraDefaultContainerBackground(tree: rootModifiers)
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
private func mapWidgetRenderingMode(_ mode: WidgetRenderingMode) -> VoltraWidgetRenderingMode {
|
|
@@ -442,18 +443,8 @@ private func reconstructWithSharedData(content: Any, root: [String: Any]) -> Dat
|
|
|
442
443
|
|
|
443
444
|
// MARK: - Deep link helpers
|
|
444
445
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
func disableWidgetMarginsIfAvailable() -> some View {
|
|
448
|
-
if #available(iOSApplicationExtension 17.0, *) {
|
|
449
|
-
containerBackground(.clear, for: .widget)
|
|
450
|
-
} else {
|
|
451
|
-
self
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
446
|
+
/// The deep link configured for the widget: the timeline entry's, else the stored static one.
|
|
447
|
+
private func configuredDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
457
448
|
// Prefer the timeline entry's deep link URL if available
|
|
458
449
|
if let entryUrl = entry.deepLinkUrl, !entryUrl.isEmpty {
|
|
459
450
|
if entryUrl.contains("://"), let url = URL(string: entryUrl) {
|
|
@@ -476,7 +467,11 @@ private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
|
476
467
|
}
|
|
477
468
|
}
|
|
478
469
|
|
|
479
|
-
|
|
470
|
+
return nil
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// The synthetic deep link used when nothing is configured.
|
|
474
|
+
private func defaultDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
480
475
|
guard let scheme = VoltraDeepLinkResolver.deepLinkScheme() else { return nil }
|
|
481
476
|
|
|
482
477
|
var tag = "unknown"
|
|
@@ -29,11 +29,8 @@ public struct VoltraWidget: Widget {
|
|
|
29
29
|
private func adaptiveConfig() -> some WidgetConfiguration {
|
|
30
30
|
ActivityConfiguration(for: VoltraAttributes.self) { context in
|
|
31
31
|
adaptiveLocksScreenView(context: context)
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
let color = JSColorParser.parse(tint)
|
|
35
|
-
view.activityBackgroundTint(color)
|
|
36
|
-
}
|
|
32
|
+
.voltraWidgetURL(configured: VoltraDeepLinkResolver.resolve(context.attributes))
|
|
33
|
+
.voltraActivityBackgroundTint(configured: context.state.activityBackgroundTint.flatMap { JSColorParser.parse($0) })
|
|
37
34
|
} dynamicIsland: { context in
|
|
38
35
|
dynamicIslandContent(context: context)
|
|
39
36
|
}
|
|
@@ -45,11 +42,8 @@ public struct VoltraWidget: Widget {
|
|
|
45
42
|
private func defaultConfig() -> some WidgetConfiguration {
|
|
46
43
|
ActivityConfiguration(for: VoltraAttributes.self) { context in
|
|
47
44
|
Voltra(root: rootNode(for: .lockScreen, from: context.state), activityId: context.activityID)
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
let color = JSColorParser.parse(tint)
|
|
51
|
-
view.activityBackgroundTint(color)
|
|
52
|
-
}
|
|
45
|
+
.voltraWidgetURL(configured: VoltraDeepLinkResolver.resolve(context.attributes))
|
|
46
|
+
.voltraActivityBackgroundTint(configured: context.state.activityBackgroundTint.flatMap { JSColorParser.parse($0) })
|
|
53
47
|
} dynamicIsland: { context in
|
|
54
48
|
dynamicIslandContent(context: context)
|
|
55
49
|
}
|
|
@@ -65,32 +59,46 @@ public struct VoltraWidget: Widget {
|
|
|
65
59
|
// MARK: - Dynamic Island (shared between iOS versions)
|
|
66
60
|
|
|
67
61
|
private func dynamicIslandContent(context: ActivityViewContext<VoltraAttributes>) -> DynamicIsland {
|
|
68
|
-
|
|
62
|
+
// Set once on the Dynamic Island, which makes it the default for every region; the regions
|
|
63
|
+
// only drop the tree's own widgetURL so the configured link takes precedence.
|
|
64
|
+
let deepLinkURL = VoltraDeepLinkResolver.resolve(context.attributes)
|
|
65
|
+
for region in VoltraRootModifiers.islandTapRegions {
|
|
66
|
+
VoltraRootModifiers.warnIfIslandIgnoresWidgetURL(
|
|
67
|
+
in: region,
|
|
68
|
+
root: rootNode(for: region, from: context.state),
|
|
69
|
+
configured: deepLinkURL
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
var dynamicIsland = DynamicIsland {
|
|
69
73
|
DynamicIslandExpandedRegion(.leading) {
|
|
70
74
|
Voltra(root: rootNode(for: .islandExpandedLeading, from: context.state), activityId: context.activityID)
|
|
71
|
-
.
|
|
75
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
72
76
|
}
|
|
73
77
|
DynamicIslandExpandedRegion(.trailing) {
|
|
74
78
|
Voltra(root: rootNode(for: .islandExpandedTrailing, from: context.state), activityId: context.activityID)
|
|
75
|
-
.
|
|
79
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
76
80
|
}
|
|
77
81
|
DynamicIslandExpandedRegion(.center) {
|
|
78
82
|
Voltra(root: rootNode(for: .islandExpandedCenter, from: context.state), activityId: context.activityID)
|
|
79
|
-
.
|
|
83
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
80
84
|
}
|
|
81
85
|
DynamicIslandExpandedRegion(.bottom) {
|
|
82
86
|
Voltra(root: rootNode(for: .islandExpandedBottom, from: context.state), activityId: context.activityID)
|
|
83
|
-
.
|
|
87
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
84
88
|
}
|
|
85
89
|
} compactLeading: {
|
|
86
90
|
Voltra(root: rootNode(for: .islandCompactLeading, from: context.state), activityId: context.activityID)
|
|
87
|
-
.
|
|
91
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
88
92
|
} compactTrailing: {
|
|
89
93
|
Voltra(root: rootNode(for: .islandCompactTrailing, from: context.state), activityId: context.activityID)
|
|
90
|
-
.
|
|
94
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
91
95
|
} minimal: {
|
|
92
96
|
Voltra(root: rootNode(for: .islandMinimal, from: context.state), activityId: context.activityID)
|
|
93
|
-
.
|
|
97
|
+
.voltraDeferringWidgetURL(to: deepLinkURL)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if let deepLinkURL {
|
|
101
|
+
dynamicIsland = dynamicIsland.widgetURL(deepLinkURL)
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
// Apply keylineTint if specified
|
|
@@ -37,6 +37,13 @@ struct VoltraElementView: View {
|
|
|
37
37
|
let element: VoltraElement
|
|
38
38
|
|
|
39
39
|
var body: some View {
|
|
40
|
+
// Native modifiers wrap the finished, styled component (ADR 0005).
|
|
41
|
+
component
|
|
42
|
+
.applyNativeModifiers(element.nativeModifiers)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ViewBuilder
|
|
46
|
+
private var component: some View {
|
|
40
47
|
switch element.type {
|
|
41
48
|
case "Button":
|
|
42
49
|
VoltraButton(element)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Turns the deep link strings Voltra accepts into URLs. The ActivityKit-specific entry point is
|
|
4
|
+
/// in `VoltraDeepLinkResolver.swift`; this half only needs Foundation, so native modifiers such as
|
|
5
|
+
/// `widgetURL` resolve links the same way.
|
|
6
|
+
public enum VoltraDeepLinkResolver {
|
|
7
|
+
static func deepLinkScheme() -> String? {
|
|
8
|
+
if let types = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]] {
|
|
9
|
+
for t in types {
|
|
10
|
+
if let schemes = t["CFBundleURLSchemes"] as? [String], let s = schemes.first, !s.isEmpty {
|
|
11
|
+
return s
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return Bundle.main.bundleIdentifier
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Resolves a URL string, supporting both absolute and relative paths
|
|
19
|
+
/// - Parameter raw: The URL string (e.g., "myapp://path", "/path", or "path")
|
|
20
|
+
/// - Returns: A resolved URL, or nil if invalid
|
|
21
|
+
public static func resolveUrl(_ raw: String) -> URL? {
|
|
22
|
+
guard !raw.isEmpty else { return nil }
|
|
23
|
+
|
|
24
|
+
// If it's already an absolute URL, use it as-is
|
|
25
|
+
if raw.contains("://"), let url = URL(string: raw) {
|
|
26
|
+
return url
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Otherwise, prepend the app's URL scheme
|
|
30
|
+
if let scheme = deepLinkScheme() {
|
|
31
|
+
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
|
32
|
+
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -2,18 +2,7 @@ import ActivityKit
|
|
|
2
2
|
import Foundation
|
|
3
3
|
import SwiftUI
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
static func deepLinkScheme() -> String? {
|
|
7
|
-
if let types = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]] {
|
|
8
|
-
for t in types {
|
|
9
|
-
if let schemes = t["CFBundleURLSchemes"] as? [String], let s = schemes.first, !s.isEmpty {
|
|
10
|
-
return s
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return Bundle.main.bundleIdentifier
|
|
15
|
-
}
|
|
16
|
-
|
|
5
|
+
extension VoltraDeepLinkResolver {
|
|
17
6
|
static func resolve(
|
|
18
7
|
_ attributes: VoltraAttributes
|
|
19
8
|
) -> URL? {
|
|
@@ -22,24 +11,4 @@ public enum VoltraDeepLinkResolver {
|
|
|
22
11
|
}
|
|
23
12
|
return nil
|
|
24
13
|
}
|
|
25
|
-
|
|
26
|
-
/// Resolves a URL string, supporting both absolute and relative paths
|
|
27
|
-
/// - Parameter raw: The URL string (e.g., "myapp://path", "/path", or "path")
|
|
28
|
-
/// - Returns: A resolved URL, or nil if invalid
|
|
29
|
-
public static func resolveUrl(_ raw: String) -> URL? {
|
|
30
|
-
guard !raw.isEmpty else { return nil }
|
|
31
|
-
|
|
32
|
-
// If it's already an absolute URL, use it as-is
|
|
33
|
-
if raw.contains("://"), let url = URL(string: raw) {
|
|
34
|
-
return url
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Otherwise, prepend the app's URL scheme
|
|
38
|
-
if let scheme = deepLinkScheme() {
|
|
39
|
-
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
|
40
|
-
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return nil
|
|
44
|
-
}
|
|
45
14
|
}
|