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.
- package/android/src/main/java/expo/modules/invoke/ExpoInvokeModule.kt +35 -0
- package/android/src/main/java/expo/modules/invoke/InvokeSliceProvider.kt +9 -0
- package/build/plugin/codegen/generateSwiftIntents.js +22 -0
- package/build/plugin/codegen/generateSwiftIntents.js.map +1 -1
- package/build/plugin/codegen/generateSwiftLiveActivities.d.ts +17 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js +165 -0
- package/build/plugin/codegen/generateSwiftLiveActivities.js.map +1 -0
- package/build/plugin/codegen/generateSwiftWidget.d.ts +5 -0
- package/build/plugin/codegen/generateSwiftWidget.js.map +1 -1
- package/build/plugin/ios/widgets/withAppGroupEntitlements.d.ts +6 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js +27 -0
- package/build/plugin/ios/widgets/withAppGroupEntitlements.js.map +1 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.d.ts +7 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js +12 -0
- package/build/plugin/ios/widgets/withAppInfoPlist.js.map +1 -0
- package/build/plugin/ios/widgets/withEasConfig.d.ts +8 -0
- package/build/plugin/ios/widgets/withEasConfig.js +52 -0
- package/build/plugin/ios/widgets/withEasConfig.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWarning.d.ts +7 -0
- package/build/plugin/ios/widgets/withIosWarning.js +10 -0
- package/build/plugin/ios/widgets/withIosWarning.js.map +1 -0
- package/build/plugin/ios/widgets/withIosWidgets.d.ts +11 -0
- package/build/plugin/ios/widgets/withIosWidgets.js +83 -0
- package/build/plugin/ios/widgets/withIosWidgets.js.map +1 -0
- package/build/plugin/ios/widgets/withPodsLinking.d.ts +6 -0
- package/build/plugin/ios/widgets/withPodsLinking.js +102 -0
- package/build/plugin/ios/widgets/withPodsLinking.js.map +1 -0
- package/build/plugin/ios/widgets/withPushNotifications.d.ts +6 -0
- package/build/plugin/ios/widgets/withPushNotifications.js +12 -0
- package/build/plugin/ios/widgets/withPushNotifications.js.map +1 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.d.ts +10 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js +379 -0
- package/build/plugin/ios/widgets/withWidgetSourceFiles.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.d.ts +12 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js +41 -0
- package/build/plugin/ios/widgets/xcode/addBuildPhases.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.d.ts +5 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js +20 -0
- package/build/plugin/ios/widgets/xcode/addPbxGroup.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.d.ts +7 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js +41 -0
- package/build/plugin/ios/widgets/xcode/addProductFile.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js +17 -0
- package/build/plugin/ios/widgets/xcode/addTargetDependency.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.d.ts +14 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js +29 -0
- package/build/plugin/ios/widgets/xcode/addToPbxNativeTargetSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.d.ts +4 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js +20 -0
- package/build/plugin/ios/widgets/xcode/addToPbxProjectSection.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.d.ts +11 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js +37 -0
- package/build/plugin/ios/widgets/xcode/addXCConfigurationList.js.map +1 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.d.ts +10 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js +86 -0
- package/build/plugin/ios/widgets/xcode/withTargetXcodeProject.js.map +1 -0
- package/build/plugin/ios/withIOSInvoke.d.ts +6 -15
- package/build/plugin/ios/withIOSInvoke.js +54 -25
- package/build/plugin/ios/withIOSInvoke.js.map +1 -1
- package/build/plugin/ios/withWidgetExtension.d.ts +5 -0
- package/build/plugin/ios/withWidgetExtension.js +5 -0
- package/build/plugin/ios/withWidgetExtension.js.map +1 -1
- package/build/plugin/src/withInvoke.d.ts +34 -23
- package/build/plugin/src/withInvoke.js +16 -27
- package/build/plugin/src/withInvoke.js.map +1 -1
- package/build/plugin/types/WidgetConfig.type.d.ts +54 -0
- package/build/plugin/types/WidgetConfig.type.js +3 -0
- package/build/plugin/types/WidgetConfig.type.js.map +1 -0
- package/build/plugin/types/WidgetFamily.type.d.ts +10 -0
- package/build/plugin/types/WidgetFamily.type.js +13 -0
- package/build/plugin/types/WidgetFamily.type.js.map +1 -0
- package/build/src/hooks/useLiveActivity.d.ts +20 -10
- package/build/src/hooks/useLiveActivity.d.ts.map +1 -1
- package/build/src/hooks/useLiveActivity.js +20 -10
- package/build/src/hooks/useLiveActivity.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +5 -0
- package/build/src/index.js.map +1 -1
- package/build/src/module.d.ts +4 -0
- package/build/src/module.d.ts.map +1 -1
- package/build/src/module.js +8 -0
- package/build/src/module.js.map +1 -1
- package/build/src/web/index.d.ts +10 -1
- package/build/src/web/index.d.ts.map +1 -1
- package/build/src/web/index.js +4 -0
- package/build/src/web/index.js.map +1 -1
- package/build/src/widgets/ExpoWidgets.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts +12 -0
- package/build/src/widgets/ExpoWidgets.ios.d.ts.map +1 -0
- package/build/src/widgets/ExpoWidgets.ios.js +3 -0
- package/build/src/widgets/ExpoWidgets.ios.js.map +1 -0
- package/build/src/widgets/ExpoWidgets.js +42 -0
- package/build/src/widgets/ExpoWidgets.js.map +1 -0
- package/build/src/widgets/Widgets.d.ts +119 -0
- package/build/src/widgets/Widgets.d.ts.map +1 -0
- package/build/src/widgets/Widgets.js +158 -0
- package/build/src/widgets/Widgets.js.map +1 -0
- package/build/src/widgets/Widgets.types.d.ts +263 -0
- package/build/src/widgets/Widgets.types.d.ts.map +1 -0
- package/build/src/widgets/Widgets.types.js +2 -0
- package/build/src/widgets/Widgets.types.js.map +1 -0
- package/build/src/widgets/index.d.ts +3 -0
- package/build/src/widgets/index.d.ts.map +1 -0
- package/build/src/widgets/index.js +2 -0
- package/build/src/widgets/index.js.map +1 -0
- package/bundle/__tests__/decorator.test.ts +89 -0
- package/bundle/decorator.ts +61 -0
- package/bundle/expo-modules-core-stub.ts +1 -0
- package/bundle/expo-stub.ts +15 -0
- package/bundle/index.ts +69 -0
- package/bundle/jsx-runtime-stub.ts +71 -0
- package/bundle/react-native-stub.ts +3 -0
- package/bundle/react-stub.ts +13 -0
- package/expo-module.config.json +1 -1
- package/ios/LiveActivity/LiveActivityManager.swift +15 -14
- package/ios/Module/ExpoInvokeModule.swift +124 -0
- package/ios/Widget/WidgetManager.swift +25 -8
- package/ios/Widgets/LiveActivity.swift +81 -0
- package/ios/Widgets/LiveActivityFactory.swift +49 -0
- package/ios/Widgets/Widget/AppIntent.swift +130 -0
- package/ios/Widgets/Widget/Buttons.swift +62 -0
- package/ios/Widgets/Widget/DynamicView.swift +161 -0
- package/ios/Widgets/Widget/EntryView.swift +36 -0
- package/ios/Widgets/Widget/Fragment.swift +13 -0
- package/ios/Widgets/Widget/LiveActivityBanner.swift +19 -0
- package/ios/Widgets/Widget/RedBoxView.swift +83 -0
- package/ios/Widgets/Widget/TimelineEntry.swift +8 -0
- package/ios/Widgets/Widget/TimelineProvider.swift +45 -0
- package/ios/Widgets/Widget/Type.swift +6 -0
- package/ios/Widgets/Widget/Utils.swift +116 -0
- package/ios/Widgets/Widget/WidgetLiveActivity.swift +104 -0
- package/ios/Widgets/Widget/WidgetsJSRuntime.swift +148 -0
- package/ios/Widgets/WidgetObject.swift +31 -0
- package/ios/Widgets/WidgetsContext.swift +7 -0
- package/ios/Widgets/WidgetsEvents.swift +16 -0
- package/ios/Widgets/WidgetsExceptions.swift +25 -0
- package/ios/Widgets/WidgetsModule.swift +149 -0
- package/ios/Widgets/WidgetsRecords.swift +22 -0
- package/ios/Widgets/WidgetsStorage.swift +58 -0
- package/metro.widget.config.js +76 -0
- package/package.json +11 -1
- package/scripts/autolinking.rb +34 -0
- package/scripts/build-bundle.mjs +69 -0
- package/scripts/with-node.sh +44 -0
- package/scripts/xcode-build-bundle.sh +23 -0
- package/spm.config.json +40 -0
|
@@ -0,0 +1,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
|
+
}
|
package/bundle/index.ts
ADDED
|
@@ -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,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
|
+
};
|
package/expo-module.config.json
CHANGED
|
@@ -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
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
+
}
|