@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
import SwiftUI
|
|
4
|
+
#if canImport(VoltraSharedCore)
|
|
5
|
+
// The Swift package splits the SwiftUI-free parsing layer into its own module; the podspec
|
|
6
|
+
// compiles both folders into one.
|
|
7
|
+
import VoltraSharedCore
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
enum VoltraModifierError: Error, Equatable {
|
|
11
|
+
case missingParameter(String)
|
|
12
|
+
case invalidParameter(String)
|
|
13
|
+
case unexpectedParameter(String)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// A native modifier: the parameter names its TypeScript factory may send, and how to build it.
|
|
17
|
+
/// Declaring the names makes a renamed parameter fail the parity test instead of silently
|
|
18
|
+
/// falling back to a default.
|
|
19
|
+
struct VoltraModifierDefinition {
|
|
20
|
+
let parameters: Set<String>
|
|
21
|
+
let make: ([String: Any]) throws -> any ViewModifier
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// String-keyed table of native modifiers. Unknown types and parameters that do not decode are
|
|
25
|
+
/// logged and skipped, so a bad descriptor never breaks the widget.
|
|
26
|
+
enum VoltraModifierRegistry {
|
|
27
|
+
private static let logger = Logger(subsystem: "com.voltra", category: "modifier")
|
|
28
|
+
|
|
29
|
+
private(set) static var definitions: [String: VoltraModifierDefinition] = builtInModifierDefinitions
|
|
30
|
+
|
|
31
|
+
static var registeredTypes: Set<String> {
|
|
32
|
+
Set(definitions.keys)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Not public yet: user-registered modifiers are future work (ADR 0005).
|
|
36
|
+
static func register(_ type: String, definition: VoltraModifierDefinition) {
|
|
37
|
+
definitions[type] = definition
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Decodes the JSON-encoded `modifiers` prop. Entries without a string `$type` are dropped.
|
|
41
|
+
static func parseDescriptors(_ json: String?) -> [VoltraModifierDescriptor] {
|
|
42
|
+
VoltraModifierDescriptor.parseList(json)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Builds the modifier for a descriptor, or `nil` when the type is unknown.
|
|
46
|
+
static func makeModifier(_ descriptor: VoltraModifierDescriptor) throws -> (any ViewModifier)? {
|
|
47
|
+
guard let definition = definitions[descriptor.type] else { return nil }
|
|
48
|
+
if let unexpected = descriptor.params.keys.sorted().first(where: { !definition.parameters.contains($0) }) {
|
|
49
|
+
throw VoltraModifierError.unexpectedParameter(unexpected)
|
|
50
|
+
}
|
|
51
|
+
return try definition.make(descriptor.params)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Whether the descriptor names a known modifier whose parameters decode, so applying it
|
|
55
|
+
/// changes the view. Hosts use it before dropping a default the modifier would replace.
|
|
56
|
+
static func canApply(_ descriptor: VoltraModifierDescriptor) -> Bool {
|
|
57
|
+
(try? makeModifier(descriptor)) != nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static func apply<Content: View>(_ descriptor: VoltraModifierDescriptor, to content: Content) -> AnyView {
|
|
61
|
+
do {
|
|
62
|
+
guard let modifier = try makeModifier(descriptor) else {
|
|
63
|
+
logger.warning("Ignoring unknown modifier \(descriptor.type, privacy: .public)")
|
|
64
|
+
return AnyView(content)
|
|
65
|
+
}
|
|
66
|
+
return erase(content, modifier)
|
|
67
|
+
} catch {
|
|
68
|
+
logger.warning("Ignoring modifier \(descriptor.type, privacy: .public): \(String(describing: error), privacy: .public)")
|
|
69
|
+
return AnyView(content)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static func erase<Content: View, Modifier: ViewModifier>(_ content: Content, _ modifier: Modifier) -> AnyView {
|
|
74
|
+
AnyView(content.modifier(modifier))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Every link of a native modifier chain has this one type, so the outer shape of the view
|
|
79
|
+
/// depends only on the length of the list. Inside a link, the erased type is the concrete
|
|
80
|
+
/// modifier: value changes of the same modifier diff and animate, while a different `$type` at a
|
|
81
|
+
/// position (or an entry that stops decoding) rebuilds the wrapped component.
|
|
82
|
+
struct VoltraStableModifier: ViewModifier {
|
|
83
|
+
let descriptor: VoltraModifierDescriptor
|
|
84
|
+
|
|
85
|
+
@Environment(\.voltraHostAppliedModifierTypes) private var hostAppliedModifierTypes
|
|
86
|
+
|
|
87
|
+
func body(content: Content) -> some View {
|
|
88
|
+
// The widget host applies this modifier itself with a configured value that takes
|
|
89
|
+
// precedence; applying the tree's as well would leave SwiftUI two of them.
|
|
90
|
+
hostAppliedModifierTypes.contains(descriptor.type)
|
|
91
|
+
? AnyView(content)
|
|
92
|
+
: VoltraModifierRegistry.apply(descriptor, to: content)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private struct VoltraHostAppliedModifierTypesKey: EnvironmentKey {
|
|
97
|
+
static let defaultValue: Set<String> = []
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
extension EnvironmentValues {
|
|
101
|
+
/// Native modifier types that a widget or Live Activity host applies outside the rendered tree,
|
|
102
|
+
/// so the tree's own descriptors of these types are skipped.
|
|
103
|
+
var voltraHostAppliedModifierTypes: Set<String> {
|
|
104
|
+
get { self[VoltraHostAppliedModifierTypesKey.self] }
|
|
105
|
+
set { self[VoltraHostAppliedModifierTypesKey.self] = newValue }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
extension View {
|
|
110
|
+
/// Applies native modifiers in array order: the first descriptor is innermost.
|
|
111
|
+
@ViewBuilder
|
|
112
|
+
func applyNativeModifiers(_ descriptors: [VoltraModifierDescriptor]) -> some View {
|
|
113
|
+
if descriptors.isEmpty {
|
|
114
|
+
self
|
|
115
|
+
} else {
|
|
116
|
+
descriptors.reduce(AnyView(self)) { view, descriptor in
|
|
117
|
+
AnyView(view.modifier(VoltraStableModifier(descriptor: descriptor)))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Parameter decoding
|
|
124
|
+
|
|
125
|
+
extension [String: Any] {
|
|
126
|
+
func requiredString(_ key: String) throws -> String {
|
|
127
|
+
guard let string = try optionalString(key) else { throw VoltraModifierError.missingParameter(key) }
|
|
128
|
+
return string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func optionalString(_ key: String) throws -> String? {
|
|
132
|
+
guard let value = self[key], !(value is NSNull) else { return nil }
|
|
133
|
+
guard let string = value as? String else { throw VoltraModifierError.invalidParameter(key) }
|
|
134
|
+
return string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func requiredNumber(_ key: String) throws -> CGFloat {
|
|
138
|
+
guard let number = try optionalNumber(key) else { throw VoltraModifierError.missingParameter(key) }
|
|
139
|
+
return number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// A color string in any form `style` accepts; JSON `null` means "system default".
|
|
143
|
+
func optionalColor(_ key: String) throws -> Color? {
|
|
144
|
+
guard let string = try optionalString(key) else { return nil }
|
|
145
|
+
guard let color = JSColorParser.parse(string) else { throw VoltraModifierError.invalidParameter(key) }
|
|
146
|
+
return color
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func requiredColor(_ key: String) throws -> Color {
|
|
150
|
+
guard let color = try optionalColor(key) else { throw VoltraModifierError.missingParameter(key) }
|
|
151
|
+
return color
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func requiredEnum<Option: RawRepresentable>(_ key: String, as _: Option.Type = Option.self) throws -> Option where Option.RawValue == String {
|
|
155
|
+
guard let option = try Option(rawValue: requiredString(key)) else { throw VoltraModifierError.invalidParameter(key) }
|
|
156
|
+
return option
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func optionalEnum<Option: RawRepresentable>(_ key: String, as _: Option.Type = Option.self) throws -> Option? where Option.RawValue == String {
|
|
160
|
+
guard let string = try optionalString(key) else { return nil }
|
|
161
|
+
guard let option = Option(rawValue: string) else { throw VoltraModifierError.invalidParameter(key) }
|
|
162
|
+
return option
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func optionalBool(_ key: String) throws -> Bool? {
|
|
166
|
+
guard let value = self[key], !(value is NSNull) else { return nil }
|
|
167
|
+
// JSONSerialization bridges JSON booleans to NSNumber; reject plain numbers.
|
|
168
|
+
guard let number = value as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() else {
|
|
169
|
+
throw VoltraModifierError.invalidParameter(key)
|
|
170
|
+
}
|
|
171
|
+
return number.boolValue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func optionalNumber(_ key: String) throws -> CGFloat? {
|
|
175
|
+
guard let value = self[key], !(value is NSNull) else { return nil }
|
|
176
|
+
guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else {
|
|
177
|
+
throw VoltraModifierError.invalidParameter(key)
|
|
178
|
+
}
|
|
179
|
+
return CGFloat(number.doubleValue)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import WidgetKit
|
|
4
|
+
#if canImport(VoltraSharedCore)
|
|
5
|
+
import VoltraSharedCore
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
/// The native modifiers in a rendered tree that replace a host default, found in one walk so a
|
|
9
|
+
/// host that checks both pays for a single pass.
|
|
10
|
+
struct VoltraRootModifiers {
|
|
11
|
+
let setsWidgetURL: Bool
|
|
12
|
+
let setsContainerBackground: Bool
|
|
13
|
+
|
|
14
|
+
init(root: VoltraNode?) {
|
|
15
|
+
let found = root?.nativeModifierTypes(among: ["widgetURL", "containerBackground"]) ?? []
|
|
16
|
+
setsWidgetURL = found.contains("widgetURL")
|
|
17
|
+
setsContainerBackground = found.contains("containerBackground")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private static let logger = Logger(subsystem: "com.voltra", category: "modifier")
|
|
21
|
+
|
|
22
|
+
/// A tap on the compact or minimal Dynamic Island opens only `DynamicIsland.widgetURL`, which
|
|
23
|
+
/// hosts set from the configured deep link; a `widgetURL` inside these regions is not used.
|
|
24
|
+
static let islandTapRegions: Set<VoltraRegion> = [.islandCompactLeading, .islandCompactTrailing, .islandMinimal]
|
|
25
|
+
|
|
26
|
+
/// Warns when a compact or minimal Dynamic Island region sets `widgetURL` with no deep link
|
|
27
|
+
/// configured, because tapping the island then opens the app without a URL.
|
|
28
|
+
static func warnIfIslandIgnoresWidgetURL(in region: VoltraRegion, root: VoltraNode?, configured: URL?) {
|
|
29
|
+
guard configured == nil, islandTapRegions.contains(region), VoltraRootModifiers(root: root).setsWidgetURL else {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
logger.warning(
|
|
33
|
+
"widgetURL in the Dynamic Island region \(region.rawValue, privacy: .public) does not set where a tap opens; pass deepLinkUrl when starting the Live Activity"
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Defaults that widget and Live Activity hosts apply outside the rendered tree (ADR 0005). Host
|
|
39
|
+
/// code calls these instead of naming modifiers.
|
|
40
|
+
extension View {
|
|
41
|
+
/// Sets the tap URL of a view hierarchy that carries at most one `widgetURL`; Apple leaves several
|
|
42
|
+
/// undefined. A configured deep link takes precedence and the tree's `widgetURL` modifiers are
|
|
43
|
+
/// skipped. Otherwise a `widgetURL` modifier in the tree wins over the synthetic fallback.
|
|
44
|
+
@ViewBuilder
|
|
45
|
+
func voltraWidgetURL(configured: URL?, tree: VoltraRootModifiers? = nil, fallback: () -> URL? = { nil }) -> some View {
|
|
46
|
+
if let configured {
|
|
47
|
+
voltraHostApplies("widgetURL")
|
|
48
|
+
.widgetURL(configured)
|
|
49
|
+
} else if tree?.setsWidgetURL == true {
|
|
50
|
+
self
|
|
51
|
+
} else if let url = fallback() {
|
|
52
|
+
widgetURL(url)
|
|
53
|
+
} else {
|
|
54
|
+
self
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// For a Dynamic Island region: a configured deep link is set once on the `DynamicIsland`, which
|
|
59
|
+
/// makes it the default for every region, so the region only drops the tree's `widgetURL`
|
|
60
|
+
/// modifiers that would override it.
|
|
61
|
+
@ViewBuilder
|
|
62
|
+
func voltraDeferringWidgetURL(to configured: URL?) -> some View {
|
|
63
|
+
if configured != nil {
|
|
64
|
+
voltraHostApplies("widgetURL")
|
|
65
|
+
} else {
|
|
66
|
+
self
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Tints a Live Activity's Lock Screen background with the tint passed when starting or updating
|
|
71
|
+
/// it, which takes precedence over an `activityBackgroundTint` modifier in the tree.
|
|
72
|
+
@ViewBuilder
|
|
73
|
+
func voltraActivityBackgroundTint(configured: Color?) -> some View {
|
|
74
|
+
if let configured {
|
|
75
|
+
voltraHostApplies("activityBackgroundTint")
|
|
76
|
+
.activityBackgroundTint(configured)
|
|
77
|
+
} else {
|
|
78
|
+
self
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Clears the widget's container background, unless the tree sets its own with a
|
|
83
|
+
/// `containerBackground` modifier.
|
|
84
|
+
@ViewBuilder
|
|
85
|
+
func voltraDefaultContainerBackground(tree: VoltraRootModifiers) -> some View {
|
|
86
|
+
if tree.setsContainerBackground {
|
|
87
|
+
self
|
|
88
|
+
} else if #available(iOS 17.0, *) {
|
|
89
|
+
containerBackground(.clear, for: .widget)
|
|
90
|
+
} else {
|
|
91
|
+
self
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Marks a native modifier type as applied by the host, so the tree's descriptors of that type
|
|
96
|
+
/// are skipped. Adds to the set, so several host defaults compose.
|
|
97
|
+
private func voltraHostApplies(_ type: String) -> some View {
|
|
98
|
+
transformEnvironment(\.voltraHostAppliedModifierTypes) { $0.insert(type) }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -6,7 +6,9 @@ struct TextStyle {
|
|
|
6
6
|
var fontSize: CGFloat = 17
|
|
7
7
|
var fontWeight: Font.Weight = .regular
|
|
8
8
|
var fontFamily: String?
|
|
9
|
-
|
|
9
|
+
/// Only set when `textAlign` is given, so an unset alignment leaves the inherited value, such
|
|
10
|
+
/// as a `multilineTextAlignment` native modifier, in effect.
|
|
11
|
+
var alignment: TextAlignment?
|
|
10
12
|
var lineLimit: Int?
|
|
11
13
|
var lineSpacing: CGFloat = 0 // Extra space between lines
|
|
12
14
|
var decoration: TextDecoration = .none
|
|
@@ -43,7 +45,7 @@ struct TextStyleModifier: ViewModifier {
|
|
|
43
45
|
// 2. Color
|
|
44
46
|
.foregroundColor(resolvedColor)
|
|
45
47
|
// 3. Layout / Spacing
|
|
46
|
-
.
|
|
48
|
+
.voltraIfLet(style.alignment) { view, alignment in view.multilineTextAlignment(alignment) }
|
|
47
49
|
.lineLimit(style.lineLimit)
|
|
48
50
|
.lineSpacing(style.lineSpacing)
|
|
49
51
|
}
|
|
@@ -25,7 +25,7 @@ extension View {
|
|
|
25
25
|
|
|
26
26
|
func applyStyle(_ style: (LayoutStyle, DecorationStyle, RenderingStyle, TextStyle), contentAlignment: Alignment? = nil) -> some View {
|
|
27
27
|
let (layout, decoration, rendering, text) = style
|
|
28
|
-
let frameAlignment = contentAlignment ?? Alignment(horizontal: text.alignment.horizontalAlignment, vertical: .top)
|
|
28
|
+
let frameAlignment = contentAlignment ?? Alignment(horizontal: (text.alignment ?? .leading).horizontalAlignment, vertical: .top)
|
|
29
29
|
return self
|
|
30
30
|
// 1. Text Properties (Propagate font size for measurement)
|
|
31
31
|
.modifier(TextStyleModifier(style: text))
|
|
@@ -51,13 +51,9 @@ public struct VoltraText: VoltraView {
|
|
|
51
51
|
return baseFont
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return JSStyleParser.textAlignment(mta)
|
|
58
|
-
}
|
|
59
|
-
return textStyle.alignment
|
|
60
|
-
}()
|
|
54
|
+
// Parameter takes precedence over style. Without either, the inherited alignment applies, so a
|
|
55
|
+
// `multilineTextAlignment` native modifier on this Text or a container is not overridden.
|
|
56
|
+
let alignment: TextAlignment? = params.multilineTextAlignment.map { JSStyleParser.textAlignment($0) } ?? textStyle.alignment
|
|
61
57
|
|
|
62
58
|
let resolvedColor: Color = {
|
|
63
59
|
if let widget = voltraEnvironment.widget,
|
|
@@ -77,7 +73,7 @@ public struct VoltraText: VoltraView {
|
|
|
77
73
|
// These technically work on View, but good to keep close
|
|
78
74
|
.font(font)
|
|
79
75
|
.foregroundColor(resolvedColor)
|
|
80
|
-
.multilineTextAlignment(alignment)
|
|
76
|
+
.voltraIfLet(alignment) { view, alignment in view.multilineTextAlignment(alignment) }
|
|
81
77
|
.lineSpacing(textStyle.lineSpacing)
|
|
82
78
|
.voltraIfLet(params.numberOfLines) { view, numberOfLines in
|
|
83
79
|
view.lineLimit(Int(numberOfLines))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-voltra/ios-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Client-only Voltra APIs for iOS",
|
|
5
5
|
"main": "./build/commonjs/index.js",
|
|
6
6
|
"module": "./build/module/index.js",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"@expo/plist": "^0.3.5",
|
|
40
40
|
"dedent": "^1.7.1",
|
|
41
41
|
"xcode": "^3.0.1",
|
|
42
|
-
"@use-voltra/compiler": "^2.
|
|
43
|
-
"@use-voltra/expo-plugin": "^2.
|
|
44
|
-
"@use-voltra/ios": "^2.
|
|
42
|
+
"@use-voltra/compiler": "^2.4.0",
|
|
43
|
+
"@use-voltra/expo-plugin": "^2.4.0",
|
|
44
|
+
"@use-voltra/ios": "^2.4.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"expo": "*",
|
|
@@ -23,3 +23,31 @@ export const updateDynamicWidget = async (
|
|
|
23
23
|
|
|
24
24
|
return getNativeVoltra().updateDynamicWidget(dynamicWidgetId, dynamicWidgetPropsJson)
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render Dynamic Widgets and Dynamic Live Activities in a language the app chooses.
|
|
29
|
+
*
|
|
30
|
+
* iOS 17 and 18 do not pass a per-app language chosen in Settings to widget extensions, and apps
|
|
31
|
+
* with their own language setting have no other way to reach them. The tag (BCP-47, for example
|
|
32
|
+
* `"pl"` or `"pt-BR"`) is stored in the App Group and reaches every render as `env.appLocale`;
|
|
33
|
+
* `resolveLocale` gives it precedence over the user's system languages. Pass `null` to clear it.
|
|
34
|
+
*
|
|
35
|
+
* Widgets reload and running Dynamic Live Activities re-render once the value is stored.
|
|
36
|
+
* Requires `groupIdentifier` in the Voltra config plugin, and rejects without one.
|
|
37
|
+
*/
|
|
38
|
+
export const setDynamicWidgetLocale = async (tag: string | null): Promise<void> => {
|
|
39
|
+
if (!assertRunningOnApple()) return Promise.resolve()
|
|
40
|
+
|
|
41
|
+
return getNativeVoltra().setDynamicWidgetLocale(normalizeDynamicWidgetLocale(tag))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeDynamicWidgetLocale(tag: string | null): string | null {
|
|
45
|
+
if (tag === null) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
if (typeof tag !== 'string') {
|
|
49
|
+
throw new TypeError('setDynamicWidgetLocale expects a BCP-47 language tag or null.')
|
|
50
|
+
}
|
|
51
|
+
const trimmed = tag.trim().replace(/_/g, '-')
|
|
52
|
+
return trimmed.length === 0 ? null : trimmed
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,12 @@ export {
|
|
|
5
5
|
} from './components/VoltraLiveActivityPreview.js'
|
|
6
6
|
export { VoltraView, type VoltraViewProps } from './components/VoltraView.js'
|
|
7
7
|
export { VoltraWidgetPreview, type VoltraWidgetPreviewProps } from './components/VoltraWidgetPreview.js'
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
type DynamicWidgetProps,
|
|
10
|
+
type DynamicWidgetPropsValue,
|
|
11
|
+
setDynamicWidgetLocale,
|
|
12
|
+
updateDynamicWidget,
|
|
13
|
+
} from './dynamic-widget/api.js'
|
|
9
14
|
export * from './events.js'
|
|
10
15
|
export { isGlassSupported, isHeadless, useIsHeadless } from './helpers.js'
|
|
11
16
|
export { logger, type VoltraLogLevel } from './logger.js'
|
|
@@ -117,6 +117,8 @@ export interface Spec extends TurboModule {
|
|
|
117
117
|
reloadDynamicLiveActivities(definitionIds?: string[] | null): Promise<void>
|
|
118
118
|
clearPreloadedImages(keys?: string[] | null): Promise<void>
|
|
119
119
|
updateDynamicWidget(dynamicWidgetId: string, dynamicWidgetPropsJson: string): Promise<void>
|
|
120
|
+
/** Language override for Dynamic Widgets and Live Activities (`env.appLocale`); null clears it. */
|
|
121
|
+
setDynamicWidgetLocale(tag: string | null): Promise<void>
|
|
120
122
|
updateWidget(widgetId: string, jsonString: string, options: UpdateWidgetOptions): Promise<void>
|
|
121
123
|
scheduleWidget(widgetId: string, timelineJson: string): Promise<void>
|
|
122
124
|
reloadWidgets(widgetIds?: string[] | null): Promise<void>
|