expo-invoke 1.0.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 (144) hide show
  1. package/MIGRATION.md +125 -0
  2. package/README.md +942 -0
  3. package/SECURITY.md +35 -0
  4. package/android/build.gradle +42 -0
  5. package/android/src/main/java/expo/modules/invoke/AppActionsHandler.kt +74 -0
  6. package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +135 -0
  7. package/android/src/main/java/expo/modules/invoke/QuickTileService.kt +63 -0
  8. package/android/src/main/java/expo/modules/invoke/ShortcutsHelper.kt +72 -0
  9. package/build/cli/commands/generate.d.ts +1 -0
  10. package/build/cli/commands/generate.js +119 -0
  11. package/build/cli/commands/generate.js.map +1 -0
  12. package/build/cli/commands/test.d.ts +1 -0
  13. package/build/cli/commands/test.js +53 -0
  14. package/build/cli/commands/test.js.map +1 -0
  15. package/build/cli/commands/validate.d.ts +1 -0
  16. package/build/cli/commands/validate.js +116 -0
  17. package/build/cli/commands/validate.js.map +1 -0
  18. package/build/cli/index.d.ts +2 -0
  19. package/build/cli/index.js +37 -0
  20. package/build/cli/index.js.map +1 -0
  21. package/build/plugin/android/withAndroidInvoke.d.ts +8 -0
  22. package/build/plugin/android/withAndroidInvoke.js +106 -0
  23. package/build/plugin/android/withAndroidInvoke.js.map +1 -0
  24. package/build/plugin/codegen/generateActionsXml.d.ts +2 -0
  25. package/build/plugin/codegen/generateActionsXml.js +69 -0
  26. package/build/plugin/codegen/generateActionsXml.js.map +1 -0
  27. package/build/plugin/codegen/generateShortcutsXml.d.ts +2 -0
  28. package/build/plugin/codegen/generateShortcutsXml.js +45 -0
  29. package/build/plugin/codegen/generateShortcutsXml.js.map +1 -0
  30. package/build/plugin/codegen/generateSwiftIntents.d.ts +2 -0
  31. package/build/plugin/codegen/generateSwiftIntents.js +129 -0
  32. package/build/plugin/codegen/generateSwiftIntents.js.map +1 -0
  33. package/build/plugin/ios/withIOSInvoke.d.ts +9 -0
  34. package/build/plugin/ios/withIOSInvoke.js +83 -0
  35. package/build/plugin/ios/withIOSInvoke.js.map +1 -0
  36. package/build/plugin/src/withInvoke.d.ts +16 -0
  37. package/build/plugin/src/withInvoke.js +13 -0
  38. package/build/plugin/src/withInvoke.js.map +1 -0
  39. package/build/src/components/AddToSiriButton.d.ts +18 -0
  40. package/build/src/components/AddToSiriButton.d.ts.map +1 -0
  41. package/build/src/components/AddToSiriButton.js +35 -0
  42. package/build/src/components/AddToSiriButton.js.map +1 -0
  43. package/build/src/components/InvokeDebugger.d.ts +12 -0
  44. package/build/src/components/InvokeDebugger.d.ts.map +1 -0
  45. package/build/src/components/InvokeDebugger.js +122 -0
  46. package/build/src/components/InvokeDebugger.js.map +1 -0
  47. package/build/src/components/ShortcutChip.d.ts +20 -0
  48. package/build/src/components/ShortcutChip.d.ts.map +1 -0
  49. package/build/src/components/ShortcutChip.js +44 -0
  50. package/build/src/components/ShortcutChip.js.map +1 -0
  51. package/build/src/hooks/useAddToSiri.d.ts +9 -0
  52. package/build/src/hooks/useAddToSiri.d.ts.map +1 -0
  53. package/build/src/hooks/useAddToSiri.js +15 -0
  54. package/build/src/hooks/useAddToSiri.js.map +1 -0
  55. package/build/src/hooks/useDonation.d.ts +26 -0
  56. package/build/src/hooks/useDonation.d.ts.map +1 -0
  57. package/build/src/hooks/useDonation.js +21 -0
  58. package/build/src/hooks/useDonation.js.map +1 -0
  59. package/build/src/hooks/useFocusFilter.d.ts +11 -0
  60. package/build/src/hooks/useFocusFilter.d.ts.map +1 -0
  61. package/build/src/hooks/useFocusFilter.js +24 -0
  62. package/build/src/hooks/useFocusFilter.js.map +1 -0
  63. package/build/src/hooks/useHandoff.d.ts +29 -0
  64. package/build/src/hooks/useHandoff.d.ts.map +1 -0
  65. package/build/src/hooks/useHandoff.js +27 -0
  66. package/build/src/hooks/useHandoff.js.map +1 -0
  67. package/build/src/hooks/useInvoke.d.ts +43 -0
  68. package/build/src/hooks/useInvoke.d.ts.map +1 -0
  69. package/build/src/hooks/useInvoke.js +70 -0
  70. package/build/src/hooks/useInvoke.js.map +1 -0
  71. package/build/src/hooks/useInvokeAnalytics.d.ts +23 -0
  72. package/build/src/hooks/useInvokeAnalytics.d.ts.map +1 -0
  73. package/build/src/hooks/useInvokeAnalytics.js +27 -0
  74. package/build/src/hooks/useInvokeAnalytics.js.map +1 -0
  75. package/build/src/hooks/useInvokeHistory.d.ts +11 -0
  76. package/build/src/hooks/useInvokeHistory.d.ts.map +1 -0
  77. package/build/src/hooks/useInvokeHistory.js +26 -0
  78. package/build/src/hooks/useInvokeHistory.js.map +1 -0
  79. package/build/src/hooks/useInvokeInit.d.ts +16 -0
  80. package/build/src/hooks/useInvokeInit.d.ts.map +1 -0
  81. package/build/src/hooks/useInvokeInit.js +32 -0
  82. package/build/src/hooks/useInvokeInit.js.map +1 -0
  83. package/build/src/hooks/useLiveActivityIntent.d.ts +21 -0
  84. package/build/src/hooks/useLiveActivityIntent.d.ts.map +1 -0
  85. package/build/src/hooks/useLiveActivityIntent.js +9 -0
  86. package/build/src/hooks/useLiveActivityIntent.js.map +1 -0
  87. package/build/src/hooks/useQuickTile.d.ts +23 -0
  88. package/build/src/hooks/useQuickTile.d.ts.map +1 -0
  89. package/build/src/hooks/useQuickTile.js +15 -0
  90. package/build/src/hooks/useQuickTile.js.map +1 -0
  91. package/build/src/hooks/useShortcuts.d.ts +25 -0
  92. package/build/src/hooks/useShortcuts.d.ts.map +1 -0
  93. package/build/src/hooks/useShortcuts.js +23 -0
  94. package/build/src/hooks/useShortcuts.js.map +1 -0
  95. package/build/src/hooks/useSpotlight.d.ts +23 -0
  96. package/build/src/hooks/useSpotlight.d.ts.map +1 -0
  97. package/build/src/hooks/useSpotlight.js +13 -0
  98. package/build/src/hooks/useSpotlight.js.map +1 -0
  99. package/build/src/hooks/useWidgetData.d.ts +20 -0
  100. package/build/src/hooks/useWidgetData.d.ts.map +1 -0
  101. package/build/src/hooks/useWidgetData.js +12 -0
  102. package/build/src/hooks/useWidgetData.js.map +1 -0
  103. package/build/src/index.d.ts +32 -0
  104. package/build/src/index.d.ts.map +1 -0
  105. package/build/src/index.js +27 -0
  106. package/build/src/index.js.map +1 -0
  107. package/build/src/module.d.ts +25 -0
  108. package/build/src/module.d.ts.map +1 -0
  109. package/build/src/module.js +102 -0
  110. package/build/src/module.js.map +1 -0
  111. package/build/src/types/index.d.ts +156 -0
  112. package/build/src/types/index.d.ts.map +1 -0
  113. package/build/src/types/index.js +4 -0
  114. package/build/src/types/index.js.map +1 -0
  115. package/build/src/utils/actionStore.d.ts +6 -0
  116. package/build/src/utils/actionStore.d.ts.map +1 -0
  117. package/build/src/utils/actionStore.js +32 -0
  118. package/build/src/utils/actionStore.js.map +1 -0
  119. package/build/src/utils/analytics.d.ts +12 -0
  120. package/build/src/utils/analytics.d.ts.map +1 -0
  121. package/build/src/utils/analytics.js +29 -0
  122. package/build/src/utils/analytics.js.map +1 -0
  123. package/build/src/utils/testing.d.ts +39 -0
  124. package/build/src/utils/testing.d.ts.map +1 -0
  125. package/build/src/utils/testing.js +58 -0
  126. package/build/src/utils/testing.js.map +1 -0
  127. package/build/src/utils/validation.d.ts +11 -0
  128. package/build/src/utils/validation.d.ts.map +1 -0
  129. package/build/src/utils/validation.js +128 -0
  130. package/build/src/utils/validation.js.map +1 -0
  131. package/expo-module.config.json +9 -0
  132. package/ios/Focus/FocusFilterManager.swift +19 -0
  133. package/ios/Intents/BaseInvokeIntent.swift +42 -0
  134. package/ios/Intents/HandoffManager.swift +49 -0
  135. package/ios/Intents/InvokeActionStore.swift +21 -0
  136. package/ios/Intents/InvokeParameterTypes.swift +142 -0
  137. package/ios/LiveActivity/LiveActivityIntentBridge.swift +44 -0
  138. package/ios/Module/ExpoInvokeModule.swift +170 -0
  139. package/ios/Notifications/NotificationActionHandler.swift +80 -0
  140. package/ios/Shortcuts/DonationManager.swift +44 -0
  141. package/ios/Shortcuts/ShortcutsManager.swift +32 -0
  142. package/ios/Shortcuts/SpotlightManager.swift +48 -0
  143. package/ios/Widget/WidgetManager.swift +19 -0
  144. package/package.json +111 -0
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateConfig = validateConfig;
4
+ const VALID_IDENTIFIER = /^[a-z][a-z0-9_]*$/i;
5
+ const MAX_PHRASE_LENGTH = 140;
6
+ const MAX_IOS_SHORTCUTS = 10;
7
+ const MAX_ANDROID_SHORTCUTS = 5;
8
+ function validatePhrase(phrase, intentId) {
9
+ const errs = [];
10
+ if (phrase.length > MAX_PHRASE_LENGTH) {
11
+ errs.push({
12
+ field: `intents[${intentId}].phrases`,
13
+ message: `Phrase "${phrase.slice(0, 40)}..." exceeds ${MAX_PHRASE_LENGTH} characters.`,
14
+ });
15
+ }
16
+ if (!phrase.includes('${applicationName}')) {
17
+ errs.push({
18
+ field: `intents[${intentId}].phrases`,
19
+ message: `Phrase "${phrase.slice(0, 60)}" should include \${applicationName} for Siri to locate the app.`,
20
+ });
21
+ }
22
+ return errs;
23
+ }
24
+ function validateIntent(intent) {
25
+ var _a, _b;
26
+ const errs = [];
27
+ if (!VALID_IDENTIFIER.test(intent.id)) {
28
+ errs.push({
29
+ field: `intents[${intent.id}].id`,
30
+ message: `Intent id "${intent.id}" must be a valid identifier (letters, numbers, underscores).`,
31
+ });
32
+ }
33
+ if (!intent.phrases || intent.phrases.length === 0) {
34
+ errs.push({
35
+ field: `intents[${intent.id}].phrases`,
36
+ message: `Intent "${intent.id}" must have at least one voice phrase.`,
37
+ });
38
+ }
39
+ for (const phrase of (_a = intent.phrases) !== null && _a !== void 0 ? _a : []) {
40
+ errs.push(...validatePhrase(phrase, intent.id));
41
+ }
42
+ const params = (_b = intent.parameters) !== null && _b !== void 0 ? _b : [];
43
+ const paramNames = new Set();
44
+ for (const param of params) {
45
+ if (paramNames.has(param.name)) {
46
+ errs.push({
47
+ field: `intents[${intent.id}].parameters`,
48
+ message: `Duplicate parameter name "${param.name}" in intent "${intent.id}".`,
49
+ });
50
+ }
51
+ paramNames.add(param.name);
52
+ if (!VALID_IDENTIFIER.test(param.name)) {
53
+ errs.push({
54
+ field: `intents[${intent.id}].parameters[${param.name}]`,
55
+ message: `Parameter name "${param.name}" must be a valid identifier.`,
56
+ });
57
+ }
58
+ if (param.type === 'enum') {
59
+ const optionIds = new Set();
60
+ for (const opt of param.options) {
61
+ if (optionIds.has(opt.id)) {
62
+ errs.push({
63
+ field: `intents[${intent.id}].parameters[${param.name}].options`,
64
+ message: `Duplicate enum option id "${opt.id}".`,
65
+ });
66
+ }
67
+ optionIds.add(opt.id);
68
+ }
69
+ }
70
+ }
71
+ return errs;
72
+ }
73
+ function validateConfig(config) {
74
+ var _a;
75
+ const errors = [];
76
+ const warnings = [];
77
+ if (!config.intents || config.intents.length === 0) {
78
+ errors.push({ field: 'intents', message: 'At least one intent is required.' });
79
+ return { valid: false, errors, warnings };
80
+ }
81
+ // Check for duplicate intent IDs
82
+ const intentIds = new Set();
83
+ for (const intent of config.intents) {
84
+ if (intentIds.has(intent.id)) {
85
+ errors.push({
86
+ field: 'intents',
87
+ message: `Duplicate intent id "${intent.id}".`,
88
+ });
89
+ }
90
+ intentIds.add(intent.id);
91
+ errors.push(...validateIntent(intent));
92
+ }
93
+ // Check for conflicting phrases across intents
94
+ const phraseMap = new Map();
95
+ for (const intent of config.intents) {
96
+ for (const phrase of (_a = intent.phrases) !== null && _a !== void 0 ? _a : []) {
97
+ const normalized = phrase.toLowerCase().trim();
98
+ const existing = phraseMap.get(normalized);
99
+ if (existing) {
100
+ errors.push({
101
+ field: 'intents',
102
+ message: `Phrase "${phrase}" is used by both "${existing}" and "${intent.id}".`,
103
+ });
104
+ }
105
+ phraseMap.set(normalized, intent.id);
106
+ }
107
+ }
108
+ // Shortcut count warnings
109
+ const shortcutIntents = config.intents.filter((i) => i.shortTitle);
110
+ if (shortcutIntents.length > MAX_IOS_SHORTCUTS) {
111
+ warnings.push({
112
+ field: 'intents',
113
+ message: `${shortcutIntents.length} intents with shortTitle — iOS allows max ${MAX_IOS_SHORTCUTS}.`,
114
+ });
115
+ }
116
+ if (shortcutIntents.length > MAX_ANDROID_SHORTCUTS) {
117
+ warnings.push({
118
+ field: 'intents',
119
+ message: `${shortcutIntents.length} intents with shortTitle — Android allows max ${MAX_ANDROID_SHORTCUTS} launcher shortcuts.`,
120
+ });
121
+ }
122
+ return {
123
+ valid: errors.length === 0,
124
+ errors,
125
+ warnings,
126
+ };
127
+ }
128
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/utils/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["ExpoInvokeModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.invoke.ExpoInvokeModule"]
8
+ }
9
+ }
@@ -0,0 +1,19 @@
1
+ import Foundation
2
+ import Intents
3
+
4
+ /// Reads the currently active iOS Focus mode (Do Not Disturb, Work, Personal, Sleep, etc.).
5
+ /// Returns the display name string or nil if no Focus is active.
6
+ public struct FocusFilterManager {
7
+ public static func currentFocusMode() -> String? {
8
+ // INFocusStatusCenter is available iOS 15+
9
+ if #available(iOS 15, *) {
10
+ let status = INFocusStatusCenter.default.focusStatus
11
+ if status.isFocused == true {
12
+ // System doesn't expose the name directly; return a generic identifier
13
+ return "focused"
14
+ }
15
+ return nil
16
+ }
17
+ return nil
18
+ }
19
+ }
@@ -0,0 +1,42 @@
1
+ import AppIntents
2
+ import Foundation
3
+
4
+ /// Base protocol for App Intents created by expo-invoke consumers.
5
+ ///
6
+ /// The config plugin auto-generates a concrete struct for each intent in app.json.
7
+ /// For custom parameter handling, you can override the generated file.
8
+ ///
9
+ /// Generated file example (InvokeIntents.swift):
10
+ ///
11
+ /// ```swift
12
+ /// @available(iOS 16.4, *)
13
+ /// struct CreateErrandIntent: AppIntent {
14
+ /// static var title: LocalizedStringResource = "Create an Errand"
15
+ /// static var description = IntentDescription("Start a new errand with Mira.")
16
+ /// static var openAppWhenRun: Bool = true
17
+ ///
18
+ /// @Parameter(title: "Pickup Location")
19
+ /// var pickup: CLPlacemark?
20
+ ///
21
+ /// @Parameter(title: "Errand Type")
22
+ /// var errandType: String?
23
+ ///
24
+ /// func perform() async throws -> some IntentResult {
25
+ /// ExpoInvokeModule.writePendingIntent(
26
+ /// intentId: "create_errand",
27
+ /// source: "siri",
28
+ /// parameters: [
29
+ /// "pickup": pickup.map { ["latitude": $0.location?.coordinate.latitude ?? 0,
30
+ /// "longitude": $0.location?.coordinate.longitude ?? 0,
31
+ /// "name": $0.name ?? ""] } as Any,
32
+ /// "errandType": errandType as Any,
33
+ /// ].compactMapValues { $0 }
34
+ /// )
35
+ /// return .result()
36
+ /// }
37
+ /// }
38
+ /// ```
39
+ @available(iOS 16.4, *)
40
+ public protocol InvokeAppIntent: AppIntent {
41
+ static var intentId: String { get }
42
+ }
@@ -0,0 +1,49 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ private var _currentActivity: NSUserActivity?
5
+
6
+ public struct HandoffManager {
7
+
8
+ public static func advertise(intentId: String, paramsJson: String) {
9
+ let activity = NSUserActivity(activityType: "expo.invoke.handoff.\(intentId)")
10
+ activity.title = intentId
11
+ activity.isEligibleForHandoff = true
12
+ activity.isEligibleForSearch = false
13
+
14
+ var userInfo: [String: Any] = ["intentId": intentId, "source": "handoff"]
15
+ if let data = paramsJson.data(using: .utf8),
16
+ let params = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
17
+ userInfo["parameters"] = params
18
+ }
19
+ activity.userInfo = userInfo
20
+
21
+ // Must be set as the current activity on the key window scene
22
+ DispatchQueue.main.async {
23
+ _currentActivity = activity
24
+ activity.becomeCurrent()
25
+ }
26
+ }
27
+
28
+ public static func stop() {
29
+ DispatchQueue.main.async {
30
+ _currentActivity?.invalidate()
31
+ _currentActivity = nil
32
+ }
33
+ }
34
+
35
+ /// Call from AppDelegate application(_:continue:restorationHandler:)
36
+ public static func handleContinue(userActivity: NSUserActivity) -> Bool {
37
+ guard userActivity.activityType.hasPrefix("expo.invoke.handoff."),
38
+ let userInfo = userActivity.userInfo,
39
+ let intentId = userInfo["intentId"] as? String else { return false }
40
+
41
+ let params = userInfo["parameters"] as? [String: Any] ?? [:]
42
+ ExpoInvokeModule.writePendingIntent(
43
+ intentId: intentId,
44
+ source: "handoff",
45
+ parameters: params
46
+ )
47
+ return true
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import Foundation
2
+
3
+ /// Convenience wrapper used by generated App Intent files.
4
+ /// Thin wrapper around ExpoInvokeModule static helpers.
5
+ public struct InvokeActionStore {
6
+ public static func write(
7
+ intentId: String,
8
+ source: String = "siri",
9
+ parameters: [String: Any] = [:]
10
+ ) {
11
+ ExpoInvokeModule.writePendingIntent(
12
+ intentId: intentId,
13
+ source: source,
14
+ parameters: parameters
15
+ )
16
+ }
17
+
18
+ public static func consume() -> String? {
19
+ return ExpoInvokeModule.consumePendingIntent()
20
+ }
21
+ }
@@ -0,0 +1,142 @@
1
+ import AppIntents
2
+ import Contacts
3
+ import CoreLocation
4
+
5
+ // ─── Location Entity ──────────────────────────────────────────────────────────
6
+
7
+ @available(iOS 16.0, *)
8
+ public struct LocationEntity: AppEntity {
9
+ public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Location"
10
+ public static var defaultQuery = LocationEntityQuery()
11
+
12
+ public let id: String
13
+ public let name: String
14
+ public let latitude: Double
15
+ public let longitude: Double
16
+
17
+ public var displayRepresentation: DisplayRepresentation {
18
+ DisplayRepresentation(title: "\(name)")
19
+ }
20
+
21
+ public init(id: String, name: String, latitude: Double, longitude: Double) {
22
+ self.id = id
23
+ self.name = name
24
+ self.latitude = latitude
25
+ self.longitude = longitude
26
+ }
27
+
28
+ public func toJSON() -> [String: Any] {
29
+ return ["latitude": latitude, "longitude": longitude, "name": name]
30
+ }
31
+ }
32
+
33
+ @available(iOS 16.0, *)
34
+ public struct LocationEntityQuery: EntityQuery {
35
+ public init() {}
36
+ public func entities(for identifiers: [String]) async throws -> [LocationEntity] { [] }
37
+ }
38
+
39
+ // ─── Enum Entity ──────────────────────────────────────────────────────────────
40
+
41
+ @available(iOS 16.0, *)
42
+ public struct EnumEntity: AppEntity {
43
+ public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Option"
44
+ public static var defaultQuery = EnumEntityQuery()
45
+
46
+ public let id: String
47
+ public let title: String
48
+ public let subtitle: String?
49
+
50
+ public var displayRepresentation: DisplayRepresentation {
51
+ if let sub = subtitle {
52
+ return DisplayRepresentation(title: "\(title)", subtitle: "\(sub)")
53
+ }
54
+ return DisplayRepresentation(title: "\(title)")
55
+ }
56
+
57
+ public init(id: String, title: String, subtitle: String? = nil) {
58
+ self.id = id
59
+ self.title = title
60
+ self.subtitle = subtitle
61
+ }
62
+ }
63
+
64
+ @available(iOS 16.0, *)
65
+ public struct EnumEntityQuery: EntityQuery {
66
+ public init() {}
67
+ public func entities(for identifiers: [String]) async throws -> [EnumEntity] { [] }
68
+ }
69
+
70
+ // ─── Person Entity ────────────────────────────────────────────────────────────
71
+
72
+ @available(iOS 16.0, *)
73
+ public struct PersonEntity: AppEntity {
74
+ public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Person"
75
+ public static var defaultQuery = PersonEntityQuery()
76
+
77
+ public let id: String
78
+ public let displayName: String
79
+ public let phoneNumber: String?
80
+ public let emailAddress: String?
81
+ public let contactIdentifier: String?
82
+
83
+ public var displayRepresentation: DisplayRepresentation {
84
+ DisplayRepresentation(title: "\(displayName)")
85
+ }
86
+
87
+ public init(
88
+ id: String,
89
+ displayName: String,
90
+ phoneNumber: String? = nil,
91
+ emailAddress: String? = nil,
92
+ contactIdentifier: String? = nil
93
+ ) {
94
+ self.id = id
95
+ self.displayName = displayName
96
+ self.phoneNumber = phoneNumber
97
+ self.emailAddress = emailAddress
98
+ self.contactIdentifier = contactIdentifier
99
+ }
100
+
101
+ public func toJSON() -> [String: Any] {
102
+ var dict: [String: Any] = ["displayName": displayName]
103
+ if let phone = phoneNumber { dict["phoneNumber"] = phone }
104
+ if let email = emailAddress { dict["emailAddress"] = email }
105
+ if let cid = contactIdentifier { dict["contactIdentifier"] = cid }
106
+ return dict
107
+ }
108
+ }
109
+
110
+ @available(iOS 16.0, *)
111
+ public struct PersonEntityQuery: EntityQuery {
112
+ public init() {}
113
+ public func entities(for identifiers: [String]) async throws -> [PersonEntity] { [] }
114
+ }
115
+
116
+ // ─── Date Entity ──────────────────────────────────────────────────────────────
117
+
118
+ @available(iOS 16.0, *)
119
+ public struct DateEntity: AppEntity {
120
+ public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Date"
121
+ public static var defaultQuery = DateEntityQuery()
122
+
123
+ public let id: String
124
+ public let date: Date
125
+
126
+ public var displayRepresentation: DisplayRepresentation {
127
+ let formatter = DateFormatter()
128
+ formatter.dateStyle = .medium
129
+ return DisplayRepresentation(title: "\(formatter.string(from: date))")
130
+ }
131
+
132
+ public init(id: String, date: Date) {
133
+ self.id = id
134
+ self.date = date
135
+ }
136
+ }
137
+
138
+ @available(iOS 16.0, *)
139
+ public struct DateEntityQuery: EntityQuery {
140
+ public init() {}
141
+ public func entities(for identifiers: [String]) async throws -> [DateEntity] { [] }
142
+ }
@@ -0,0 +1,44 @@
1
+ import AppIntents
2
+ import Foundation
3
+
4
+ /// Bridges a Live Activity tap to an expo-invoke intent.
5
+ ///
6
+ /// When expo-live-activity is installed alongside expo-invoke, the config plugin
7
+ /// links them automatically. Tapping the Dynamic Island or Lock Screen Live Activity
8
+ /// fires the intent configured via `setLiveActivityIntent(activityId, intentId)`.
9
+ ///
10
+ /// Usage (generated by config plugin, not written manually):
11
+ ///
12
+ /// ```swift
13
+ /// @available(iOS 16.2, *)
14
+ /// struct TrackErrandLiveActivityIntent: LiveActivityIntent {
15
+ /// static var title: LocalizedStringResource = "Track Errand"
16
+ /// static var openAppWhenRun: Bool = true
17
+ ///
18
+ /// func perform() async throws -> some IntentResult {
19
+ /// // Look up which intentId is registered for this activity type
20
+ /// let intentId = UserDefaults.standard.string(
21
+ /// forKey: "expo.invoke.liveActivityIntent.errandInProgress"
22
+ /// ) ?? "track_errand"
23
+ ///
24
+ /// ExpoInvokeModule.writePendingIntent(
25
+ /// intentId: intentId,
26
+ /// source: "live_activity"
27
+ /// )
28
+ /// return .result()
29
+ /// }
30
+ /// }
31
+ /// ```
32
+ public struct LiveActivityIntentBridge {
33
+ /// Write the intent for a given Live Activity type. Called from generated Swift.
34
+ public static func fire(activityType: String, source: String = "live_activity") {
35
+ let intentId = UserDefaults.standard.string(
36
+ forKey: "expo.invoke.liveActivityIntent.\(activityType)"
37
+ ) ?? activityType
38
+
39
+ ExpoInvokeModule.writePendingIntent(
40
+ intentId: intentId,
41
+ source: source
42
+ )
43
+ }
44
+ }
@@ -0,0 +1,170 @@
1
+ import ExpoModulesCore
2
+ import Foundation
3
+ import CoreSpotlight
4
+ import Intents
5
+
6
+ private let kPendingIntentKey = "expo.invoke.pendingIntent"
7
+ private let kConfigKey = "expo.invoke.config"
8
+ private let kShortcutsKey = "expo.invoke.shortcuts"
9
+ private let kWidgetGroupSuite = "group.expo.invoke.widget"
10
+ private let kDefaultTTL: TimeInterval = 30.0
11
+
12
+ public class ExpoInvokeModule: Module {
13
+ public func definition() -> ModuleDefinition {
14
+ Name("ExpoInvoke")
15
+
16
+ Events("onIntent")
17
+
18
+ // ── configure ────────────────────────────────────────────────────────────
19
+ Function("configure") { (configJson: String) in
20
+ UserDefaults.standard.set(configJson, forKey: kConfigKey)
21
+ UserDefaults.standard.synchronize()
22
+ }
23
+
24
+ // ── getInitialIntent ─────────────────────────────────────────────────────
25
+ Function("getInitialIntent") { () -> String? in
26
+ return ExpoInvokeModule.consumePendingIntent()
27
+ }
28
+
29
+ // ── Donations ─────────────────────────────────────────────────────────────
30
+ Function("donateAction") { (json: String) in
31
+ DonationManager.donate(from: json)
32
+ }
33
+
34
+ Function("deleteAction") { (intentId: String) in
35
+ DonationManager.delete(intentId: intentId)
36
+ }
37
+
38
+ Function("deleteAllActions") {
39
+ DonationManager.deleteAll()
40
+ }
41
+
42
+ // ── Dynamic shortcuts ─────────────────────────────────────────────────────
43
+ Function("setDynamicShortcuts") { (json: String) in
44
+ UserDefaults.standard.set(json, forKey: kShortcutsKey)
45
+ UserDefaults.standard.synchronize()
46
+ ShortcutsManager.update(from: json)
47
+ }
48
+
49
+ Function("getDynamicShortcuts") { () -> String in
50
+ return UserDefaults.standard.string(forKey: kShortcutsKey) ?? "[]"
51
+ }
52
+
53
+ Function("clearDynamicShortcuts") {
54
+ UserDefaults.standard.removeObject(forKey: kShortcutsKey)
55
+ UserDefaults.standard.synchronize()
56
+ ShortcutsManager.clear()
57
+ }
58
+
59
+ // ── Spotlight ─────────────────────────────────────────────────────────────
60
+ Function("indexSpotlightItems") { (json: String) in
61
+ SpotlightManager.index(from: json)
62
+ }
63
+
64
+ Function("deindexSpotlightItems") { (ids: [String]) in
65
+ SpotlightManager.deindex(ids: ids)
66
+ }
67
+
68
+ // ── Widget data (App Groups) ──────────────────────────────────────────────
69
+ Function("setWidgetData") { (key: String, value: String) in
70
+ let suite = UserDefaults(suiteName: kWidgetGroupSuite) ?? UserDefaults.standard
71
+ suite.set(value, forKey: key)
72
+ suite.synchronize()
73
+ // Reload all widget timelines to display fresh data
74
+ if #available(iOS 14.0, *) {
75
+ WidgetManager.reloadAll()
76
+ }
77
+ }
78
+
79
+ Function("getWidgetData") { (key: String) -> String? in
80
+ let suite = UserDefaults(suiteName: kWidgetGroupSuite) ?? UserDefaults.standard
81
+ return suite.string(forKey: key)
82
+ }
83
+
84
+ // ── Handoff ───────────────────────────────────────────────────────────────
85
+ Function("advertiseHandoff") { (intentId: String, paramsJson: String) in
86
+ HandoffManager.advertise(intentId: intentId, paramsJson: paramsJson)
87
+ }
88
+
89
+ Function("stopHandoff") {
90
+ HandoffManager.stop()
91
+ }
92
+
93
+ // ── Live Activity intent ──────────────────────────────────────────────────
94
+ Function("setLiveActivityIntent") { (activityId: String, intentId: String) in
95
+ UserDefaults.standard.set(intentId, forKey: "expo.invoke.liveActivityIntent.\(activityId)")
96
+ UserDefaults.standard.synchronize()
97
+ }
98
+
99
+ // ── Add to Siri ───────────────────────────────────────────────────────────
100
+ Function("addIntentToSiri") { (intentId: String) in
101
+ // Store the intent ID so the AddToSiriButton component can read it
102
+ UserDefaults.standard.set(intentId, forKey: "expo.invoke.addToSiriIntent")
103
+ UserDefaults.standard.synchronize()
104
+ // Post notification so the native AddToSiriButton can pick it up
105
+ NotificationCenter.default.post(
106
+ name: Notification.Name("ExpoInvokeAddToSiri"),
107
+ object: nil,
108
+ userInfo: ["intentId": intentId]
109
+ )
110
+ }
111
+
112
+ // ── Focus mode ────────────────────────────────────────────────────────────
113
+ Function("getFocusMode") { () -> String? in
114
+ return FocusFilterManager.currentFocusMode()
115
+ }
116
+
117
+ // ── Quick tile — no-op on iOS ─────────────────────────────────────────────
118
+ Function("updateQuickTile") { (_: String, _: String) in /* iOS no-op */ }
119
+
120
+ // ── Warm start ────────────────────────────────────────────────────────────
121
+ OnAppBecomesActive {
122
+ if let json = ExpoInvokeModule.consumePendingIntent() {
123
+ self.sendEvent("onIntent", ["json": json])
124
+ }
125
+ }
126
+ }
127
+
128
+ // ─── Public static helpers (called by native App Intents) ──────────────────
129
+
130
+ /// Write a resolved intent to UserDefaults before the app launches.
131
+ /// Call this from your AppIntent's perform() method.
132
+ public static func writePendingIntent(
133
+ intentId: String,
134
+ source: String = "siri",
135
+ parameters: [String: Any] = [],
136
+ spokenResponse: String? = nil
137
+ ) {
138
+ var payload: [String: Any] = [
139
+ "intentId": intentId,
140
+ "source": source,
141
+ "timestamp": Date().timeIntervalSince1970 * 1000,
142
+ "parameters": parameters,
143
+ ]
144
+ if let spoken = spokenResponse {
145
+ payload["_spokenResponse"] = spoken
146
+ }
147
+ if let data = try? JSONSerialization.data(withJSONObject: payload),
148
+ let json = String(data: data, encoding: .utf8) {
149
+ UserDefaults.standard.set(json, forKey: kPendingIntentKey)
150
+ UserDefaults.standard.synchronize()
151
+ }
152
+ }
153
+
154
+ /// Read and atomically clear the pending intent. Returns nil if stale.
155
+ public static func consumePendingIntent() -> String? {
156
+ guard let json = UserDefaults.standard.string(forKey: kPendingIntentKey),
157
+ let data = json.data(using: .utf8),
158
+ let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
159
+ let tsMs = payload["timestamp"] as? Double else { return nil }
160
+
161
+ UserDefaults.standard.removeObject(forKey: kPendingIntentKey)
162
+ UserDefaults.standard.synchronize()
163
+
164
+ let ttl = (payload["_ttl"] as? Double) ?? (kDefaultTTL * 1000)
165
+ let ageMs = Date().timeIntervalSince1970 * 1000 - tsMs
166
+ guard ageMs < ttl else { return nil }
167
+
168
+ return json
169
+ }
170
+ }