expo-app-intents 0.4.7 → 0.5.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/CHANGELOG.md +18 -0
- package/README.md +27 -0
- package/build/ExpoAppIntents.types.d.ts +25 -0
- package/build/ExpoAppIntents.types.js.map +1 -1
- package/build/ExpoAppIntentsModule.d.ts +3 -1
- package/build/ExpoAppIntentsModule.js.map +1 -1
- package/build/index.d.ts +41 -1
- package/build/index.js +48 -0
- package/build/index.js.map +1 -1
- package/ios/AppIntentDonation.swift +304 -0
- package/ios/ExpoAppIntentsModule.swift +13 -0
- package/package.json +5 -5
- package/src/ExpoAppIntents.types.ts +27 -0
- package/src/ExpoAppIntentsModule.ts +4 -0
- package/src/index.ts +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `donateIntentAsync()` and `deleteDonationsAsync()` to donate App Intents to the system. ([#50760](https://github.com/expo/expo/pull/50760) by [@chrfalch](https://github.com/chrfalch))
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies. ([#50881](https://github.com/expo/expo/pull/50881), [#49933](https://github.com/expo/expo/pull/49933))
|
|
12
|
+
- @expo/ui@58.0.11
|
|
13
|
+
|
|
14
|
+
## 0.4.8
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies. ([#50801](https://github.com/expo/expo/pull/50801), [#49986](https://github.com/expo/expo/pull/49986), [#50674](https://github.com/expo/expo/pull/50674), [#50786](https://github.com/expo/expo/pull/50786), [#50851](https://github.com/expo/expo/pull/50851))
|
|
19
|
+
- @expo/ui@58.0.10
|
|
20
|
+
|
|
3
21
|
## 0.4.7
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -45,6 +45,33 @@ export function AppIntentHandler() {
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Donating intents
|
|
49
|
+
|
|
50
|
+
Donate an intent when the user performs its action inside your app, so the system can suggest it later on the Lock Screen, in Siri Suggestions, and in Spotlight. Make the intent donatable in Swift and register it in the `OnCreate` of your `AppIntentsSetup` module. JavaScript donates by the registered name. Nothing requires it to match the name the intent dispatches, but reusing that name gives each intent one name in JavaScript. The params that JavaScript passes are converted to the intent's `DonationParams` record, so a missing required field or a field of the wrong type rejects the donation. An intent without params only needs to conform to `DonatableAppIntent`. Declare the record with `@Field` properties, because the `@Record` macro is not available in the app target:
|
|
51
|
+
|
|
52
|
+
```swift
|
|
53
|
+
extension SaveNoteIntent: DonatableAppIntent {
|
|
54
|
+
struct DonationParams: Record {
|
|
55
|
+
@Field(.required) var text: String = ""
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
init(donationParams: DonationParams) {
|
|
59
|
+
self.init()
|
|
60
|
+
self.text = donationParams.text
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
AppIntentDonationRegistry.shared.register("saveNote", as: SaveNoteIntent.self)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then donate it from JavaScript, and delete donations when they no longer apply:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
await AppIntents.donateIntentAsync('saveNote', { text: 'Buy milk' });
|
|
71
|
+
|
|
72
|
+
await AppIntents.deleteDonationsAsync({ intent: 'saveNote' });
|
|
73
|
+
```
|
|
74
|
+
|
|
48
75
|
## Limitations
|
|
49
76
|
|
|
50
77
|
- Shortcut phrases are compiled at build time and cannot be created from JavaScript at runtime. Only parameter values are dynamic.
|
|
@@ -77,6 +77,31 @@ export type AppEntityIdentifierModifier = ModifierConfig & {
|
|
|
77
77
|
/** Stable entity id from the matching App Intents entity catalog. */
|
|
78
78
|
id: string;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* A value that can be passed as an App Intent parameter: anything JSON can represent.
|
|
82
|
+
*/
|
|
83
|
+
export type AppIntentJSONValue = string | number | boolean | null | AppIntentJSONValue[] | {
|
|
84
|
+
[key: string]: AppIntentJSONValue;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Selects the donations that `deleteDonationsAsync()` deletes. Pass one of these shapes:
|
|
88
|
+
*
|
|
89
|
+
* - `{ ids }` deletes the donations with the given IDs, as returned by `donateIntentAsync()`.
|
|
90
|
+
* - `{ intent }` deletes every donation of the intent type registered under that name. When
|
|
91
|
+
* several names are registered for one intent type, this also deletes the donations made under
|
|
92
|
+
* the other names.
|
|
93
|
+
* - `{ entity, id }` deletes every donation that refers to the given entity. The `entity` value
|
|
94
|
+
* must be registered with `AppEntityIdentifierRegistry.shared.register(_:as:)` or
|
|
95
|
+
* `AppEntityIdentifierRegistry.shared.registerIndexed(_:as:)`.
|
|
96
|
+
*/
|
|
97
|
+
export type AppIntentDonationFilter = {
|
|
98
|
+
ids: string[];
|
|
99
|
+
} | {
|
|
100
|
+
intent: string;
|
|
101
|
+
} | {
|
|
102
|
+
entity: string;
|
|
103
|
+
id: string;
|
|
104
|
+
};
|
|
80
105
|
export type ExpoAppIntentsModuleEvents = {
|
|
81
106
|
onIntent: (invocation: AppIntentInvocation) => void;
|
|
82
107
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["ExpoAppIntents.types.ts"],"sourcesContent":[null],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wBAAwB;
|
|
1
|
+
{"version":3,"sources":["ExpoAppIntents.types.ts"],"sourcesContent":[null],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wBAAwB;AAmHxB,WAEE"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NativeModule } from 'expo-modules-core';
|
|
2
|
-
import type { AppIntentEntity, AppIntentInvocation, ExpoAppIntentsModuleEvents } from './ExpoAppIntents.types';
|
|
2
|
+
import type { AppIntentDonationFilter, AppIntentEntity, AppIntentInvocation, AppIntentJSONValue, ExpoAppIntentsModuleEvents } from './ExpoAppIntents.types';
|
|
3
3
|
declare class ExpoAppIntentsNativeModule extends NativeModule<ExpoAppIntentsModuleEvents> {
|
|
4
4
|
getPendingInvocationsAsync(): Promise<AppIntentInvocation[]>;
|
|
5
5
|
removePendingInvocationAsync(id: string): Promise<void>;
|
|
@@ -8,6 +8,8 @@ declare class ExpoAppIntentsNativeModule extends NativeModule<ExpoAppIntentsModu
|
|
|
8
8
|
reindexEntitiesAsync(kind: string | null): Promise<void>;
|
|
9
9
|
getEntityCatalogAsync(kind: string): Promise<AppIntentEntity[]>;
|
|
10
10
|
refreshShortcutsAsync(): Promise<void>;
|
|
11
|
+
donateIntentAsync(name: string, params?: Record<string, AppIntentJSONValue>): Promise<string>;
|
|
12
|
+
deleteDonationsAsync(filter: AppIntentDonationFilter): Promise<string[]>;
|
|
11
13
|
}
|
|
12
14
|
declare const _default: ExpoAppIntentsNativeModule | null;
|
|
13
15
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["ExpoAppIntentsModule.ts"],"sourcesContent":[null],"names":["NativeModule","requireOptionalNativeModule"],"mappings":"AAAA,SAASA,YAAY,EAAEC,2BAA2B,QAAQ,oBAAoB;
|
|
1
|
+
{"version":3,"sources":["ExpoAppIntentsModule.ts"],"sourcesContent":[null],"names":["NativeModule","requireOptionalNativeModule"],"mappings":"AAAA,SAASA,YAAY,EAAEC,2BAA2B,QAAQ,oBAAoB;AAsB9E,eAAeA,4BAAwD,kBAAkB"}
|
package/build/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
-
import type { AppEntityIdentifierModifier, AppIntentEntity, AppIntentInvocation, AppIntentsHandler } from './ExpoAppIntents.types';
|
|
2
|
+
import type { AppEntityIdentifierModifier, AppIntentDonationFilter, AppIntentEntity, AppIntentInvocation, AppIntentJSONValue, AppIntentsHandler } from './ExpoAppIntents.types';
|
|
3
3
|
export type * from './ExpoAppIntents.types';
|
|
4
4
|
export { default as AppEntityView } from './AppEntityView';
|
|
5
5
|
export type { AppEntityViewProps } from './AppEntityView.types';
|
|
@@ -112,6 +112,46 @@ export declare function getEntityCatalogAsync(kind: string): Promise<AppIntentEn
|
|
|
112
112
|
* shortcuts.
|
|
113
113
|
*/
|
|
114
114
|
export declare function refreshShortcutsAsync(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Tells the system that the user just performed an App Intent's action in your app, for example
|
|
117
|
+
* ordering food from a screen rather than through Siri. The system learns from donations and can
|
|
118
|
+
* suggest the action later on the Lock Screen, in Siri Suggestions, and in Spotlight.
|
|
119
|
+
*
|
|
120
|
+
* The `name` must be registered from app-target Swift with
|
|
121
|
+
* `AppIntentDonationRegistry.shared.register(_:as:)`, on an intent that conforms to
|
|
122
|
+
* `DonatableAppIntent`. Native code converts `params` to the `DonationParams` record of that intent,
|
|
123
|
+
* and the intent builds itself from the record in its `init(donationParams:)`.
|
|
124
|
+
*
|
|
125
|
+
* The returned promise is fulfilled with an ID for the donation, which
|
|
126
|
+
* [`deleteDonationsAsync()`](#appintentsdeletedonationsasyncfilter) accepts, or with `null` when
|
|
127
|
+
* App Intents are unavailable. It is rejected when no intent is registered as `name`, when `params`
|
|
128
|
+
* misses a required field of the record or has a field of the wrong type, when the intent cannot be
|
|
129
|
+
* built from the record, or when the system fails to record the donation.
|
|
130
|
+
*
|
|
131
|
+
* > **Note:** The ID is the system's donation identifier in its encoded form. Store it only for as
|
|
132
|
+
* > long as you need it. An iOS update may change how the system encodes identifiers, and an ID
|
|
133
|
+
* > stored before such an update may then be rejected. To delete donations without stored IDs,
|
|
134
|
+
* > delete by `intent` or by `entity`.
|
|
135
|
+
*
|
|
136
|
+
* @platform ios
|
|
137
|
+
*/
|
|
138
|
+
export declare function donateIntentAsync(name: string, params?: Record<string, AppIntentJSONValue>): Promise<string | null>;
|
|
139
|
+
/**
|
|
140
|
+
* Deletes donations made with [`donateIntentAsync()`](#appintentsdonateintentasyncname-params), so
|
|
141
|
+
* the system stops suggesting them. Delete donations when what they refer to is gone, for example
|
|
142
|
+
* after the user deletes an order, or when the user signs out.
|
|
143
|
+
*
|
|
144
|
+
* The returned promise is fulfilled with the IDs of the deleted donations, or with an empty array
|
|
145
|
+
* when App Intents are unavailable. It is rejected when `filter` names an intent or entity that is
|
|
146
|
+
* not registered, contains an ID that cannot be read, or contains an entity `id` that cannot be
|
|
147
|
+
* converted to the ID type of that entity. In those cases nothing is deleted. With `ids`, every ID
|
|
148
|
+
* is attempted. When the system fails to delete one or more of them, the promise is rejected with
|
|
149
|
+
* an error that lists the deleted IDs and the IDs that were not deleted. With `intent` or
|
|
150
|
+
* `entity`, the promise is rejected with the system error when the system fails to delete.
|
|
151
|
+
*
|
|
152
|
+
* @platform ios
|
|
153
|
+
*/
|
|
154
|
+
export declare function deleteDonationsAsync(filter: AppIntentDonationFilter): Promise<string[]>;
|
|
115
155
|
/**
|
|
116
156
|
* Returns an ExpoUI SwiftUI modifier config that ties a view to an AppEntity identifier.
|
|
117
157
|
*
|
package/build/index.js
CHANGED
|
@@ -217,6 +217,54 @@ function callAppIntentsHandler(handler, pendingIntents, newIntent) {
|
|
|
217
217
|
}
|
|
218
218
|
return ExpoAppIntents.refreshShortcutsAsync();
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Tells the system that the user just performed an App Intent's action in your app, for example
|
|
222
|
+
* ordering food from a screen rather than through Siri. The system learns from donations and can
|
|
223
|
+
* suggest the action later on the Lock Screen, in Siri Suggestions, and in Spotlight.
|
|
224
|
+
*
|
|
225
|
+
* The `name` must be registered from app-target Swift with
|
|
226
|
+
* `AppIntentDonationRegistry.shared.register(_:as:)`, on an intent that conforms to
|
|
227
|
+
* `DonatableAppIntent`. Native code converts `params` to the `DonationParams` record of that intent,
|
|
228
|
+
* and the intent builds itself from the record in its `init(donationParams:)`.
|
|
229
|
+
*
|
|
230
|
+
* The returned promise is fulfilled with an ID for the donation, which
|
|
231
|
+
* [`deleteDonationsAsync()`](#appintentsdeletedonationsasyncfilter) accepts, or with `null` when
|
|
232
|
+
* App Intents are unavailable. It is rejected when no intent is registered as `name`, when `params`
|
|
233
|
+
* misses a required field of the record or has a field of the wrong type, when the intent cannot be
|
|
234
|
+
* built from the record, or when the system fails to record the donation.
|
|
235
|
+
*
|
|
236
|
+
* > **Note:** The ID is the system's donation identifier in its encoded form. Store it only for as
|
|
237
|
+
* > long as you need it. An iOS update may change how the system encodes identifiers, and an ID
|
|
238
|
+
* > stored before such an update may then be rejected. To delete donations without stored IDs,
|
|
239
|
+
* > delete by `intent` or by `entity`.
|
|
240
|
+
*
|
|
241
|
+
* @platform ios
|
|
242
|
+
*/ export async function donateIntentAsync(name, params) {
|
|
243
|
+
if (!ExpoAppIntents) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return ExpoAppIntents.donateIntentAsync(name, params);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Deletes donations made with [`donateIntentAsync()`](#appintentsdonateintentasyncname-params), so
|
|
250
|
+
* the system stops suggesting them. Delete donations when what they refer to is gone, for example
|
|
251
|
+
* after the user deletes an order, or when the user signs out.
|
|
252
|
+
*
|
|
253
|
+
* The returned promise is fulfilled with the IDs of the deleted donations, or with an empty array
|
|
254
|
+
* when App Intents are unavailable. It is rejected when `filter` names an intent or entity that is
|
|
255
|
+
* not registered, contains an ID that cannot be read, or contains an entity `id` that cannot be
|
|
256
|
+
* converted to the ID type of that entity. In those cases nothing is deleted. With `ids`, every ID
|
|
257
|
+
* is attempted. When the system fails to delete one or more of them, the promise is rejected with
|
|
258
|
+
* an error that lists the deleted IDs and the IDs that were not deleted. With `intent` or
|
|
259
|
+
* `entity`, the promise is rejected with the system error when the system fails to delete.
|
|
260
|
+
*
|
|
261
|
+
* @platform ios
|
|
262
|
+
*/ export async function deleteDonationsAsync(filter) {
|
|
263
|
+
if (!ExpoAppIntents) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
return ExpoAppIntents.deleteDonationsAsync(filter);
|
|
267
|
+
}
|
|
220
268
|
/**
|
|
221
269
|
* Returns an ExpoUI SwiftUI modifier config that ties a view to an AppEntity identifier.
|
|
222
270
|
*
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["index.ts"],"sourcesContent":[null],"names":["UnavailabilityError","useEffect","useRef","ExpoAppIntents","default","AppEntityView","MAX_SEEN_INVOCATION_IDS","isAvailable","addAppIntentListener","listener","remove","addListener","callAppIntentsHandler","handler","pendingIntents","newIntent","Promise","resolve","then","catch","error","console","warn","useAppIntents","handlerRef","current","isMounted","seenLiveInvocationIds","Set","deliveryQueue","enqueue","deliver","notify","deliverNewIntent","getPendingInvocationsAsync","length","deliverInitialPendingIntents","initialPendingIntents","filter","invocation","has","id","forEach","add","subscription","size","delete","values","next","value","removePendingInvocationAsync","clearPendingInvocationsAsync","setEntityCatalogAsync","kind","entities","reindexEntitiesAsync","getEntityCatalogAsync","refreshShortcutsAsync","appEntityIdentifier","entity","$type"],"mappings":"AAAA,SAAiCA,mBAAmB,QAAQ,oBAAoB;AAChF,SAASC,SAAS,EAAEC,MAAM,QAAQ,QAAQ;
|
|
1
|
+
{"version":3,"sources":["index.ts"],"sourcesContent":[null],"names":["UnavailabilityError","useEffect","useRef","ExpoAppIntents","default","AppEntityView","MAX_SEEN_INVOCATION_IDS","isAvailable","addAppIntentListener","listener","remove","addListener","callAppIntentsHandler","handler","pendingIntents","newIntent","Promise","resolve","then","catch","error","console","warn","useAppIntents","handlerRef","current","isMounted","seenLiveInvocationIds","Set","deliveryQueue","enqueue","deliver","notify","deliverNewIntent","getPendingInvocationsAsync","length","deliverInitialPendingIntents","initialPendingIntents","filter","invocation","has","id","forEach","add","subscription","size","delete","values","next","value","removePendingInvocationAsync","clearPendingInvocationsAsync","setEntityCatalogAsync","kind","entities","reindexEntitiesAsync","getEntityCatalogAsync","refreshShortcutsAsync","donateIntentAsync","name","params","deleteDonationsAsync","appEntityIdentifier","entity","$type"],"mappings":"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"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import AppIntents
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// An `AppIntent` that JavaScript can donate by name with `donateIntentAsync()`.
|
|
6
|
+
///
|
|
7
|
+
/// A donation tells the system that the user just did this in the app, so Siri, Spotlight and the
|
|
8
|
+
/// Shortcuts app can suggest it later. The system needs a real intent value to learn from, and only
|
|
9
|
+
/// the app target can build one, so each donatable intent builds itself from the params that
|
|
10
|
+
/// JavaScript passed.
|
|
11
|
+
///
|
|
12
|
+
/// The params arrive as the intent's `DonationParams` record, so each field already has the type the
|
|
13
|
+
/// intent declares, and a missing required field is rejected before the intent is built. An intent
|
|
14
|
+
/// that takes no params from JavaScript can leave out both `DonationParams` and `init(donationParams:)`.
|
|
15
|
+
public protocol DonatableAppIntent: AppIntent {
|
|
16
|
+
associatedtype DonationParams: Record = NoDonationParams
|
|
17
|
+
|
|
18
|
+
init(donationParams: DonationParams) async throws
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// The `DonationParams` of an intent that takes no params from JavaScript. Any params passed are
|
|
22
|
+
/// ignored.
|
|
23
|
+
public struct NoDonationParams: Record {
|
|
24
|
+
public init() {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extension DonatableAppIntent where DonationParams == NoDonationParams {
|
|
28
|
+
/// An intent without params needs no donation code of its own: conforming is enough.
|
|
29
|
+
public init(donationParams: NoDonationParams) {
|
|
30
|
+
self.init()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
internal enum AppIntentDonationFilter {
|
|
35
|
+
case ids([String])
|
|
36
|
+
case intent(String)
|
|
37
|
+
case entity(String, id: String)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The system predicate a filter resolves to. `IntentDonationMatchingPredicate` is opaque, so this is
|
|
41
|
+
/// what the donor is handed instead, and what tests can inspect.
|
|
42
|
+
internal enum AppIntentDonationMatch {
|
|
43
|
+
case donation(IntentDonationIdentifier)
|
|
44
|
+
case intentType(any AppIntent.Type)
|
|
45
|
+
case entity(EntityIdentifier)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// The calls made to `IntentDonationManager`, behind a protocol so tests never donate to the device.
|
|
49
|
+
internal protocol AppIntentDonor: Sendable {
|
|
50
|
+
func donate(_ intent: any AppIntent) async throws -> IntentDonationIdentifier
|
|
51
|
+
func deleteDonations(matching match: AppIntentDonationMatch) async throws -> [IntentDonationIdentifier]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
internal struct SystemAppIntentDonor: AppIntentDonor {
|
|
55
|
+
func donate(_ intent: any AppIntent) async throws -> IntentDonationIdentifier {
|
|
56
|
+
return try await IntentDonationManager.shared.donate(intent: intent)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func deleteDonations(matching match: AppIntentDonationMatch) async throws -> [IntentDonationIdentifier] {
|
|
60
|
+
let manager = IntentDonationManager.shared
|
|
61
|
+
switch match {
|
|
62
|
+
case .donation(let identifier):
|
|
63
|
+
return try await manager.deleteDonations(matching: .donationIdentifier(identifier))
|
|
64
|
+
case .intentType(let intentType):
|
|
65
|
+
return try await manager.deleteDonations(matching: .intentType(intentType))
|
|
66
|
+
case .entity(let identifier):
|
|
67
|
+
return try await manager.deleteDonations(matching: .entityIdentifier(identifier))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Maps the names JavaScript donates by to the app-target intent types that can be donated.
|
|
73
|
+
///
|
|
74
|
+
/// Register each intent in the `OnCreate` of the `AppIntentsSetup` inline module. The name only has to
|
|
75
|
+
/// match what JavaScript passes to `donateIntentAsync()` and `deleteDonationsAsync({ intent })`.
|
|
76
|
+
/// Nothing links it to the name the intent passes to `AppIntentDispatcher.shared.dispatch(name:params:)`,
|
|
77
|
+
/// but reusing that name, where there is one, gives JavaScript one name per intent.
|
|
78
|
+
public final class AppIntentDonationRegistry: Sendable {
|
|
79
|
+
public static let shared = AppIntentDonationRegistry()
|
|
80
|
+
|
|
81
|
+
/// What `register(_:as:)` keeps for one name. The intent type is erased so that intents with
|
|
82
|
+
/// different `DonationParams` can share the registry, and `makeIntent` keeps the concrete type
|
|
83
|
+
/// that converting the params needs.
|
|
84
|
+
private struct Registration: Sendable {
|
|
85
|
+
let intentType: any AppIntent.Type
|
|
86
|
+
let makeIntent: @Sendable ([String: Any], AppContext) async throws -> any AppIntent
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Registration happens on the main actor from `OnCreate`, while lookups come from the module's
|
|
90
|
+
/// async functions at the same time.
|
|
91
|
+
private let registrations = Mutex<[String: Registration]>([:])
|
|
92
|
+
private let donor: any AppIntentDonor
|
|
93
|
+
private let entities: AppEntityIdentifierRegistry
|
|
94
|
+
|
|
95
|
+
internal init(
|
|
96
|
+
donor: any AppIntentDonor = SystemAppIntentDonor(),
|
|
97
|
+
entities: AppEntityIdentifierRegistry = .shared
|
|
98
|
+
) {
|
|
99
|
+
self.donor = donor
|
|
100
|
+
self.entities = entities
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Registering a name again replaces its earlier intent. Deleting by `{ intent }` deletes by intent
|
|
104
|
+
/// type, so when two names are registered for one type, deleting by either name deletes the
|
|
105
|
+
/// donations made under both.
|
|
106
|
+
public func register<Intent: DonatableAppIntent>(_ name: String, as intentType: Intent.Type) {
|
|
107
|
+
let registration = Registration(intentType: intentType) { params, appContext in
|
|
108
|
+
let donationParams: Intent.DonationParams
|
|
109
|
+
do {
|
|
110
|
+
donationParams = try Intent.DonationParams.from(dictionary: params, appContext: appContext)
|
|
111
|
+
} catch {
|
|
112
|
+
throw InvalidDonationParamsException((intent: name, paramsType: "\(Intent.DonationParams.self)")).causedBy(
|
|
113
|
+
error
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
do {
|
|
117
|
+
return try await Intent(donationParams: donationParams)
|
|
118
|
+
} catch {
|
|
119
|
+
throw DonationIntentInitException((intent: name, error: error))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
registrations.withLock {
|
|
123
|
+
$0[name] = registration
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Builds the intent registered as `name` from `params`, donates it, and returns the donation id
|
|
128
|
+
/// JavaScript can later delete it by.
|
|
129
|
+
internal func donate(_ name: String, params: [String: Any], appContext: AppContext) async throws -> String {
|
|
130
|
+
let intent = try await registration(named: name).makeIntent(params, appContext)
|
|
131
|
+
return try encode(await donor.donate(intent))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Deletes the donations matching `filter`, and returns the ids of the ones that were deleted.
|
|
135
|
+
internal func deleteDonations(matching filter: AppIntentDonationFilter) async throws -> [String] {
|
|
136
|
+
switch filter {
|
|
137
|
+
case .ids(let ids):
|
|
138
|
+
// Every id is read before anything is deleted, so an unreadable id deletes nothing.
|
|
139
|
+
return try await deleteEach(ids.map { (id: $0, identifier: try decode($0)) })
|
|
140
|
+
case .intent(let name):
|
|
141
|
+
return try await donor.deleteDonations(matching: .intentType(registration(named: name).intentType)).map(encode)
|
|
142
|
+
case .entity(let entity, let id):
|
|
143
|
+
guard let identifier = entities.identifier(for: entity, id: id) else {
|
|
144
|
+
throw UnregisteredDonationEntityException((entity, id))
|
|
145
|
+
}
|
|
146
|
+
return try await donor.deleteDonations(matching: .entity(identifier)).map(encode)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// `.donationIdentifiers(_:)` needs iOS 26.4 and its SDK, so each id is deleted on its own. A failed
|
|
151
|
+
/// id does not stop the ids after it, and the exception names which ids were deleted and which not.
|
|
152
|
+
private func deleteEach(_ donations: [(id: String, identifier: IntentDonationIdentifier)]) async throws -> [String] {
|
|
153
|
+
var deleted: [IntentDonationIdentifier] = []
|
|
154
|
+
var failed: [String] = []
|
|
155
|
+
var firstError: (any Error)?
|
|
156
|
+
for donation in donations {
|
|
157
|
+
do {
|
|
158
|
+
deleted += try await donor.deleteDonations(matching: .donation(donation.identifier))
|
|
159
|
+
} catch {
|
|
160
|
+
failed.append(donation.id)
|
|
161
|
+
firstError = firstError ?? error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let deletedIds = try deleted.map(encode)
|
|
165
|
+
if let firstError {
|
|
166
|
+
throw PartialDonationDeletionException((deleted: deletedIds, failed: failed)).causedBy(firstError)
|
|
167
|
+
}
|
|
168
|
+
return deletedIds
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func registration(named name: String) throws -> Registration {
|
|
172
|
+
guard let registration = registrations.withLock({ $0[name] }) else {
|
|
173
|
+
throw UnregisteredDonationIntentException(name)
|
|
174
|
+
}
|
|
175
|
+
return registration
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func encode(_ identifier: IntentDonationIdentifier) throws -> String {
|
|
179
|
+
// `JSONEncoder` only writes valid UTF-8, so there is nothing for a failable conversion to catch.
|
|
180
|
+
// swiftlint:disable:next optional_data_string_conversion
|
|
181
|
+
return String(decoding: try JSONEncoder().encode(identifier), as: UTF8.self)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private func decode(_ id: String) throws -> IntentDonationIdentifier {
|
|
185
|
+
do {
|
|
186
|
+
return try JSONDecoder().decode(IntentDonationIdentifier.self, from: Data(id.utf8))
|
|
187
|
+
} catch {
|
|
188
|
+
throw InvalidDonationIdentifierException(id)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// The filter `deleteDonationsAsync()` receives. JavaScript types it as a union, so it arrives as one
|
|
194
|
+
/// object whose keys say which variant it is.
|
|
195
|
+
@Record
|
|
196
|
+
internal struct AppIntentDonationFilterRecord {
|
|
197
|
+
var ids: [String]?
|
|
198
|
+
var intent: String?
|
|
199
|
+
var entity: String?
|
|
200
|
+
var id: String?
|
|
201
|
+
|
|
202
|
+
func toFilter() throws -> AppIntentDonationFilter {
|
|
203
|
+
switch (ids, intent, entity, id) {
|
|
204
|
+
case (let ids?, nil, nil, nil):
|
|
205
|
+
return .ids(ids)
|
|
206
|
+
case (nil, let intent?, nil, nil):
|
|
207
|
+
return .intent(intent)
|
|
208
|
+
case (nil, nil, let entity?, let id?):
|
|
209
|
+
return .entity(entity, id: id)
|
|
210
|
+
default:
|
|
211
|
+
throw InvalidDonationFilterException()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
internal final class UnregisteredDonationIntentException: GenericException<String>, @unchecked Sendable {
|
|
217
|
+
override var reason: String {
|
|
218
|
+
return """
|
|
219
|
+
expo-app-intents has no intent registered for donation as '\(param)'. JavaScript can only name \
|
|
220
|
+
intents that app-target Swift has registered, because only the app target can build them. Make \
|
|
221
|
+
the intent conform to DonatableAppIntent, then add this to the OnCreate of your AppIntentsSetup \
|
|
222
|
+
module: AppIntentDonationRegistry.shared.register("\(param)", as: YourIntent.self)
|
|
223
|
+
"""
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
internal final class InvalidDonationIdentifierException: GenericException<String>, @unchecked Sendable {
|
|
228
|
+
override var reason: String {
|
|
229
|
+
return """
|
|
230
|
+
expo-app-intents could not read the donation id '\(param)', so no donations were deleted. \
|
|
231
|
+
Donation ids are opaque, and one that was changed or built by hand no longer names a donation. \
|
|
232
|
+
Pass the id exactly as donateIntentAsync() or deleteDonationsAsync() returned it.
|
|
233
|
+
"""
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
internal final class UnregisteredDonationEntityException: GenericException<(String, String)>, @unchecked Sendable {
|
|
238
|
+
override var reason: String {
|
|
239
|
+
let (entity, id) = param
|
|
240
|
+
return """
|
|
241
|
+
expo-app-intents could not delete donations for the '\(entity)' entity '\(id)'. This happens \
|
|
242
|
+
when no AppEntity is registered as '\(entity)'. It also happens when '\(id)' is not a valid \
|
|
243
|
+
identifier for that entity. Register the entity in the OnCreate of your AppIntentsSetup module with \
|
|
244
|
+
AppEntityIdentifierRegistry.shared.register("\(entity)", as: YourEntity.self), and pass an id \
|
|
245
|
+
that converts to the type of its id property.
|
|
246
|
+
"""
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// The record's own exception, which names the field, is the cause.
|
|
251
|
+
internal final class InvalidDonationParamsException: GenericException<(intent: String, paramsType: String)>,
|
|
252
|
+
@unchecked Sendable
|
|
253
|
+
{
|
|
254
|
+
override var reason: String {
|
|
255
|
+
return """
|
|
256
|
+
expo-app-intents could not donate the '\(param.intent)' intent, because the params passed to \
|
|
257
|
+
donateIntentAsync() do not fit its DonationParams record \(param.paramsType). Nothing was \
|
|
258
|
+
donated. Pass every required field of that record, with the type the record declares.
|
|
259
|
+
"""
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Keeps the app's error in the reason rather than using `causedBy(_:)`: a cause that is not an
|
|
264
|
+
/// `Exception` is described by its `localizedDescription`, which drops the text of a plain Swift error
|
|
265
|
+
/// such as a `CustomStringConvertible` struct, and this error comes from app code.
|
|
266
|
+
internal final class DonationIntentInitException: GenericException<(intent: String, error: any Error)>,
|
|
267
|
+
@unchecked Sendable
|
|
268
|
+
{
|
|
269
|
+
override var reason: String {
|
|
270
|
+
return """
|
|
271
|
+
expo-app-intents could not donate the '\(param.intent)' intent, because its \
|
|
272
|
+
init(donationParams:) threw: \(param.error). Nothing was donated. Check that the params passed \
|
|
273
|
+
to donateIntentAsync() match what init(donationParams:) of the intent registered as \
|
|
274
|
+
'\(param.intent)' expects.
|
|
275
|
+
"""
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// The first error the system reported is the cause.
|
|
280
|
+
internal final class PartialDonationDeletionException: GenericException<(deleted: [String], failed: [String])>,
|
|
281
|
+
@unchecked Sendable
|
|
282
|
+
{
|
|
283
|
+
override var reason: String {
|
|
284
|
+
return """
|
|
285
|
+
expo-app-intents could not delete every donation. Deleted: \(list(param.deleted)). Not \
|
|
286
|
+
deleted: \(list(param.failed)). Call deleteDonationsAsync() again with the ids that were not \
|
|
287
|
+
deleted.
|
|
288
|
+
"""
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private func list(_ ids: [String]) -> String {
|
|
292
|
+
return ids.isEmpty ? "none" : ids.map { "'\($0)'" }.joined(separator: ", ")
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
internal final class InvalidDonationFilterException: Exception, @unchecked Sendable {
|
|
297
|
+
override var reason: String {
|
|
298
|
+
return """
|
|
299
|
+
deleteDonationsAsync() needs exactly one of { ids }, { intent }, or { entity, id }, so it cannot \
|
|
300
|
+
tell which donations to delete. Pass one of those shapes, and call it once per filter to delete \
|
|
301
|
+
by more than one.
|
|
302
|
+
"""
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -138,6 +138,19 @@ public final class ExpoAppIntentsModule: Module, @unchecked Sendable {
|
|
|
138
138
|
AsyncFunction("refreshShortcutsAsync") { () async throws in
|
|
139
139
|
try await self.refreshShortcuts()
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
AsyncFunction("donateIntentAsync") { (name: String, params: [String: Any]?) async throws -> String in
|
|
143
|
+
guard let appContext = self.appContext else {
|
|
144
|
+
throw Exceptions.AppContextLost()
|
|
145
|
+
}
|
|
146
|
+
// The registry converts `params` to the `DonationParams` record of the intent registered as
|
|
147
|
+
// `name`, since only that registration knows the record type.
|
|
148
|
+
return try await AppIntentDonationRegistry.shared.donate(name, params: params ?? [:], appContext: appContext)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
AsyncFunction("deleteDonationsAsync") { (filter: AppIntentDonationFilterRecord) async throws -> [String] in
|
|
152
|
+
return try await AppIntentDonationRegistry.shared.deleteDonations(matching: filter.toFilter())
|
|
153
|
+
}
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
/// Installs the `appEntityIdentifier` factory. Both factories take the `AppContext` from the call
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-intents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Expose Apple App Intents (Siri, Shortcuts, Spotlight, Apple Intelligence) from Expo apps",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"homepage": "https://docs.expo.dev/versions/latest/sdk/app-intents",
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@testing-library/react-native": "^
|
|
31
|
+
"@testing-library/react-native": "^14.0.1",
|
|
32
32
|
"@types/jest": "^29.2.1",
|
|
33
33
|
"@types/node": "^22.14.0",
|
|
34
34
|
"@types/prompts": "^2.4.9",
|
|
35
35
|
"@types/react": "~19.3.0",
|
|
36
|
-
"expo": "58.0.
|
|
37
|
-
"expo-module-scripts": "56.0.
|
|
36
|
+
"expo": "58.0.2",
|
|
37
|
+
"expo-module-scripts": "56.0.5"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@expo/ui": "^58.0.
|
|
40
|
+
"@expo/ui": "^58.0.11",
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
42
|
"prompts": "^2.4.2"
|
|
43
43
|
},
|
|
@@ -87,6 +87,33 @@ export type AppEntityIdentifierModifier = ModifierConfig & {
|
|
|
87
87
|
id: string;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* A value that can be passed as an App Intent parameter: anything JSON can represent.
|
|
92
|
+
*/
|
|
93
|
+
export type AppIntentJSONValue =
|
|
94
|
+
| string
|
|
95
|
+
| number
|
|
96
|
+
| boolean
|
|
97
|
+
| null
|
|
98
|
+
| AppIntentJSONValue[]
|
|
99
|
+
| { [key: string]: AppIntentJSONValue };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Selects the donations that `deleteDonationsAsync()` deletes. Pass one of these shapes:
|
|
103
|
+
*
|
|
104
|
+
* - `{ ids }` deletes the donations with the given IDs, as returned by `donateIntentAsync()`.
|
|
105
|
+
* - `{ intent }` deletes every donation of the intent type registered under that name. When
|
|
106
|
+
* several names are registered for one intent type, this also deletes the donations made under
|
|
107
|
+
* the other names.
|
|
108
|
+
* - `{ entity, id }` deletes every donation that refers to the given entity. The `entity` value
|
|
109
|
+
* must be registered with `AppEntityIdentifierRegistry.shared.register(_:as:)` or
|
|
110
|
+
* `AppEntityIdentifierRegistry.shared.registerIndexed(_:as:)`.
|
|
111
|
+
*/
|
|
112
|
+
export type AppIntentDonationFilter =
|
|
113
|
+
| { ids: string[] }
|
|
114
|
+
| { intent: string }
|
|
115
|
+
| { entity: string; id: string };
|
|
116
|
+
|
|
90
117
|
export type ExpoAppIntentsModuleEvents = {
|
|
91
118
|
onIntent: (invocation: AppIntentInvocation) => void;
|
|
92
119
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NativeModule, requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
AppIntentDonationFilter,
|
|
4
5
|
AppIntentEntity,
|
|
5
6
|
AppIntentInvocation,
|
|
7
|
+
AppIntentJSONValue,
|
|
6
8
|
ExpoAppIntentsModuleEvents,
|
|
7
9
|
} from './ExpoAppIntents.types';
|
|
8
10
|
|
|
@@ -14,6 +16,8 @@ declare class ExpoAppIntentsNativeModule extends NativeModule<ExpoAppIntentsModu
|
|
|
14
16
|
reindexEntitiesAsync(kind: string | null): Promise<void>;
|
|
15
17
|
getEntityCatalogAsync(kind: string): Promise<AppIntentEntity[]>;
|
|
16
18
|
refreshShortcutsAsync(): Promise<void>;
|
|
19
|
+
donateIntentAsync(name: string, params?: Record<string, AppIntentJSONValue>): Promise<string>;
|
|
20
|
+
deleteDonationsAsync(filter: AppIntentDonationFilter): Promise<string[]>;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export default requireOptionalNativeModule<ExpoAppIntentsNativeModule>('ExpoAppIntents');
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { useEffect, useRef } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
AppEntityIdentifierModifier,
|
|
6
|
+
AppIntentDonationFilter,
|
|
6
7
|
AppIntentEntity,
|
|
7
8
|
AppIntentInvocation,
|
|
9
|
+
AppIntentJSONValue,
|
|
8
10
|
AppIntentsHandler,
|
|
9
11
|
} from './ExpoAppIntents.types';
|
|
10
12
|
import ExpoAppIntents from './ExpoAppIntentsModule';
|
|
@@ -266,6 +268,61 @@ export async function refreshShortcutsAsync(): Promise<void> {
|
|
|
266
268
|
return ExpoAppIntents.refreshShortcutsAsync();
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Tells the system that the user just performed an App Intent's action in your app, for example
|
|
273
|
+
* ordering food from a screen rather than through Siri. The system learns from donations and can
|
|
274
|
+
* suggest the action later on the Lock Screen, in Siri Suggestions, and in Spotlight.
|
|
275
|
+
*
|
|
276
|
+
* The `name` must be registered from app-target Swift with
|
|
277
|
+
* `AppIntentDonationRegistry.shared.register(_:as:)`, on an intent that conforms to
|
|
278
|
+
* `DonatableAppIntent`. Native code converts `params` to the `DonationParams` record of that intent,
|
|
279
|
+
* and the intent builds itself from the record in its `init(donationParams:)`.
|
|
280
|
+
*
|
|
281
|
+
* The returned promise is fulfilled with an ID for the donation, which
|
|
282
|
+
* [`deleteDonationsAsync()`](#appintentsdeletedonationsasyncfilter) accepts, or with `null` when
|
|
283
|
+
* App Intents are unavailable. It is rejected when no intent is registered as `name`, when `params`
|
|
284
|
+
* misses a required field of the record or has a field of the wrong type, when the intent cannot be
|
|
285
|
+
* built from the record, or when the system fails to record the donation.
|
|
286
|
+
*
|
|
287
|
+
* > **Note:** The ID is the system's donation identifier in its encoded form. Store it only for as
|
|
288
|
+
* > long as you need it. An iOS update may change how the system encodes identifiers, and an ID
|
|
289
|
+
* > stored before such an update may then be rejected. To delete donations without stored IDs,
|
|
290
|
+
* > delete by `intent` or by `entity`.
|
|
291
|
+
*
|
|
292
|
+
* @platform ios
|
|
293
|
+
*/
|
|
294
|
+
export async function donateIntentAsync(
|
|
295
|
+
name: string,
|
|
296
|
+
params?: Record<string, AppIntentJSONValue>
|
|
297
|
+
): Promise<string | null> {
|
|
298
|
+
if (!ExpoAppIntents) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return ExpoAppIntents.donateIntentAsync(name, params);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Deletes donations made with [`donateIntentAsync()`](#appintentsdonateintentasyncname-params), so
|
|
306
|
+
* the system stops suggesting them. Delete donations when what they refer to is gone, for example
|
|
307
|
+
* after the user deletes an order, or when the user signs out.
|
|
308
|
+
*
|
|
309
|
+
* The returned promise is fulfilled with the IDs of the deleted donations, or with an empty array
|
|
310
|
+
* when App Intents are unavailable. It is rejected when `filter` names an intent or entity that is
|
|
311
|
+
* not registered, contains an ID that cannot be read, or contains an entity `id` that cannot be
|
|
312
|
+
* converted to the ID type of that entity. In those cases nothing is deleted. With `ids`, every ID
|
|
313
|
+
* is attempted. When the system fails to delete one or more of them, the promise is rejected with
|
|
314
|
+
* an error that lists the deleted IDs and the IDs that were not deleted. With `intent` or
|
|
315
|
+
* `entity`, the promise is rejected with the system error when the system fails to delete.
|
|
316
|
+
*
|
|
317
|
+
* @platform ios
|
|
318
|
+
*/
|
|
319
|
+
export async function deleteDonationsAsync(filter: AppIntentDonationFilter): Promise<string[]> {
|
|
320
|
+
if (!ExpoAppIntents) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
return ExpoAppIntents.deleteDonationsAsync(filter);
|
|
324
|
+
}
|
|
325
|
+
|
|
269
326
|
/**
|
|
270
327
|
* Returns an ExpoUI SwiftUI modifier config that ties a view to an AppEntity identifier.
|
|
271
328
|
*
|