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.
- package/MIGRATION.md +125 -0
- package/README.md +942 -0
- package/SECURITY.md +35 -0
- package/android/build.gradle +42 -0
- package/android/src/main/java/expo/modules/invoke/AppActionsHandler.kt +74 -0
- package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +135 -0
- package/android/src/main/java/expo/modules/invoke/QuickTileService.kt +63 -0
- package/android/src/main/java/expo/modules/invoke/ShortcutsHelper.kt +72 -0
- package/build/cli/commands/generate.d.ts +1 -0
- package/build/cli/commands/generate.js +119 -0
- package/build/cli/commands/generate.js.map +1 -0
- package/build/cli/commands/test.d.ts +1 -0
- package/build/cli/commands/test.js +53 -0
- package/build/cli/commands/test.js.map +1 -0
- package/build/cli/commands/validate.d.ts +1 -0
- package/build/cli/commands/validate.js +116 -0
- package/build/cli/commands/validate.js.map +1 -0
- package/build/cli/index.d.ts +2 -0
- package/build/cli/index.js +37 -0
- package/build/cli/index.js.map +1 -0
- package/build/plugin/android/withAndroidInvoke.d.ts +8 -0
- package/build/plugin/android/withAndroidInvoke.js +106 -0
- package/build/plugin/android/withAndroidInvoke.js.map +1 -0
- package/build/plugin/codegen/generateActionsXml.d.ts +2 -0
- package/build/plugin/codegen/generateActionsXml.js +69 -0
- package/build/plugin/codegen/generateActionsXml.js.map +1 -0
- package/build/plugin/codegen/generateShortcutsXml.d.ts +2 -0
- package/build/plugin/codegen/generateShortcutsXml.js +45 -0
- package/build/plugin/codegen/generateShortcutsXml.js.map +1 -0
- package/build/plugin/codegen/generateSwiftIntents.d.ts +2 -0
- package/build/plugin/codegen/generateSwiftIntents.js +129 -0
- package/build/plugin/codegen/generateSwiftIntents.js.map +1 -0
- package/build/plugin/ios/withIOSInvoke.d.ts +9 -0
- package/build/plugin/ios/withIOSInvoke.js +83 -0
- package/build/plugin/ios/withIOSInvoke.js.map +1 -0
- package/build/plugin/src/withInvoke.d.ts +16 -0
- package/build/plugin/src/withInvoke.js +13 -0
- package/build/plugin/src/withInvoke.js.map +1 -0
- package/build/src/components/AddToSiriButton.d.ts +18 -0
- package/build/src/components/AddToSiriButton.d.ts.map +1 -0
- package/build/src/components/AddToSiriButton.js +35 -0
- package/build/src/components/AddToSiriButton.js.map +1 -0
- package/build/src/components/InvokeDebugger.d.ts +12 -0
- package/build/src/components/InvokeDebugger.d.ts.map +1 -0
- package/build/src/components/InvokeDebugger.js +122 -0
- package/build/src/components/InvokeDebugger.js.map +1 -0
- package/build/src/components/ShortcutChip.d.ts +20 -0
- package/build/src/components/ShortcutChip.d.ts.map +1 -0
- package/build/src/components/ShortcutChip.js +44 -0
- package/build/src/components/ShortcutChip.js.map +1 -0
- package/build/src/hooks/useAddToSiri.d.ts +9 -0
- package/build/src/hooks/useAddToSiri.d.ts.map +1 -0
- package/build/src/hooks/useAddToSiri.js +15 -0
- package/build/src/hooks/useAddToSiri.js.map +1 -0
- package/build/src/hooks/useDonation.d.ts +26 -0
- package/build/src/hooks/useDonation.d.ts.map +1 -0
- package/build/src/hooks/useDonation.js +21 -0
- package/build/src/hooks/useDonation.js.map +1 -0
- package/build/src/hooks/useFocusFilter.d.ts +11 -0
- package/build/src/hooks/useFocusFilter.d.ts.map +1 -0
- package/build/src/hooks/useFocusFilter.js +24 -0
- package/build/src/hooks/useFocusFilter.js.map +1 -0
- package/build/src/hooks/useHandoff.d.ts +29 -0
- package/build/src/hooks/useHandoff.d.ts.map +1 -0
- package/build/src/hooks/useHandoff.js +27 -0
- package/build/src/hooks/useHandoff.js.map +1 -0
- package/build/src/hooks/useInvoke.d.ts +43 -0
- package/build/src/hooks/useInvoke.d.ts.map +1 -0
- package/build/src/hooks/useInvoke.js +70 -0
- package/build/src/hooks/useInvoke.js.map +1 -0
- package/build/src/hooks/useInvokeAnalytics.d.ts +23 -0
- package/build/src/hooks/useInvokeAnalytics.d.ts.map +1 -0
- package/build/src/hooks/useInvokeAnalytics.js +27 -0
- package/build/src/hooks/useInvokeAnalytics.js.map +1 -0
- package/build/src/hooks/useInvokeHistory.d.ts +11 -0
- package/build/src/hooks/useInvokeHistory.d.ts.map +1 -0
- package/build/src/hooks/useInvokeHistory.js +26 -0
- package/build/src/hooks/useInvokeHistory.js.map +1 -0
- package/build/src/hooks/useInvokeInit.d.ts +16 -0
- package/build/src/hooks/useInvokeInit.d.ts.map +1 -0
- package/build/src/hooks/useInvokeInit.js +32 -0
- package/build/src/hooks/useInvokeInit.js.map +1 -0
- package/build/src/hooks/useLiveActivityIntent.d.ts +21 -0
- package/build/src/hooks/useLiveActivityIntent.d.ts.map +1 -0
- package/build/src/hooks/useLiveActivityIntent.js +9 -0
- package/build/src/hooks/useLiveActivityIntent.js.map +1 -0
- package/build/src/hooks/useQuickTile.d.ts +23 -0
- package/build/src/hooks/useQuickTile.d.ts.map +1 -0
- package/build/src/hooks/useQuickTile.js +15 -0
- package/build/src/hooks/useQuickTile.js.map +1 -0
- package/build/src/hooks/useShortcuts.d.ts +25 -0
- package/build/src/hooks/useShortcuts.d.ts.map +1 -0
- package/build/src/hooks/useShortcuts.js +23 -0
- package/build/src/hooks/useShortcuts.js.map +1 -0
- package/build/src/hooks/useSpotlight.d.ts +23 -0
- package/build/src/hooks/useSpotlight.d.ts.map +1 -0
- package/build/src/hooks/useSpotlight.js +13 -0
- package/build/src/hooks/useSpotlight.js.map +1 -0
- package/build/src/hooks/useWidgetData.d.ts +20 -0
- package/build/src/hooks/useWidgetData.d.ts.map +1 -0
- package/build/src/hooks/useWidgetData.js +12 -0
- package/build/src/hooks/useWidgetData.js.map +1 -0
- package/build/src/index.d.ts +32 -0
- package/build/src/index.d.ts.map +1 -0
- package/build/src/index.js +27 -0
- package/build/src/index.js.map +1 -0
- package/build/src/module.d.ts +25 -0
- package/build/src/module.d.ts.map +1 -0
- package/build/src/module.js +102 -0
- package/build/src/module.js.map +1 -0
- package/build/src/types/index.d.ts +156 -0
- package/build/src/types/index.d.ts.map +1 -0
- package/build/src/types/index.js +4 -0
- package/build/src/types/index.js.map +1 -0
- package/build/src/utils/actionStore.d.ts +6 -0
- package/build/src/utils/actionStore.d.ts.map +1 -0
- package/build/src/utils/actionStore.js +32 -0
- package/build/src/utils/actionStore.js.map +1 -0
- package/build/src/utils/analytics.d.ts +12 -0
- package/build/src/utils/analytics.d.ts.map +1 -0
- package/build/src/utils/analytics.js +29 -0
- package/build/src/utils/analytics.js.map +1 -0
- package/build/src/utils/testing.d.ts +39 -0
- package/build/src/utils/testing.d.ts.map +1 -0
- package/build/src/utils/testing.js +58 -0
- package/build/src/utils/testing.js.map +1 -0
- package/build/src/utils/validation.d.ts +11 -0
- package/build/src/utils/validation.d.ts.map +1 -0
- package/build/src/utils/validation.js +128 -0
- package/build/src/utils/validation.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Focus/FocusFilterManager.swift +19 -0
- package/ios/Intents/BaseInvokeIntent.swift +42 -0
- package/ios/Intents/HandoffManager.swift +49 -0
- package/ios/Intents/InvokeActionStore.swift +21 -0
- package/ios/Intents/InvokeParameterTypes.swift +142 -0
- package/ios/LiveActivity/LiveActivityIntentBridge.swift +44 -0
- package/ios/Module/ExpoInvokeModule.swift +170 -0
- package/ios/Notifications/NotificationActionHandler.swift +80 -0
- package/ios/Shortcuts/DonationManager.swift +44 -0
- package/ios/Shortcuts/ShortcutsManager.swift +32 -0
- package/ios/Shortcuts/SpotlightManager.swift +48 -0
- package/ios/Widget/WidgetManager.swift +19 -0
- 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,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
|
+
}
|