expo-invoke 2.6.15 → 2.7.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 (149) hide show
  1. package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +35 -0
  2. package/android/src/main/java/expo/modules/invoke/InvokeSliceProvider.kt +9 -0
  3. package/build/plugin/codegen/generateSwiftIntents.js +22 -0
  4. package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
  5. package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
  6. package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
  7. package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
  8. package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
  9. package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
  10. package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
  11. package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
  12. package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
  13. package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
  14. package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
  15. package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
  16. package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
  17. package/build/plugin/ios/widgets/withEasConfig.js +52 -0
  18. package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
  19. package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
  20. package/build/plugin/ios/widgets/withIosWarning.js +10 -0
  21. package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
  22. package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
  23. package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
  24. package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
  25. package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
  26. package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
  27. package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
  28. package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
  29. package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
  30. package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
  31. package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
  32. package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
  33. package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
  34. package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
  35. package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
  36. package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
  37. package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
  38. package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
  39. package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
  40. package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
  41. package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
  42. package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
  43. package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
  44. package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
  45. package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
  46. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
  47. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
  48. package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
  49. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
  50. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
  51. package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
  52. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
  53. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
  54. package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
  55. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
  56. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
  57. package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
  58. package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
  59. package/build/plugin/ios/withIOSInvoke.js +54 -25
  60. package/build/plugin/ios/withIOSInvoke.js.map +1 -1
  61. package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
  62. package/build/plugin/ios/withWidgetExtension.js +5 -0
  63. package/build/plugin/ios/withWidgetExtension.js.map +1 -1
  64. package/build/plugin/src/withInvoke.d.ts +34 -23
  65. package/build/plugin/src/withInvoke.js +16 -27
  66. package/build/plugin/src/withInvoke.js.map +1 -1
  67. package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
  68. package/build/plugin/types/WidgetConfig.type.js +3 -0
  69. package/build/plugin/types/WidgetConfig.type.js.map +1 -0
  70. package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
  71. package/build/plugin/types/WidgetFamily.type.js +13 -0
  72. package/build/plugin/types/WidgetFamily.type.js.map +1 -0
  73. package/build/src/hooks/useLiveActivity.d.ts +20 -10
  74. package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
  75. package/build/src/hooks/useLiveActivity.js +20 -10
  76. package/build/src/hooks/useLiveActivity.js.map +1 -1
  77. package/build/src/index.d.ts +2 -0
  78. package/build/src/index.d.ts.map +1 -1
  79. package/build/src/index.js +5 -0
  80. package/build/src/index.js.map +1 -1
  81. package/build/src/module.d.ts +4 -0
  82. package/build/src/module.d.ts.map +1 -1
  83. package/build/src/module.js +8 -0
  84. package/build/src/module.js.map +1 -1
  85. package/build/src/web/index.d.ts +10 -1
  86. package/build/src/web/index.d.ts.map +1 -1
  87. package/build/src/web/index.js +4 -0
  88. package/build/src/web/index.js.map +1 -1
  89. package/build/src/widgets/ExpoWidgets.d.ts +12 -0
  90. package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
  91. package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
  92. package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
  93. package/build/src/widgets/ExpoWidgets.ios.js +3 -0
  94. package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
  95. package/build/src/widgets/ExpoWidgets.js +42 -0
  96. package/build/src/widgets/ExpoWidgets.js.map +1 -0
  97. package/build/src/widgets/Widgets.d.ts +119 -0
  98. package/build/src/widgets/Widgets.d.ts.map +1 -0
  99. package/build/src/widgets/Widgets.js +158 -0
  100. package/build/src/widgets/Widgets.js.map +1 -0
  101. package/build/src/widgets/Widgets.types.d.ts +263 -0
  102. package/build/src/widgets/Widgets.types.d.ts.map +1 -0
  103. package/build/src/widgets/Widgets.types.js +2 -0
  104. package/build/src/widgets/Widgets.types.js.map +1 -0
  105. package/build/src/widgets/index.d.ts +3 -0
  106. package/build/src/widgets/index.d.ts.map +1 -0
  107. package/build/src/widgets/index.js +2 -0
  108. package/build/src/widgets/index.js.map +1 -0
  109. package/bundle/__tests__/decorator.test.ts +89 -0
  110. package/bundle/decorator.ts +61 -0
  111. package/bundle/expo-modules-core-stub.ts +1 -0
  112. package/bundle/expo-stub.ts +15 -0
  113. package/bundle/index.ts +69 -0
  114. package/bundle/jsx-runtime-stub.ts +71 -0
  115. package/bundle/react-native-stub.ts +3 -0
  116. package/bundle/react-stub.ts +13 -0
  117. package/expo-module.config.json +1 -1
  118. package/ios/LiveActivity/LiveActivityManager.swift +15 -14
  119. package/ios/Module/ExpoInvokeModule.swift +124 -0
  120. package/ios/Widget/WidgetManager.swift +25 -8
  121. package/ios/Widgets/LiveActivity.swift +81 -0
  122. package/ios/Widgets/LiveActivityFactory.swift +49 -0
  123. package/ios/Widgets/Widget/AppIntent.swift +130 -0
  124. package/ios/Widgets/Widget/Buttons.swift +62 -0
  125. package/ios/Widgets/Widget/DynamicView.swift +161 -0
  126. package/ios/Widgets/Widget/EntryView.swift +36 -0
  127. package/ios/Widgets/Widget/Fragment.swift +13 -0
  128. package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
  129. package/ios/Widgets/Widget/RedBoxView.swift +83 -0
  130. package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
  131. package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
  132. package/ios/Widgets/Widget/Type.swift +6 -0
  133. package/ios/Widgets/Widget/Utils.swift +116 -0
  134. package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
  135. package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
  136. package/ios/Widgets/WidgetObject.swift +31 -0
  137. package/ios/Widgets/WidgetsContext.swift +7 -0
  138. package/ios/Widgets/WidgetsEvents.swift +16 -0
  139. package/ios/Widgets/WidgetsExceptions.swift +25 -0
  140. package/ios/Widgets/WidgetsModule.swift +149 -0
  141. package/ios/Widgets/WidgetsRecords.swift +22 -0
  142. package/ios/Widgets/WidgetsStorage.swift +58 -0
  143. package/metro.widget.config.js +76 -0
  144. package/package.json +11 -1
  145. package/scripts/autolinking.rb +34 -0
  146. package/scripts/build-bundle.mjs +69 -0
  147. package/scripts/with-node.sh +44 -0
  148. package/scripts/xcode-build-bundle.sh +23 -0
  149. package/spm.config.json +40 -0
@@ -0,0 +1,89 @@
1
+ import '../index';
2
+ import { jsx, jsxs } from '../jsx-runtime-stub';
3
+
4
+ jest.mock('@expo/ui/swift-ui', () => ({}));
5
+ jest.mock('@expo/ui/swift-ui/modifiers', () => ({}));
6
+
7
+ describe('jsx-runtime-stub', () => {
8
+ afterEach(() => {
9
+ delete globalThis.__expoWidgetLayout;
10
+ });
11
+
12
+ it('adds button targets during render', () => {
13
+ globalThis.__expoWidgetLayout = () =>
14
+ jsxs('View', {
15
+ children: [
16
+ jsx('Button', { label: 'First', onButtonPress: () => ({ id: 'first' }) }),
17
+ jsx('Button', { label: 'Second', onButtonPress: () => ({ id: 'second' }) }),
18
+ ],
19
+ });
20
+
21
+ const tree = globalThis.__expoWidgetRender({}, { timestamp: 1 }) as any;
22
+
23
+ expect(tree.props.children[0].props.target).toBe('__expo_widgets_target_0');
24
+ expect(tree.props.children[1].props.target).toBe('__expo_widgets_target_1');
25
+ });
26
+
27
+ it('uses the nearest keyed parent when generating button targets', () => {
28
+ globalThis.__expoWidgetLayout = () =>
29
+ jsx(
30
+ 'Row',
31
+ {
32
+ children: jsx('Wrapper', {
33
+ children: jsx('Button', {
34
+ label: 'Press',
35
+ onButtonPress: () => ({ id: 'nested' }),
36
+ }),
37
+ }),
38
+ },
39
+ 'row-1'
40
+ );
41
+
42
+ const tree = globalThis.__expoWidgetRender({}, { timestamp: 1 }) as any;
43
+
44
+ expect(tree.props.children.props.children.props.target).toBe('__expo_widgets_target_0_row-1');
45
+ });
46
+
47
+ it('preserves explicit button targets', () => {
48
+ globalThis.__expoWidgetLayout = () =>
49
+ jsx('Button', {
50
+ label: 'Custom',
51
+ target: 'custom-target',
52
+ onButtonPress: () => ({ id: 'custom' }),
53
+ });
54
+
55
+ const tree = globalThis.__expoWidgetRender({}, { timestamp: 1 }) as any;
56
+
57
+ expect(tree.props.target).toBe('custom-target');
58
+ });
59
+
60
+ it('reuses generated button targets when handling presses', () => {
61
+ const firstPress = jest.fn(() => ({ id: 'first' }));
62
+ const secondPress = jest.fn(() => ({ id: 'second' }));
63
+
64
+ globalThis.__expoWidgetLayout = () =>
65
+ jsxs('View', {
66
+ children: [
67
+ jsx(
68
+ 'Row',
69
+ { children: jsx('Button', { label: 'First', onButtonPress: firstPress }) },
70
+ 'row-1'
71
+ ),
72
+ jsx(
73
+ 'Row',
74
+ { children: jsx('Button', { label: 'Second', onButtonPress: secondPress }) },
75
+ 'row-2'
76
+ ),
77
+ ],
78
+ });
79
+
80
+ const tree = globalThis.__expoWidgetRender({}, { timestamp: 1 }) as any;
81
+ const secondTarget = tree.props.children[1].props.children.props.target;
82
+
83
+ const result = globalThis.__expoWidgetHandlePress({}, { timestamp: 1, target: secondTarget });
84
+
85
+ expect(result).toEqual({ id: 'second' });
86
+ expect(firstPress).not.toHaveBeenCalled();
87
+ expect(secondPress).toHaveBeenCalledTimes(1);
88
+ });
89
+ });
@@ -0,0 +1,61 @@
1
+ import { ReactElementNode } from './jsx-runtime-stub';
2
+
3
+ export function decorateInteractiveTargets(node: unknown) {
4
+ return decorateNode(node, {
5
+ nearestParentKey: null,
6
+ nextTargetIndex: {
7
+ current: 0,
8
+ },
9
+ typesToDecorate: ['Button'],
10
+ });
11
+ }
12
+
13
+ function decorateNode(
14
+ node: unknown,
15
+ context: {
16
+ nearestParentKey: string | null;
17
+ nextTargetIndex: {
18
+ current: number;
19
+ };
20
+ typesToDecorate: string[];
21
+ }
22
+ ): unknown {
23
+ if (Array.isArray(node)) {
24
+ return node.map((child) => decorateNode(child, context));
25
+ }
26
+
27
+ if (!isReactElementNode(node)) {
28
+ return node;
29
+ }
30
+
31
+ const props = node.props ?? {};
32
+
33
+ if (props.target == null && context.typesToDecorate.includes(node.type as string)) {
34
+ props.target = buildButtonTargetId(context.nextTargetIndex.current, context.nearestParentKey);
35
+ context.nextTargetIndex.current += 1;
36
+ }
37
+
38
+ if ('children' in props) {
39
+ props.children = decorateNode(props.children, {
40
+ nearestParentKey: node.key ?? context.nearestParentKey,
41
+ nextTargetIndex: context.nextTargetIndex,
42
+ typesToDecorate: context.typesToDecorate,
43
+ });
44
+ }
45
+
46
+ return node;
47
+ }
48
+
49
+ function isReactElementNode(node: unknown): node is ReactElementNode {
50
+ return Boolean(node) && typeof node === 'object' && 'type' in node && 'props' in node;
51
+ }
52
+
53
+ function buildButtonTargetId(index: number, parentKey: string | null) {
54
+ const baseTarget = `__expo_widgets_target_${index}`;
55
+
56
+ if (!parentKey) {
57
+ return baseTarget;
58
+ }
59
+
60
+ return `${baseTarget}_${parentKey}`;
61
+ }
@@ -0,0 +1 @@
1
+ export function useReleasingSharedObject() {}
@@ -0,0 +1,15 @@
1
+ export function requireNativeModule(name: string) {
2
+ return {};
3
+ }
4
+
5
+ export function requireNativeView(moduleName: string, viewName: string) {
6
+ const wrapper = {
7
+ [viewName]: (props?: Record<string, any>) => {
8
+ return {
9
+ type: viewName,
10
+ props: props || {},
11
+ };
12
+ },
13
+ };
14
+ return wrapper[viewName] as unknown as React.ComponentType<any>;
15
+ }
@@ -0,0 +1,69 @@
1
+ /* eslint-disable no-var */
2
+
3
+ import * as swiftUI from '@expo/ui/swift-ui';
4
+ import * as modifiers from '@expo/ui/swift-ui/modifiers';
5
+
6
+ import { decorateInteractiveTargets } from './decorator';
7
+ import * as jsxRuntime from './jsx-runtime-stub';
8
+ import * as ReactNative from './react-native-stub';
9
+ import * as React from './react-stub';
10
+
11
+ type Dictionary = Record<string, unknown>;
12
+
13
+ declare global {
14
+ var __expoWidgetLayout: (props: Dictionary, environment: Dictionary) => Dictionary;
15
+ var __expoWidgetRender: (props: Dictionary, environment: Dictionary) => Dictionary;
16
+ var __expoWidgetHandlePress: (
17
+ props: Dictionary,
18
+ environment: Dictionary & { target?: string }
19
+ ) => Dictionary | undefined;
20
+ }
21
+
22
+ const __expoWidgetRender = function (props: Dictionary, environment: Dictionary) {
23
+ const { timestamp, ...rest } = environment;
24
+ const decoratedEnvironment: Dictionary = { ...rest };
25
+ if (timestamp) {
26
+ decoratedEnvironment.date = new Date(timestamp as number);
27
+ }
28
+
29
+ return decorateInteractiveTargets(globalThis.__expoWidgetLayout(props, decoratedEnvironment));
30
+ };
31
+
32
+ const __expoWidgetHandlePress = function (
33
+ props: Dictionary,
34
+ environment: Dictionary & { target?: string }
35
+ ) {
36
+ const { target, ...renderEnvironment } = environment;
37
+
38
+ function findAndCallOnPress(node?: Dictionary): Dictionary | undefined {
39
+ const props = node?.props as {
40
+ onButtonPress?: () => Dictionary;
41
+ target?: string;
42
+ children?: unknown;
43
+ };
44
+ if (props?.onButtonPress && props?.target === target) {
45
+ return props.onButtonPress();
46
+ }
47
+
48
+ for (const child of React.Children.toArray(props?.children)) {
49
+ const result = findAndCallOnPress(child as Dictionary);
50
+ if (result) {
51
+ return result;
52
+ }
53
+ }
54
+ }
55
+
56
+ const node = globalThis.__expoWidgetRender(props, renderEnvironment);
57
+ return findAndCallOnPress(node as Dictionary);
58
+ };
59
+
60
+ Object.assign(globalThis, {
61
+ ...swiftUI,
62
+ ...modifiers,
63
+ ...jsxRuntime,
64
+ ...React,
65
+ ...ReactNative,
66
+ React,
67
+ __expoWidgetRender,
68
+ __expoWidgetHandlePress,
69
+ });
@@ -0,0 +1,71 @@
1
+ import { Fragment } from './react-stub';
2
+
3
+ export type ReactElementNode = {
4
+ type: unknown;
5
+ key: string | null;
6
+ props: Record<string, any>;
7
+ };
8
+
9
+ function ReactElement(
10
+ type: unknown,
11
+ key: string | null,
12
+ props: Record<string, any>
13
+ ): ReactElementNode {
14
+ if (typeof type === 'function') {
15
+ return (type as any)(props);
16
+ }
17
+
18
+ const element = {
19
+ type,
20
+ key,
21
+ props,
22
+ };
23
+
24
+ return element;
25
+ }
26
+
27
+ function jsxProd(
28
+ type: unknown,
29
+ config: any,
30
+ maybeKey?: string | number | bigint
31
+ ): ReactElementNode {
32
+ let key = null;
33
+ if (maybeKey !== undefined) {
34
+ key = '' + maybeKey;
35
+ }
36
+ if (hasValidKey(config)) {
37
+ key = '' + config.key;
38
+ }
39
+
40
+ let props: Record<string, any>;
41
+ if (!('key' in config)) {
42
+ props = config;
43
+ } else {
44
+ props = {};
45
+ for (const propName in config) {
46
+ if (propName !== 'key') {
47
+ props[propName] = config[propName];
48
+ }
49
+ }
50
+ }
51
+
52
+ return ReactElement(type, key, props);
53
+ }
54
+
55
+ function hasValidKey(config: any) {
56
+ return config.key !== undefined;
57
+ }
58
+
59
+ const jsxFileName = 'widget';
60
+ export {
61
+ Fragment,
62
+ Fragment as _Fragment,
63
+ jsxFileName as _jsxFileName,
64
+ jsxProd,
65
+ jsxProd as jsx,
66
+ jsxProd as jsxs,
67
+ jsxProd as jsxDEV,
68
+ jsxProd as _jsx,
69
+ jsxProd as _jsxs,
70
+ jsxProd as _jsxDEV,
71
+ };
@@ -0,0 +1,3 @@
1
+ export const PlatformColor = (...names: string[]) => {
2
+ return { semantic: names };
3
+ };
@@ -0,0 +1,13 @@
1
+ export const Fragment = 'react.fragment';
2
+
3
+ export const Children = {
4
+ toArray(children: unknown) {
5
+ if (children === undefined || children === null) {
6
+ return [];
7
+ }
8
+ return Array.isArray(children) ? children : [children];
9
+ },
10
+ };
11
+ export const isValidElement = (value: unknown) => {
12
+ return Boolean(value) && typeof value === 'object' && 'type' in (value as object);
13
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "platforms": ["ios", "android"],
3
3
  "ios": {
4
- "modules": ["ExpoInvokeModule"]
4
+ "modules": ["ExpoInvokeModule", "WidgetsModule"]
5
5
  },
6
6
  "android": {
7
7
  "modules": ["expo.modules.invoke.ExpoInvokeModule"]
@@ -23,21 +23,18 @@ import Foundation
23
23
  throw LiveActivityError.invalidConfig
24
24
  }
25
25
 
26
- let activityType = config["activityType"] as? String ?? "UnknownActivity"
27
- let contentState = config["contentState"] as? [String: Any] ?? [:]
28
- let staleDate = config["staleDate"].flatMap { ($0 as? Double).map { Date(timeIntervalSince1970: $0 / 1000) } }
29
- let relevance = config["relevanceScore"] as? Double
30
-
31
26
  let activityId = UUID().uuidString
32
27
 
33
- // Store config for update/end actual ActivityKit usage requires generated ActivityAttributes
34
- // types, which the codegen produces. We store the info for the bridge layer to use.
35
- lock.lock()
36
- // Store a placeholder; concrete Activity objects are managed in generated code via
37
- // LiveActivityRegistry (generated by plugin/codegen/generateSwiftLiveActivities.ts)
38
- lock.unlock()
28
+ // When ActivityKit is available (iOS 16.1+) AND the plugin has run `npx expo prebuild`
29
+ // with `enableLiveActivities: true`, the generated InvokeLiveActivities.swift provides
30
+ // a real ActivityKit-backed extension on this class. That extension is called here.
31
+ // Without prebuild, activityId is returned but the activity is never registered.
32
+ #if canImport(ActivityKit)
33
+ if #available(iOS 16.1, *) {
34
+ try self.createActivity(activityId: activityId, config: config)
35
+ }
36
+ #endif
39
37
 
40
- _ = staleDate; _ = relevance; _ = contentState; _ = activityType
41
38
  return activityId
42
39
  }
43
40
 
@@ -72,8 +69,11 @@ import Foundation
72
69
  }
73
70
  }
74
71
 
75
- // LiveActivityRegistry is generated by generateSwiftLiveActivities.ts codegen.
76
- // Provide a stub so the module compiles without codegen having run yet.
72
+ // LiveActivityRegistry is generated by generateSwiftLiveActivities.ts codegen
73
+ // (runs during `npx expo prebuild` when enableLiveActivities: true).
74
+ // This stub compiles only when ActivityKit is unavailable so it doesn't conflict
75
+ // with the real implementation in InvokeLiveActivities.swift.
76
+ #if !canImport(ActivityKit)
77
77
  @objc public class LiveActivityRegistry: NSObject {
78
78
  @objc public static let shared = LiveActivityRegistry()
79
79
  private override init() {}
@@ -81,3 +81,4 @@ import Foundation
81
81
  @objc public func end(activityId: String, finalStateJson: String?) {}
82
82
  @objc public func listActivities() -> [[String: Any]] { return [] }
83
83
  }
84
+ #endif
@@ -123,6 +123,130 @@ public class ExpoInvokeModule: Module {
123
123
  )
124
124
  }
125
125
 
126
+ // ── Widget reload ─────────────────────────────────────────────────────────
127
+ Function("reloadWidget") { (widgetKind: String?) in
128
+ if #available(iOS 14.0, *) {
129
+ WidgetManager.reload(kind: widgetKind)
130
+ }
131
+ }
132
+
133
+ // ── Live Activity lifecycle (iOS 16.1+) ───────────────────────────────────
134
+ AsyncFunction("startLiveActivity") { (configJson: String) -> String in
135
+ if #available(iOS 16.1, *) {
136
+ return try await LiveActivityManager.shared.startActivity(configJson: configJson)
137
+ }
138
+ return ""
139
+ }
140
+
141
+ Function("updateLiveActivity") { (activityId: String, stateJson: String) in
142
+ if #available(iOS 16.1, *) {
143
+ LiveActivityManager.shared.updateActivity(activityId: activityId, stateJson: stateJson)
144
+ }
145
+ }
146
+
147
+ Function("endLiveActivity") { (activityId: String, finalStateJson: String?) in
148
+ if #available(iOS 16.1, *) {
149
+ LiveActivityManager.shared.endActivity(activityId: activityId, finalStateJson: finalStateJson)
150
+ }
151
+ }
152
+
153
+ AsyncFunction("getLiveActivities") { () -> String in
154
+ if #available(iOS 16.1, *) {
155
+ let activities = await LiveActivityManager.shared.listActivities()
156
+ if let data = try? JSONSerialization.data(withJSONObject: activities),
157
+ let json = String(data: data, encoding: .utf8) {
158
+ return json
159
+ }
160
+ }
161
+ return "[]"
162
+ }
163
+
164
+ // ── Focus filter result (iOS 16+) ─────────────────────────────────────────
165
+ Function("reportFocusFilterResult") { (filterId: String, success: Bool) in
166
+ if #available(iOS 16.0, *) {
167
+ FocusFilterRegistrar.shared.reportResult(filterId: filterId, success: success)
168
+ }
169
+ }
170
+
171
+ // ── App Entities (iOS 16.4+) ──────────────────────────────────────────────
172
+ Function("provideEntityResults") { (entityId: String, resultsJson: String) in
173
+ if #available(iOS 16.4, *) {
174
+ guard let data = resultsJson.data(using: .utf8),
175
+ let results = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
176
+ return
177
+ }
178
+ EntityQueryBridge.shared.receiveResults(entityId: entityId, results: results)
179
+ }
180
+ }
181
+
182
+ // ── Background intent (iOS 16+) ───────────────────────────────────────────
183
+ Function("registerBackgroundHandler") { (_: String) in
184
+ // No-op: the bridge routes events via writePendingIntent; JS registers via addIntentListener.
185
+ }
186
+
187
+ Function("reportBackgroundResult") { (intentId: String, resultJson: String) in
188
+ if #available(iOS 16.0, *) {
189
+ guard let data = resultJson.data(using: .utf8),
190
+ let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
191
+ return
192
+ }
193
+ let success = result["success"] as? Bool ?? false
194
+ let spokenResponse = result["spokenResponse"] as? String
195
+ let error = result["error"] as? String
196
+ ExpoInvokeBackgroundBridge.shared.reportResult(
197
+ intentId: intentId,
198
+ success: success,
199
+ spokenResponse: spokenResponse,
200
+ error: error
201
+ )
202
+ }
203
+ }
204
+
205
+ // ── Push-to-Talk (iOS 16+) ────────────────────────────────────────────────
206
+ AsyncFunction("startPTTSession") { (channelId: String, participantName: String?) in
207
+ if #available(iOS 16.0, *) {
208
+ try await PTTManager.shared.startSession(channelId: channelId, participantName: participantName)
209
+ }
210
+ }
211
+
212
+ AsyncFunction("endPTTSession") { (channelId: String) in
213
+ if #available(iOS 16.0, *) {
214
+ try await PTTManager.shared.endSession(channelId: channelId)
215
+ }
216
+ }
217
+
218
+ AsyncFunction("setTransmitting") { (channelId: String, isTransmitting: Bool) in
219
+ if #available(iOS 16.0, *) {
220
+ try await PTTManager.shared.setTransmitting(isTransmitting, channelId: channelId)
221
+ }
222
+ }
223
+
224
+ AsyncFunction("getPTTState") { (channelId: String) -> String in
225
+ let state: [String: Any]
226
+ if #available(iOS 16.0, *) {
227
+ state = PTTManager.shared.getState(channelId: channelId)
228
+ } else {
229
+ state = ["isActive": false, "isTransmitting": false]
230
+ }
231
+ guard let data = try? JSONSerialization.data(withJSONObject: state),
232
+ let json = String(data: data, encoding: .utf8) else {
233
+ return "{\"isActive\":false,\"isTransmitting\":false}"
234
+ }
235
+ return json
236
+ }
237
+
238
+ // ── Android Slices — no-op on iOS ─────────────────────────────────────────
239
+ Function("notifySliceChange") { (_: String) in /* iOS no-op */ }
240
+
241
+ // ── Persistent analytics — managed in JS, no native storage needed ────────
242
+ AsyncFunction("getAnalyticsPersisted") { () -> String in
243
+ return "{\"fired\":{},\"donated\":{},\"bySource\":{}}"
244
+ }
245
+
246
+ AsyncFunction("clearAnalyticsPersisted") {
247
+ // No-op: JS layer handles clearing via expo-file-system.
248
+ }
249
+
126
250
  // ── Focus mode ────────────────────────────────────────────────────────────
127
251
  Function("getFocusMode") { () -> String? in
128
252
  return FocusFilterManager.currentFocusMode()
@@ -1,19 +1,36 @@
1
1
  import Foundation
2
2
 
3
- /// Triggers a reload of all WidgetKit widget timelines so they display fresh data
4
- /// after setWidgetData() is called.
3
+ /// Triggers WidgetKit timeline reloads via reflection to avoid hard-linking WidgetKit.
5
4
  public struct WidgetManager {
5
+
6
6
  @available(iOS 14.0, *)
7
7
  public static func reloadAll() {
8
- // WidgetCenter is in WidgetKit use performSelector to avoid hard linking
9
- // the framework in packages that don't need it.
10
- if let widgetCenter = NSClassFromString("WidgetKit.WidgetCenter") as? NSObject.Type,
11
- let center = widgetCenter.value(forKey: "shared") as? NSObject {
12
- center.performSelector(inBackground: #selector(NSObject.reloadAllTimelines), with: nil)
8
+ guard let center = widgetCenter() else { return }
9
+ center.performSelector(inBackground: #selector(NSObject._invokeReloadAllTimelines), with: nil)
10
+ }
11
+
12
+ /// Reload a specific widget kind. Falls back to reloading all timelines if kind is nil.
13
+ @available(iOS 14.0, *)
14
+ public static func reload(kind: String?) {
15
+ guard let kind = kind, !kind.isEmpty else {
16
+ reloadAll()
17
+ return
13
18
  }
19
+ guard let center = widgetCenter() else { return }
20
+ let sel = NSSelectorFromString("reloadTimelinesOfKind:")
21
+ if center.responds(to: sel) {
22
+ center.perform(sel, with: kind)
23
+ } else {
24
+ center.performSelector(inBackground: #selector(NSObject._invokeReloadAllTimelines), with: nil)
25
+ }
26
+ }
27
+
28
+ private static func widgetCenter() -> NSObject? {
29
+ guard let cls = NSClassFromString("WidgetKit.WidgetCenter") as? NSObject.Type else { return nil }
30
+ return cls.value(forKey: "shared") as? NSObject
14
31
  }
15
32
  }
16
33
 
17
34
  extension NSObject {
18
- @objc func reloadAllTimelines() {}
35
+ @objc func _invokeReloadAllTimelines() {}
19
36
  }
@@ -0,0 +1,81 @@
1
+ import ExpoModulesCore
2
+ import ActivityKit
3
+
4
+ final class LiveActivity: SharedObject {
5
+ let id: String
6
+ let name: String
7
+ private var pushTokenObserverTask: Task<Void, Never>?
8
+
9
+ init(id: String, name: String) {
10
+ self.id = id
11
+ self.name = name
12
+ super.init()
13
+ }
14
+
15
+ func update(props: String?) async throws {
16
+ guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() }
17
+
18
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
19
+ throw LiveActivityNotFoundException(id)
20
+ }
21
+
22
+ let newState = LiveActivityAttributes.ContentState(name: name, props: props)
23
+ await activity.update(ActivityContent(state: newState, staleDate: nil))
24
+ }
25
+
26
+ func end(dismissalPolicy: LiveActivityDismissalPolicy?, afterDate: Date?, props: String?, contentDate: Date?) async throws {
27
+ guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() }
28
+
29
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
30
+ throw LiveActivityNotFoundException(id)
31
+ }
32
+
33
+ let content: ActivityContent<LiveActivityAttributes.ContentState>?
34
+ if let props {
35
+ content = ActivityContent(state: LiveActivityAttributes.ContentState(name: name, props: props), staleDate: nil)
36
+ } else {
37
+ content = nil
38
+ }
39
+
40
+ guard let contentDate, #available(iOS 17.2, *) else {
41
+ return await activity.end(content, dismissalPolicy: dismissalPolicy?.toDismissalPolicy(date: afterDate) ?? .default)
42
+ }
43
+
44
+ await activity.end(content, dismissalPolicy: dismissalPolicy?.toDismissalPolicy(date: afterDate) ?? .default, timestamp: contentDate)
45
+ }
46
+
47
+ func getPushToken() throws -> String? {
48
+ guard #available(iOS 16.1, *) else { throw LiveActivitiesNotSupportedException() }
49
+
50
+ guard let activity = Activity<LiveActivityAttributes>.activities.first(where: { $0.id == id }) else {
51
+ throw LiveActivityNotFoundException(id)
52
+ }
53
+
54
+ guard let tokenData = activity.pushToken else { return nil }
55
+
56
+ return tokenData.reduce("") { $0 + String(format: "%02x", $1) }
57
+ }
58
+
59
+ @available(iOS 16.1, *)
60
+ func observePushTokenUpdates(for activity: Activity<LiveActivityAttributes>, pushNotificationsEnabled: Bool) {
61
+ guard pushNotificationsEnabled else {
62
+ return
63
+ }
64
+
65
+ pushTokenObserverTask?.cancel()
66
+ pushTokenObserverTask = Task {
67
+ for await data in activity.pushTokenUpdates {
68
+ let token = data.reduce("") { $0 + String(format: "%02x", $1) }
69
+ emit(event: onTokenReceived, payload: [
70
+ "activityId": activity.id,
71
+ "pushToken": token
72
+ ])
73
+ }
74
+ }
75
+ }
76
+
77
+ override func sharedObjectWillRelease() {
78
+ pushTokenObserverTask?.cancel()
79
+ pushTokenObserverTask = nil
80
+ }
81
+ }