@use-voltra/ios-client 2.3.1 → 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.
Files changed (132) hide show
  1. package/build/commonjs/dynamic-widget/api.js +28 -1
  2. package/build/commonjs/dynamic-widget/api.js.map +1 -1
  3. package/build/commonjs/index.js +7 -12
  4. package/build/commonjs/index.js.map +1 -1
  5. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  6. package/build/commonjs/utils/useUpdateOnHMR.js +6 -14
  7. package/build/commonjs/utils/useUpdateOnHMR.js.map +1 -1
  8. package/build/module/dynamic-widget/api.js +26 -0
  9. package/build/module/dynamic-widget/api.js.map +1 -1
  10. package/build/module/index.js +1 -2
  11. package/build/module/index.js.map +1 -1
  12. package/build/module/native/NativeVoltra.js.map +1 -1
  13. package/build/module/utils/useUpdateOnHMR.js +1 -13
  14. package/build/module/utils/useUpdateOnHMR.js.map +1 -1
  15. package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
  16. package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
  17. package/build/typescript/commonjs/index.d.ts +1 -2
  18. package/build/typescript/commonjs/index.d.ts.map +1 -1
  19. package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
  20. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  21. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts +1 -1
  22. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts.map +1 -1
  23. package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
  24. package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
  25. package/build/typescript/module/index.d.ts +1 -2
  26. package/build/typescript/module/index.d.ts.map +1 -1
  27. package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
  28. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  29. package/build/typescript/module/utils/useUpdateOnHMR.d.ts +1 -1
  30. package/build/typescript/module/utils/useUpdateOnHMR.d.ts.map +1 -1
  31. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
  32. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
  33. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
  34. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  35. package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
  36. package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
  37. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
  38. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
  39. package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
  40. package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
  41. package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
  42. package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
  43. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
  44. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
  45. package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
  46. package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
  47. package/expo-plugin/build/cjs/validation.js +1 -0
  48. package/expo-plugin/build/cjs/validation.js.map +1 -1
  49. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
  50. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
  51. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
  52. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  53. package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
  54. package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
  55. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
  56. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
  57. package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
  58. package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
  59. package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
  60. package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
  61. package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
  62. package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
  63. package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
  64. package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
  65. package/expo-plugin/build/esm/validation.js +2 -1
  66. package/expo-plugin/build/esm/validation.js.map +1 -1
  67. package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
  68. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
  69. package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
  70. package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
  71. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
  72. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
  73. package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
  74. package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
  75. package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
  76. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
  77. package/expo-plugin/build/types/types.d.ts +13 -4
  78. package/expo-plugin/build/types/types.d.ts.map +1 -1
  79. package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
  80. package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
  81. package/expo-plugin/build/types/validation.d.ts.map +1 -1
  82. package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
  83. package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
  84. package/expo-plugin/src/ios-widget/files/index.ts +14 -1
  85. package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
  86. package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
  87. package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
  88. package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
  89. package/expo-plugin/src/types.ts +13 -3
  90. package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
  91. package/expo-plugin/src/validation.ts +2 -0
  92. package/ios/Package.swift +8 -1
  93. package/ios/VoltraWidget.podspec +1 -1
  94. package/ios/app/NativeVoltra.mm +15 -2
  95. package/ios/app/VoltraModule.swift +11 -0
  96. package/ios/app/VoltraModuleImpl.swift +14 -0
  97. package/ios/app/VoltraViewComponentView.mm +2 -2
  98. package/ios/app/VoltraViewRoot.swift +6 -12
  99. package/ios/shared/ShortNames.swift +1 -0
  100. package/ios/shared/VoltraConstants.swift +4 -0
  101. package/ios/shared/VoltraElement.swift +43 -2
  102. package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
  103. package/ios/shared/VoltraModifierDescriptor.swift +33 -0
  104. package/ios/shared/VoltraWidgetDefaults.swift +24 -0
  105. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
  106. package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
  107. package/ios/target/VoltraHomeWidget.swift +10 -15
  108. package/ios/target/VoltraWidget.swift +26 -18
  109. package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
  110. package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
  111. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
  112. package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
  113. package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
  114. package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
  115. package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
  116. package/ios/ui/Style/TextStyle.swift +4 -2
  117. package/ios/ui/Style/View+applyStyle.swift +1 -1
  118. package/ios/ui/Views/VoltraText.swift +4 -8
  119. package/package.json +4 -4
  120. package/src/dynamic-widget/api.ts +28 -0
  121. package/src/index.ts +6 -2
  122. package/src/native/NativeVoltra.ts +2 -0
  123. package/src/utils/useUpdateOnHMR.ts +1 -16
  124. package/build/commonjs/utils/helpers.js +0 -25
  125. package/build/commonjs/utils/helpers.js.map +0 -1
  126. package/build/module/utils/helpers.js +0 -4
  127. package/build/module/utils/helpers.js.map +0 -1
  128. package/build/typescript/commonjs/utils/helpers.d.ts +0 -2
  129. package/build/typescript/commonjs/utils/helpers.d.ts.map +0 -1
  130. package/build/typescript/module/utils/helpers.d.ts +0 -2
  131. package/build/typescript/module/utils/helpers.d.ts.map +0 -1
  132. package/src/utils/helpers.ts +0 -1
@@ -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
- var alignment: TextAlignment = .leading
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
- .multilineTextAlignment(style.alignment)
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
- let alignment: TextAlignment = {
55
- // Parameter takes precedence over style
56
- if let mta = params.multilineTextAlignment {
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.1",
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.3.1",
43
- "@use-voltra/expo-plugin": "^2.3.1",
44
- "@use-voltra/ios": "^2.3.1"
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 { type DynamicWidgetProps, type DynamicWidgetPropsValue, updateDynamicWidget } from './dynamic-widget/api.js'
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'
@@ -47,7 +52,6 @@ export { assertRunningOnApple } from './utils/assertRunningOnApple.js'
47
52
  export { enableWidgetHotReload } from './utils/enableWidgetHotReload.js'
48
53
  export { enableDynamicLiveActivityHotReload } from './utils/enableDynamicLiveActivityHotReload.js'
49
54
  export { useUpdateOnHMR } from './utils/useUpdateOnHMR.js'
50
- export * from './utils/helpers.js'
51
55
  export type { VoltraElementJson, VoltraNodeJson } from './types.js'
52
56
  export {
53
57
  clearWidgetServerUpdate,
@@ -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>
@@ -1,16 +1 @@
1
- import { getFastRefreshHub } from '@use-voltra/ios'
2
- import { useEffect, useState } from 'react'
3
-
4
- export const useUpdateOnHMR = () => {
5
- const [, forceUpdate] = useState(0)
6
-
7
- useEffect(() => {
8
- if (!__DEV__) {
9
- return
10
- }
11
-
12
- return getFastRefreshHub().onPatch(() => {
13
- forceUpdate((prev) => prev + 1)
14
- })
15
- }, [])
16
- }
1
+ export { useUpdateOnHMR } from '@use-voltra/ios'
@@ -1,25 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- Object.defineProperty(exports, "isGlassSupported", {
7
- enumerable: true,
8
- get: function () {
9
- return _helpers.isGlassSupported;
10
- }
11
- });
12
- Object.defineProperty(exports, "isHeadless", {
13
- enumerable: true,
14
- get: function () {
15
- return _helpers.isHeadless;
16
- }
17
- });
18
- Object.defineProperty(exports, "useIsHeadless", {
19
- enumerable: true,
20
- get: function () {
21
- return _helpers.useIsHeadless;
22
- }
23
- });
24
- var _helpers = require("../helpers.js");
25
- //# sourceMappingURL=helpers.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_helpers","require"],"sourceRoot":"../../../src","sources":["utils/helpers.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,QAAA,GAAAC,OAAA","ignoreList":[]}
@@ -1,4 +0,0 @@
1
- "use strict";
2
-
3
- export { isGlassSupported, isHeadless, useIsHeadless } from '../helpers.js';
4
- //# sourceMappingURL=helpers.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["isGlassSupported","isHeadless","useIsHeadless"],"sourceRoot":"../../../src","sources":["utils/helpers.ts"],"mappings":";;AAAA,SAASA,gBAAgB,EAAEC,UAAU,EAAEC,aAAa,QAAQ,eAAe","ignoreList":[]}
@@ -1,2 +0,0 @@
1
- export { isGlassSupported, isHeadless, useIsHeadless } from '../helpers.js';
2
- //# sourceMappingURL=helpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA"}
@@ -1,2 +0,0 @@
1
- export { isGlassSupported, isHeadless, useIsHeadless } from '../helpers.js';
2
- //# sourceMappingURL=helpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA"}
@@ -1 +0,0 @@
1
- export { isGlassSupported, isHeadless, useIsHeadless } from '../helpers.js'