expo-invoke 2.6.15 → 2.8.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/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +35 -0
- package/android/src/main/java/expo/modules/invoke/InvokeSliceProvider.kt +9 -0
- package/build/plugin/android/withAndroidInvoke.d.ts +1 -0
- package/build/plugin/android/withAndroidInvoke.js +1 -0
- package/build/plugin/android/withAndroidInvoke.js.map +1 -1
- package/build/plugin/codegen/generateSwiftIntents.js +22 -0
- package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
- package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
- package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
- package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
- package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
- package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
- package/build/plugin/ios/widgets/withEasConfig.js +52 -0
- package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
- package/build/plugin/ios/widgets/withIosWarning.js +10 -0
- package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
- package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
- package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
- package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
- package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
- package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
- package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
- package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
- package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
- package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
- package/build/plugin/ios/withIOSInvoke.js +57 -26
- package/build/plugin/ios/withIOSInvoke.js.map +1 -1
- package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
- package/build/plugin/ios/withWidgetExtension.js +5 -0
- package/build/plugin/ios/withWidgetExtension.js.map +1 -1
- package/build/plugin/src/withInvoke.d.ts +34 -23
- package/build/plugin/src/withInvoke.js +16 -27
- package/build/plugin/src/withInvoke.js.map +1 -1
- package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
- package/build/plugin/types/WidgetConfig.type.js +3 -0
- package/build/plugin/types/WidgetConfig.type.js.map +1 -0
- package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
- package/build/plugin/types/WidgetFamily.type.js +13 -0
- package/build/plugin/types/WidgetFamily.type.js.map +1 -0
- package/build/src/hooks/useLiveActivity.d.ts +20 -10
- package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
- package/build/src/hooks/useLiveActivity.js +20 -10
- package/build/src/hooks/useLiveActivity.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +5 -0
- package/build/src/index.js.map +1 -1
- package/build/src/module.d.ts +4 -0
- package/build/src/module.d.ts.map +1 -1
- package/build/src/module.js +8 -0
- package/build/src/module.js.map +1 -1
- package/build/src/web/index.d.ts +10 -1
- package/build/src/web/index.d.ts.map +1 -1
- package/build/src/web/index.js +4 -0
- package/build/src/web/index.js.map +1 -1
- package/build/src/widgets/ExpoWidgets.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.js +3 -0
- package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
- package/build/src/widgets/ExpoWidgets.js +42 -0
- package/build/src/widgets/ExpoWidgets.js.map +1 -0
- package/build/src/widgets/Widgets.d.ts +119 -0
- package/build/src/widgets/Widgets.d.ts.map +1 -0
- package/build/src/widgets/Widgets.js +158 -0
- package/build/src/widgets/Widgets.js.map +1 -0
- package/build/src/widgets/Widgets.types.d.ts +263 -0
- package/build/src/widgets/Widgets.types.d.ts.map +1 -0
- package/build/src/widgets/Widgets.types.js +2 -0
- package/build/src/widgets/Widgets.types.js.map +1 -0
- package/build/src/widgets/index.d.ts +3 -0
- package/build/src/widgets/index.d.ts.map +1 -0
- package/build/src/widgets/index.js +2 -0
- package/build/src/widgets/index.js.map +1 -0
- package/bundle/__tests__/decorator.test.ts +89 -0
- package/bundle/decorator.ts +61 -0
- package/bundle/expo-modules-core-stub.ts +1 -0
- package/bundle/expo-stub.ts +15 -0
- package/bundle/index.ts +69 -0
- package/bundle/jsx-runtime-stub.ts +71 -0
- package/bundle/react-native-stub.ts +3 -0
- package/bundle/react-stub.ts +13 -0
- package/expo-module.config.json +1 -1
- package/ios/LiveActivity/LiveActivityManager.swift +15 -14
- package/ios/Module/ExpoInvokeModule.swift +124 -0
- package/ios/Widget/WidgetManager.swift +25 -8
- package/ios/Widgets/LiveActivity.swift +81 -0
- package/ios/Widgets/LiveActivityFactory.swift +49 -0
- package/ios/Widgets/Widget/AppIntent.swift +130 -0
- package/ios/Widgets/Widget/Buttons.swift +62 -0
- package/ios/Widgets/Widget/DynamicView.swift +161 -0
- package/ios/Widgets/Widget/EntryView.swift +36 -0
- package/ios/Widgets/Widget/Fragment.swift +13 -0
- package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
- package/ios/Widgets/Widget/RedBoxView.swift +83 -0
- package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
- package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
- package/ios/Widgets/Widget/Type.swift +6 -0
- package/ios/Widgets/Widget/Utils.swift +116 -0
- package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
- package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
- package/ios/Widgets/WidgetObject.swift +31 -0
- package/ios/Widgets/WidgetsContext.swift +7 -0
- package/ios/Widgets/WidgetsEvents.swift +16 -0
- package/ios/Widgets/WidgetsExceptions.swift +25 -0
- package/ios/Widgets/WidgetsModule.swift +149 -0
- package/ios/Widgets/WidgetsRecords.swift +22 -0
- package/ios/Widgets/WidgetsStorage.swift +58 -0
- package/metro.widget.config.js +76 -0
- package/package.json +12 -2
- package/scripts/autolinking.rb +34 -0
- package/scripts/build-bundle.mjs +69 -0
- package/scripts/with-node.sh +44 -0
- package/scripts/xcode-build-bundle.sh +23 -0
- package/spm.config.json +40 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
func parseTimeline(identifier: String, name: String, family: WidgetFamily) -> [WidgetsTimelineEntry] {
|
|
6
|
+
guard let timeline = WidgetsStorage.getArray(forKey: "__expo_widgets_\(name)_timeline") else {
|
|
7
|
+
return [WidgetsTimelineEntry(date: Date(), name: name, props: nil, entryIndex: nil)]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let entries: [WidgetsTimelineEntry?] = timeline.enumerated().map { index, entry in
|
|
11
|
+
if let entry = entry as? [String: Any], let timestamp = entry["timestamp"] as? Int, let props = entry["props"] as? [String: Any] {
|
|
12
|
+
return WidgetsTimelineEntry(
|
|
13
|
+
date: Date(timeIntervalSince1970: Double(timestamp) / 1000),
|
|
14
|
+
name: name,
|
|
15
|
+
props: props,
|
|
16
|
+
entryIndex: index
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return entries.compactMap(\.self)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public func createRedBox(message: String, stack: String? = nil) -> [String: Any] {
|
|
26
|
+
var props: [String: Any] = ["message": message]
|
|
27
|
+
if let stack {
|
|
28
|
+
props["stack"] = stack
|
|
29
|
+
}
|
|
30
|
+
return ["type": "RedBoxView", "props": props]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public func evaluateLayout(
|
|
34
|
+
layout: String,
|
|
35
|
+
props: [String: Any]?,
|
|
36
|
+
environment: [String: Any]
|
|
37
|
+
) -> [String: Any] {
|
|
38
|
+
switch evaluateWidgetLayout(layout: layout, props: props, environment: environment) {
|
|
39
|
+
case .success(let result):
|
|
40
|
+
return result
|
|
41
|
+
case .failure(let error):
|
|
42
|
+
print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
|
|
43
|
+
return createRedBox(message: error.message)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func getLiveActivityNodes(forName name: String, props: String? = nil, environment: [String: Any]) -> [String: Any] {
|
|
48
|
+
let layout = WidgetsStorage.getString(forKey: "__expo_widgets_live_activity_\(name)_layout") ?? ""
|
|
49
|
+
let propsDict = props.flatMap { props in
|
|
50
|
+
props.data(using: .utf8).flatMap {
|
|
51
|
+
try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch evaluateWidgetLayout(layout: layout, props: propsDict, environment: environment) {
|
|
56
|
+
case .success(let result):
|
|
57
|
+
return result
|
|
58
|
+
case .failure(let error):
|
|
59
|
+
print("[ExpoWidgets] Layout evaluation failed: \(error.message)")
|
|
60
|
+
return ["banner": createRedBox(message: error.message)]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func getLiveActivityUrl(forName name: String) -> URL? {
|
|
65
|
+
guard let urlString = WidgetsStorage.getString(forKey: "__expo_widgets_live_activity_\(name)_url") else {
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
return URL(string: urlString)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public func getWidgetEnvironment(environment: EnvironmentValues) -> [String: Any] {
|
|
72
|
+
var env: [String: Any] = [
|
|
73
|
+
"showsContainerBackground": environment.showsWidgetContainerBackground,
|
|
74
|
+
"widgetFamily": environment.widgetFamily.description,
|
|
75
|
+
"colorScheme": "\(environment.colorScheme)"
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if #available(iOS 16.0, *) {
|
|
79
|
+
env["isLuminanceReduced"] = environment.isLuminanceReduced
|
|
80
|
+
env["widgetRenderingMode"] = environment.widgetRenderingMode.description
|
|
81
|
+
env["showsWidgetLabel"] = environment.showsWidgetLabel
|
|
82
|
+
}
|
|
83
|
+
if #available(iOS 17.0, *) {
|
|
84
|
+
env["widgetContentMargins"] = [
|
|
85
|
+
"top": environment.widgetContentMargins.top,
|
|
86
|
+
"bottom": environment.widgetContentMargins.bottom,
|
|
87
|
+
"leading": environment.widgetContentMargins.leading,
|
|
88
|
+
"trailing": environment.widgetContentMargins.trailing,
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
if #available(iOS 26.0, *) {
|
|
92
|
+
env["levelOfDetail"] = environment.levelOfDetail == .simplified ? "simplified" : environment.levelOfDetail == .default ? "default" : nil
|
|
93
|
+
}
|
|
94
|
+
return env
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func getLiveActivityEnvironment(environment: EnvironmentValues) -> [String: Any] {
|
|
98
|
+
var env: [String: Any] = [
|
|
99
|
+
"colorScheme": "\(environment.colorScheme)"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if #available(iOS 16.0, *) {
|
|
103
|
+
env["isLuminanceReduced"] = environment.isLuminanceReduced
|
|
104
|
+
}
|
|
105
|
+
if #available(iOS 16.1, *) {
|
|
106
|
+
env["isActivityFullscreen"] = environment.isActivityFullscreen
|
|
107
|
+
}
|
|
108
|
+
if #available(iOS 18.0, *) {
|
|
109
|
+
env["isActivityUpdateReduced"] = environment.isActivityUpdateReduced
|
|
110
|
+
env["activityFamily"] = "\(environment.activityFamily)"
|
|
111
|
+
}
|
|
112
|
+
if #available(iOS 26.0, *) {
|
|
113
|
+
env["levelOfDetail"] = environment.levelOfDetail == .simplified ? "simplified" : environment.levelOfDetail == .default ? "default" : nil
|
|
114
|
+
}
|
|
115
|
+
return env
|
|
116
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WidgetKit
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import ActivityKit
|
|
5
|
+
|
|
6
|
+
struct LiveActivityAttributes: ActivityAttributes {
|
|
7
|
+
public struct ContentState: Codable, Hashable {
|
|
8
|
+
var name: String
|
|
9
|
+
var props: String?
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@available(iOS 16.1, *)
|
|
14
|
+
public struct WidgetLiveActivity: Widget {
|
|
15
|
+
@Environment(\.self) var env
|
|
16
|
+
|
|
17
|
+
let widgetContext: AppContext = AppContext()
|
|
18
|
+
|
|
19
|
+
var environment: [String: Any] {
|
|
20
|
+
return getLiveActivityEnvironment(environment: env)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public init() {}
|
|
24
|
+
|
|
25
|
+
public var body: some WidgetConfiguration {
|
|
26
|
+
ActivityConfiguration(for: LiveActivityAttributes.self) { context in
|
|
27
|
+
let nodes = getLiveActivityNodes(
|
|
28
|
+
forName: context.state.name,
|
|
29
|
+
props: context.state.props,
|
|
30
|
+
environment: environment
|
|
31
|
+
)
|
|
32
|
+
LiveActivityBannerView(context: context, nodes: nodes)
|
|
33
|
+
} dynamicIsland: { context in
|
|
34
|
+
let nodes = getLiveActivityNodes(
|
|
35
|
+
forName: context.state.name,
|
|
36
|
+
props: context.state.props,
|
|
37
|
+
environment: environment
|
|
38
|
+
)
|
|
39
|
+
return DynamicIsland {
|
|
40
|
+
DynamicIslandExpandedRegion(.center) {
|
|
41
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedCenter")
|
|
42
|
+
}
|
|
43
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
44
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedLeading")
|
|
45
|
+
}
|
|
46
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
47
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedTrailing")
|
|
48
|
+
}
|
|
49
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
50
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "expandedBottom")
|
|
51
|
+
}
|
|
52
|
+
} compactLeading: {
|
|
53
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactLeading")
|
|
54
|
+
} compactTrailing: {
|
|
55
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "compactTrailing")
|
|
56
|
+
} minimal: {
|
|
57
|
+
LiveActivitySectionView(context: context, nodes: nodes, sectionName: "minimal")
|
|
58
|
+
}
|
|
59
|
+
.widgetURL(getLiveActivityUrl(forName: context.state.name))
|
|
60
|
+
}
|
|
61
|
+
.supplementalActivityFamiliesIfAvailable()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@available(iOS 16.1, *)
|
|
66
|
+
private struct LiveActivitySectionView: View {
|
|
67
|
+
let context: ActivityViewContext<LiveActivityAttributes>
|
|
68
|
+
let nodes: [String: Any]
|
|
69
|
+
let sectionName: String
|
|
70
|
+
|
|
71
|
+
var body: some View {
|
|
72
|
+
if let node = nodes[sectionName] as? [String: Any] {
|
|
73
|
+
WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
|
|
74
|
+
} else {
|
|
75
|
+
EmptyView()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@available(iOS 16.1, *)
|
|
81
|
+
private struct LiveActivityBannerView: View {
|
|
82
|
+
var context: ActivityViewContext<LiveActivityAttributes>
|
|
83
|
+
let nodes: [String: Any]
|
|
84
|
+
|
|
85
|
+
var body: some View {
|
|
86
|
+
if #available(iOS 18.0, *) {
|
|
87
|
+
LiveActivityBanner(context: context, nodes: nodes)
|
|
88
|
+
} else if let node = nodes["banner"] as? [String: Any] {
|
|
89
|
+
WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
|
|
90
|
+
} else {
|
|
91
|
+
EmptyView()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
extension WidgetConfiguration {
|
|
97
|
+
func supplementalActivityFamiliesIfAvailable() -> some WidgetConfiguration {
|
|
98
|
+
if #available(iOS 18.0, iOSApplicationExtension 18.0, *) {
|
|
99
|
+
return self.supplementalActivityFamilies([.small, .medium])
|
|
100
|
+
} else {
|
|
101
|
+
return self
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import JavaScriptCore
|
|
3
|
+
|
|
4
|
+
struct WidgetJavaScriptError: Error {
|
|
5
|
+
let message: String
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
typealias WidgetJavaScriptResult<Value> = Result<Value, WidgetJavaScriptError>
|
|
9
|
+
|
|
10
|
+
private final class WidgetsJSRuntime {
|
|
11
|
+
static let shared = WidgetsJSRuntime()
|
|
12
|
+
|
|
13
|
+
private let lock = NSRecursiveLock()
|
|
14
|
+
private var context: JSContext?
|
|
15
|
+
private var bundleScript: String?
|
|
16
|
+
private var layoutCache: [String: JSValue] = [:]
|
|
17
|
+
|
|
18
|
+
private init() {}
|
|
19
|
+
|
|
20
|
+
func render(layout: String, props: [String: Any]?, environment: [String: Any]) -> WidgetJavaScriptResult<[String: Any]> {
|
|
21
|
+
call(layout: layout, functionName: "__expoWidgetRender") { context in
|
|
22
|
+
[props.map { $0 } ?? JSValue(undefinedIn: context) as Any, environment]
|
|
23
|
+
}.flatMap { result in
|
|
24
|
+
guard let renderedNode = result?.toObject() as? [String: Any] else {
|
|
25
|
+
return .failure(WidgetJavaScriptError(message: "Expo widget render did not produce any results."))
|
|
26
|
+
}
|
|
27
|
+
return .success(renderedNode)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func handlePress(layout: String, props: [String: Any], environment: [String: Any]) -> WidgetJavaScriptResult<[String: Any]?> {
|
|
32
|
+
call(layout: layout, functionName: "__expoWidgetHandlePress") { _ in [props, environment] }
|
|
33
|
+
.map { $0?.toObject() as? [String: Any] }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func call(
|
|
37
|
+
layout: String,
|
|
38
|
+
functionName: String,
|
|
39
|
+
arguments: (JSContext) -> [Any]
|
|
40
|
+
) -> WidgetJavaScriptResult<JSValue?> {
|
|
41
|
+
lock.lock()
|
|
42
|
+
defer { lock.unlock() }
|
|
43
|
+
|
|
44
|
+
guard let context = getContext() else {
|
|
45
|
+
return .failure(WidgetJavaScriptError(message: "Could not create context for layout evaluation."))
|
|
46
|
+
}
|
|
47
|
+
guard let layoutValue = getLayoutValue(layout, in: context) else {
|
|
48
|
+
return .failure(WidgetJavaScriptError(message: contextExceptionMessage(context) ?? "Could not evaluate layout."))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
context.exception = nil
|
|
52
|
+
context.setObject(layoutValue, forKeyedSubscript: "__expoWidgetLayout" as NSString)
|
|
53
|
+
|
|
54
|
+
let function = context.objectForKeyedSubscript(functionName)
|
|
55
|
+
guard let function, function.isObject else {
|
|
56
|
+
return .failure(WidgetJavaScriptError(message: "Expo widget runtime function \(functionName) is unavailable."))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let result = function.call(withArguments: arguments(context))
|
|
60
|
+
if let exceptionMessage = contextExceptionMessage(context) {
|
|
61
|
+
return .failure(WidgetJavaScriptError(message: exceptionMessage))
|
|
62
|
+
}
|
|
63
|
+
return .success(result)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private func getContext() -> JSContext? {
|
|
67
|
+
if let context {
|
|
68
|
+
return context
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
guard let context = JSContext() else {
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
guard let script = getBundleScript() else {
|
|
76
|
+
print("[ExpoWidgets] Missing ExpoWidgets.bundle")
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
context.evaluateScript(script)
|
|
81
|
+
if let exceptionMessage = contextExceptionMessage(context) {
|
|
82
|
+
print("[ExpoWidgets] Bundle evaluation failed: \(exceptionMessage)")
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
self.context = context
|
|
87
|
+
return context
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func getBundleScript() -> String? {
|
|
91
|
+
if let bundleScript {
|
|
92
|
+
return bundleScript
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
guard let bundleURL = Bundle.main.url(forResource: "ExpoWidgets", withExtension: "bundle"),
|
|
96
|
+
let bundle = Bundle(url: bundleURL),
|
|
97
|
+
let url = bundle.url(forResource: "ExpoWidgets", withExtension: "bundle"),
|
|
98
|
+
let script = try? String(contentsOf: url, encoding: .utf8) else {
|
|
99
|
+
return nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
bundleScript = script
|
|
103
|
+
return script
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private func getLayoutValue(_ layout: String, in context: JSContext) -> JSValue? {
|
|
107
|
+
if let layoutValue = layoutCache[layout] {
|
|
108
|
+
return layoutValue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
context.exception = nil
|
|
112
|
+
guard let layoutValue = context.evaluateScript("(\(layout))"),
|
|
113
|
+
!layoutValue.isUndefined else {
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
guard context.exception == nil else {
|
|
117
|
+
return nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
layoutCache[layout] = layoutValue
|
|
121
|
+
return layoutValue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func contextExceptionMessage(_ context: JSContext) -> String? {
|
|
125
|
+
guard let exception = context.exception else {
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
context.exception = nil
|
|
130
|
+
return exception.toString() ?? "Unknown JavaScript exception."
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func evaluateWidgetLayout(
|
|
135
|
+
layout: String,
|
|
136
|
+
props: [String: Any]?,
|
|
137
|
+
environment: [String: Any]
|
|
138
|
+
) -> WidgetJavaScriptResult<[String: Any]> {
|
|
139
|
+
WidgetsJSRuntime.shared.render(layout: layout, props: props, environment: environment)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func evaluateWidgetButtonPress(
|
|
143
|
+
layout: String,
|
|
144
|
+
props: [String: Any],
|
|
145
|
+
environment: [String: Any]
|
|
146
|
+
) -> WidgetJavaScriptResult<[String: Any]?> {
|
|
147
|
+
WidgetsJSRuntime.shared.handlePress(layout: layout, props: props, environment: environment)
|
|
148
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
final class WidgetObject: SharedObject {
|
|
5
|
+
let name: String
|
|
6
|
+
init(name: String, layout: String) {
|
|
7
|
+
self.name = name
|
|
8
|
+
WidgetsStorage.set(layout, forKey: "__expo_widgets_\(name)_layout")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func reload() {
|
|
12
|
+
WidgetCenter.shared.reloadTimelines(ofKind: name)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func updateTimeline(entries: [WidgetsJSTimelineEntry]) throws {
|
|
16
|
+
if WidgetsStorage.getString(forKey: "__expo_widgets_\(name)_layout") == nil {
|
|
17
|
+
throw UpdatedTimelineWithoutLayout(name)
|
|
18
|
+
}
|
|
19
|
+
WidgetsStorage.set(entries.map { $0.toDictionary() }, forKey: "__expo_widgets_\(name)_timeline")
|
|
20
|
+
|
|
21
|
+
self.reload()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func getTimeline() throws -> [WidgetsJSTimelineEntry] {
|
|
25
|
+
guard let entries = WidgetsStorage.getArray(forKey: "__expo_widgets_\(name)_timeline") as? [[String: Any]],
|
|
26
|
+
let appContext else {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
29
|
+
return try entries.map { try WidgetsJSTimelineEntry(from: $0, appContext: appContext) }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
public enum WidgetsEventType: String {
|
|
2
|
+
case userEvent = "ExpoWidgetsUserInteraction"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
public class WidgetsEvents {
|
|
6
|
+
public static let shared = WidgetsEvents()
|
|
7
|
+
|
|
8
|
+
private init() {}
|
|
9
|
+
|
|
10
|
+
public func sendNotification(type: WidgetsEventType, data: [String: Any]) {
|
|
11
|
+
var eventData = data
|
|
12
|
+
eventData["type"] = type.rawValue
|
|
13
|
+
let userInfo = ["eventData": eventData] as [String: Any]
|
|
14
|
+
NotificationCenter.default.post(name: onUserInteractionNotification, object: nil, userInfo: userInfo)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
internal final class LiveActivitiesNotSupportedException: Exception, @unchecked Sendable {
|
|
4
|
+
override var reason: String {
|
|
5
|
+
"Live Activities are not supported on this device"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
internal final class StartLiveActivityException: GenericException<String>, @unchecked Sendable {
|
|
10
|
+
override var reason: String {
|
|
11
|
+
"Failed to start live activity: \(param)"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
internal final class LiveActivityNotFoundException: GenericException<String>, @unchecked Sendable {
|
|
16
|
+
override var reason: String {
|
|
17
|
+
"Can't find live activity with id: \(param)"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
internal final class UpdatedTimelineWithoutLayout: GenericException<String>, @unchecked Sendable {
|
|
22
|
+
override var reason: String {
|
|
23
|
+
"Cannot update widget timeline without a layout, first register a layout for: \(param)"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import ActivityKit
|
|
3
|
+
import WidgetKit
|
|
4
|
+
|
|
5
|
+
let pushNotificationsEnabledKey: String = "ExpoWidgets_EnablePushNotifications"
|
|
6
|
+
|
|
7
|
+
let onUserInteraction = "onExpoWidgetsUserInteraction"
|
|
8
|
+
let onPushToStartTokenReceived = "onExpoWidgetsPushToStartTokenReceived"
|
|
9
|
+
let onTokenReceived = "onExpoWidgetsTokenReceived"
|
|
10
|
+
let onUserInteractionNotification = Notification.Name(onUserInteraction)
|
|
11
|
+
|
|
12
|
+
public final class WidgetsModule: Module {
|
|
13
|
+
var pushToStartTokenObserverTask: Task<Void, Never>?
|
|
14
|
+
|
|
15
|
+
public func definition() -> ModuleDefinition {
|
|
16
|
+
Name("ExpoWidgets")
|
|
17
|
+
|
|
18
|
+
Events(onPushToStartTokenReceived, onTokenReceived, onUserInteraction)
|
|
19
|
+
|
|
20
|
+
OnStartObserving(onUserInteraction) {
|
|
21
|
+
NotificationCenter.default.addObserver(
|
|
22
|
+
self,
|
|
23
|
+
selector: #selector(handleUserInteractionNotification),
|
|
24
|
+
name: onUserInteractionNotification,
|
|
25
|
+
object: nil
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
OnStopObserving(onUserInteraction) {
|
|
30
|
+
NotificationCenter.default.removeObserver(
|
|
31
|
+
self,
|
|
32
|
+
name: onUserInteractionNotification,
|
|
33
|
+
object: nil
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
OnStartObserving(onPushToStartTokenReceived) {
|
|
38
|
+
if pushNotificationsEnabled {
|
|
39
|
+
observePushToStartToken()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
OnStopObserving(onPushToStartTokenReceived) {
|
|
44
|
+
pushToStartTokenObserverTask?.cancel()
|
|
45
|
+
pushToStartTokenObserverTask = nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Constant("widgetsDirectory") { () -> String? in
|
|
49
|
+
guard let appGroupIdentifier = WidgetsStorage.appGroupIdentifier,
|
|
50
|
+
let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
let directoryUrl = containerUrl.appendingPathComponent("ExpoWidgets", isDirectory: true)
|
|
54
|
+
do {
|
|
55
|
+
try FileManager.default.createDirectory(at: directoryUrl, withIntermediateDirectories: true)
|
|
56
|
+
return directoryUrl.absoluteString
|
|
57
|
+
} catch {
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Function("reloadAllWidgets") {
|
|
63
|
+
WidgetCenter.shared.reloadAllTimelines()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Class("Widget", WidgetObject.self) {
|
|
67
|
+
Constructor { (name: String, layout: String) in
|
|
68
|
+
WidgetObject(name: name, layout: layout)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Function("reload") { (widget: WidgetObject) in
|
|
72
|
+
widget.reload()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Function("updateTimeline") { (widget: WidgetObject, entries: [WidgetsJSTimelineEntry]) in
|
|
76
|
+
try widget.updateTimeline(entries: entries)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Function("getTimeline") { (widget: WidgetObject) in
|
|
80
|
+
try widget.getTimeline()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Class("LiveActivityFactory", LiveActivityFactory.self) {
|
|
85
|
+
Constructor { (name: String, layout: String) in
|
|
86
|
+
LiveActivityFactory(name: name, layout: layout)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Function("start") { (liveActivity: LiveActivityFactory, props: String?, url: URL?) in
|
|
90
|
+
try liveActivity.start(props: props, url: url)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Function("getInstances") { (liveActivity: LiveActivityFactory) in
|
|
94
|
+
try liveActivity.getInstances()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Class("LiveActivity", LiveActivity.self) {
|
|
99
|
+
AsyncFunction("update") { (instance: LiveActivity, props: String?) in
|
|
100
|
+
try await instance.update(props: props)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
AsyncFunction("end") { (instance: LiveActivity, dismissalPolicy: LiveActivityDismissalPolicy?, afterDate: Date?, props: String?, contentDate: Date?) in
|
|
104
|
+
try await instance.end(dismissalPolicy: dismissalPolicy, afterDate: afterDate, props: props, contentDate: contentDate)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
AsyncFunction("getPushToken") { (instance: LiveActivity) in
|
|
108
|
+
try instance.getPushToken()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@objc func handleUserInteractionNotification(_ notification: Notification) {
|
|
114
|
+
guard let userInfo = notification.userInfo as? [String: Any],
|
|
115
|
+
let eventData = userInfo["eventData"] as? [String: Any]
|
|
116
|
+
else { return }
|
|
117
|
+
self.sendEvent(onUserInteraction, eventData)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func sendPushToStartToken(activityPushToStartToken: String) {
|
|
121
|
+
sendEvent(
|
|
122
|
+
onPushToStartTokenReceived,
|
|
123
|
+
[
|
|
124
|
+
"activityPushToStartToken": activityPushToStartToken
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func observePushToStartToken() {
|
|
130
|
+
guard #available(iOS 17.2, *), ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
131
|
+
pushToStartTokenObserverTask = Task {
|
|
132
|
+
let initialToken = (Activity<LiveActivityAttributes>.pushToStartToken?.reduce("") { $0 + String(format: "%02x", $1) })
|
|
133
|
+
if let initialToken {
|
|
134
|
+
sendPushToStartToken(activityPushToStartToken: initialToken)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for await data in Activity<LiveActivityAttributes>.pushToStartTokenUpdates {
|
|
138
|
+
let token = data.reduce("") { $0 + String(format: "%02x", $1) }
|
|
139
|
+
if token != initialToken {
|
|
140
|
+
sendPushToStartToken(activityPushToStartToken: token)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private var pushNotificationsEnabled: Bool {
|
|
147
|
+
Bundle.main.object(forInfoDictionaryKey: pushNotificationsEnabledKey) as? Bool ?? false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import ActivityKit
|
|
3
|
+
|
|
4
|
+
struct WidgetsJSTimelineEntry: Record {
|
|
5
|
+
@Field var timestamp: Int
|
|
6
|
+
@Field var props: [String: Any] = [:]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
internal enum LiveActivityDismissalPolicy: String, Enumerable {
|
|
10
|
+
case `default`
|
|
11
|
+
case immediate
|
|
12
|
+
case after
|
|
13
|
+
|
|
14
|
+
@available(iOS 16.1, *)
|
|
15
|
+
internal func toDismissalPolicy(date: Date?) -> ActivityUIDismissalPolicy {
|
|
16
|
+
return switch self {
|
|
17
|
+
case .default: .default
|
|
18
|
+
case .immediate: .immediate
|
|
19
|
+
case .after: .after(date ?? Date.now)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
public enum WidgetsStorage {
|
|
2
|
+
public static var appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String
|
|
3
|
+
static let defaults = UserDefaults(suiteName: appGroupIdentifier)
|
|
4
|
+
|
|
5
|
+
static func set(_ value: [String: Any], forKey key: String) {
|
|
6
|
+
guard let defaults else { return }
|
|
7
|
+
|
|
8
|
+
defaults.set(value, forKey: key)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static func set(_ value: [[String: Any]], forKey key: String) {
|
|
12
|
+
guard let defaults else { return }
|
|
13
|
+
|
|
14
|
+
defaults.set(value, forKey: key)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static func set(_ value: String, forKey key: String) {
|
|
18
|
+
guard let defaults else { return }
|
|
19
|
+
|
|
20
|
+
defaults.set(value, forKey: key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func set(_ value: Data, forKey key: String) {
|
|
24
|
+
guard let defaults else { return }
|
|
25
|
+
|
|
26
|
+
defaults.set(value, forKey: key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static func getDictionary(forKey key: String) -> [String: Any]? {
|
|
30
|
+
guard let defaults else { return nil }
|
|
31
|
+
|
|
32
|
+
return defaults.dictionary(forKey: key)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static func getArray(forKey key: String) -> [Any]? {
|
|
36
|
+
guard let defaults else { return nil }
|
|
37
|
+
|
|
38
|
+
return defaults.array(forKey: key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static func getData(forKey key: String) -> Data? {
|
|
42
|
+
guard let defaults else { return nil }
|
|
43
|
+
|
|
44
|
+
return defaults.data(forKey: key)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static func getString(forKey key: String) -> String? {
|
|
48
|
+
guard let defaults else { return nil }
|
|
49
|
+
|
|
50
|
+
return defaults.string(forKey: key)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static func removeObject(forKey key: String) {
|
|
54
|
+
guard let defaults else { return }
|
|
55
|
+
|
|
56
|
+
defaults.removeObject(forKey: key)
|
|
57
|
+
}
|
|
58
|
+
}
|