expo-widgets 56.0.13 → 56.0.15

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 (76) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/android/build.gradle +38 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/expo/modules/widgets/ExpoWidgetsAppWidgetProvider.kt +10 -0
  5. package/android/src/main/java/expo/modules/widgets/ExpoWidgetsGlanceWidget.kt +17 -0
  6. package/android/src/main/java/expo/modules/widgets/WidgetsModule.kt +10 -0
  7. package/expo-module.config.json +4 -1
  8. package/package.json +5 -5
  9. package/plugin/build/android/resourceNames.d.ts +5 -0
  10. package/plugin/build/android/resourceNames.js +29 -0
  11. package/plugin/build/android/withAndroidWidgetFiles.d.ts +4 -0
  12. package/plugin/build/android/withAndroidWidgetFiles.js +142 -0
  13. package/plugin/build/android/withAndroidWidgetManifest.d.ts +4 -0
  14. package/plugin/build/android/withAndroidWidgetManifest.js +58 -0
  15. package/plugin/build/android/withAndroidWidgets.d.ts +6 -0
  16. package/plugin/build/android/withAndroidWidgets.js +19 -0
  17. package/plugin/build/ios/withIosWidgets.d.ts +11 -0
  18. package/plugin/build/ios/withIosWidgets.js +81 -0
  19. package/plugin/build/{withWidgetSourceFiles.d.ts → ios/withWidgetSourceFiles.d.ts} +1 -1
  20. package/plugin/build/{withWidgetSourceFiles.js → ios/withWidgetSourceFiles.js} +18 -13
  21. package/plugin/build/types/WidgetConfig.type.d.ts +17 -1
  22. package/plugin/build/types/WidgetFamily.type.d.ts +10 -9
  23. package/plugin/build/types/WidgetFamily.type.js +9 -10
  24. package/plugin/build/withWidgets.js +6 -63
  25. package/plugin/src/android/resourceNames.ts +29 -0
  26. package/plugin/src/android/withAndroidWidgetFiles.ts +143 -0
  27. package/plugin/src/android/withAndroidWidgetManifest.ts +94 -0
  28. package/plugin/src/android/withAndroidWidgets.ts +21 -0
  29. package/plugin/src/ios/withIosWidgets.ts +99 -0
  30. package/plugin/src/{withWidgetSourceFiles.ts → ios/withWidgetSourceFiles.ts} +20 -14
  31. package/plugin/src/types/WidgetConfig.type.ts +20 -1
  32. package/plugin/src/types/WidgetFamily.type.ts +11 -9
  33. package/plugin/src/withWidgets.ts +7 -71
  34. package/plugin/tsconfig.tsbuildinfo +1 -0
  35. /package/plugin/build/{withAppGroupEntitlements.d.ts → ios/withAppGroupEntitlements.d.ts} +0 -0
  36. /package/plugin/build/{withAppGroupEntitlements.js → ios/withAppGroupEntitlements.js} +0 -0
  37. /package/plugin/build/{withAppInfoPlist.d.ts → ios/withAppInfoPlist.d.ts} +0 -0
  38. /package/plugin/build/{withAppInfoPlist.js → ios/withAppInfoPlist.js} +0 -0
  39. /package/plugin/build/{withEasConfig.d.ts → ios/withEasConfig.d.ts} +0 -0
  40. /package/plugin/build/{withEasConfig.js → ios/withEasConfig.js} +0 -0
  41. /package/plugin/build/{withIosWarning.d.ts → ios/withIosWarning.d.ts} +0 -0
  42. /package/plugin/build/{withIosWarning.js → ios/withIosWarning.js} +0 -0
  43. /package/plugin/build/{withPodsLinking.d.ts → ios/withPodsLinking.d.ts} +0 -0
  44. /package/plugin/build/{withPodsLinking.js → ios/withPodsLinking.js} +0 -0
  45. /package/plugin/build/{withPushNotifications.d.ts → ios/withPushNotifications.d.ts} +0 -0
  46. /package/plugin/build/{withPushNotifications.js → ios/withPushNotifications.js} +0 -0
  47. /package/plugin/build/{xcode → ios/xcode}/addBuildPhases.d.ts +0 -0
  48. /package/plugin/build/{xcode → ios/xcode}/addBuildPhases.js +0 -0
  49. /package/plugin/build/{xcode → ios/xcode}/addPbxGroup.d.ts +0 -0
  50. /package/plugin/build/{xcode → ios/xcode}/addPbxGroup.js +0 -0
  51. /package/plugin/build/{xcode → ios/xcode}/addProductFile.d.ts +0 -0
  52. /package/plugin/build/{xcode → ios/xcode}/addProductFile.js +0 -0
  53. /package/plugin/build/{xcode → ios/xcode}/addTargetDependency.d.ts +0 -0
  54. /package/plugin/build/{xcode → ios/xcode}/addTargetDependency.js +0 -0
  55. /package/plugin/build/{xcode → ios/xcode}/addToPbxNativeTargetSection.d.ts +0 -0
  56. /package/plugin/build/{xcode → ios/xcode}/addToPbxNativeTargetSection.js +0 -0
  57. /package/plugin/build/{xcode → ios/xcode}/addToPbxProjectSection.d.ts +0 -0
  58. /package/plugin/build/{xcode → ios/xcode}/addToPbxProjectSection.js +0 -0
  59. /package/plugin/build/{xcode → ios/xcode}/addXCConfigurationList.d.ts +0 -0
  60. /package/plugin/build/{xcode → ios/xcode}/addXCConfigurationList.js +0 -0
  61. /package/plugin/build/{xcode → ios/xcode}/withTargetXcodeProject.d.ts +0 -0
  62. /package/plugin/build/{xcode → ios/xcode}/withTargetXcodeProject.js +0 -0
  63. /package/plugin/src/{withAppGroupEntitlements.ts → ios/withAppGroupEntitlements.ts} +0 -0
  64. /package/plugin/src/{withAppInfoPlist.ts → ios/withAppInfoPlist.ts} +0 -0
  65. /package/plugin/src/{withEasConfig.ts → ios/withEasConfig.ts} +0 -0
  66. /package/plugin/src/{withIosWarning.ts → ios/withIosWarning.ts} +0 -0
  67. /package/plugin/src/{withPodsLinking.ts → ios/withPodsLinking.ts} +0 -0
  68. /package/plugin/src/{withPushNotifications.ts → ios/withPushNotifications.ts} +0 -0
  69. /package/plugin/src/{xcode → ios/xcode}/addBuildPhases.ts +0 -0
  70. /package/plugin/src/{xcode → ios/xcode}/addPbxGroup.ts +0 -0
  71. /package/plugin/src/{xcode → ios/xcode}/addProductFile.ts +0 -0
  72. /package/plugin/src/{xcode → ios/xcode}/addTargetDependency.ts +0 -0
  73. /package/plugin/src/{xcode → ios/xcode}/addToPbxNativeTargetSection.ts +0 -0
  74. /package/plugin/src/{xcode → ios/xcode}/addToPbxProjectSection.ts +0 -0
  75. /package/plugin/src/{xcode → ios/xcode}/addXCConfigurationList.ts +0 -0
  76. /package/plugin/src/{xcode → ios/xcode}/withTargetXcodeProject.ts +0 -0
@@ -1,9 +1,10 @@
1
- export declare enum WidgetFamily {
2
- systemSmall = "systemSmall",
3
- systemMedium = "systemMedium",
4
- systemLarge = "systemLarge",
5
- systemExtraLarge = "systemExtraLarge",
6
- accessoryCircular = "accessoryCircular",
7
- accessoryRectangular = "accessoryRectangular",
8
- accessoryInline = "accessoryInline"
9
- }
1
+ export declare const WidgetFamily: {
2
+ readonly systemSmall: "systemSmall";
3
+ readonly systemMedium: "systemMedium";
4
+ readonly systemLarge: "systemLarge";
5
+ readonly systemExtraLarge: "systemExtraLarge";
6
+ readonly accessoryCircular: "accessoryCircular";
7
+ readonly accessoryRectangular: "accessoryRectangular";
8
+ readonly accessoryInline: "accessoryInline";
9
+ };
10
+ export type WidgetFamily = (typeof WidgetFamily)[keyof typeof WidgetFamily];
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WidgetFamily = void 0;
4
- var WidgetFamily;
5
- (function (WidgetFamily) {
6
- WidgetFamily["systemSmall"] = "systemSmall";
7
- WidgetFamily["systemMedium"] = "systemMedium";
8
- WidgetFamily["systemLarge"] = "systemLarge";
9
- WidgetFamily["systemExtraLarge"] = "systemExtraLarge";
10
- WidgetFamily["accessoryCircular"] = "accessoryCircular";
11
- WidgetFamily["accessoryRectangular"] = "accessoryRectangular";
12
- WidgetFamily["accessoryInline"] = "accessoryInline";
13
- })(WidgetFamily || (exports.WidgetFamily = WidgetFamily = {}));
4
+ exports.WidgetFamily = {
5
+ systemSmall: 'systemSmall',
6
+ systemMedium: 'systemMedium',
7
+ systemLarge: 'systemLarge',
8
+ systemExtraLarge: 'systemExtraLarge',
9
+ accessoryCircular: 'accessoryCircular',
10
+ accessoryRectangular: 'accessoryRectangular',
11
+ accessoryInline: 'accessoryInline',
12
+ };
@@ -4,71 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const config_plugins_1 = require("expo/config-plugins");
7
- const withAppGroupEntitlements_1 = __importDefault(require("./withAppGroupEntitlements"));
8
- const withAppInfoPlist_1 = __importDefault(require("./withAppInfoPlist"));
9
- const withEasConfig_1 = __importDefault(require("./withEasConfig"));
10
- const withIosWarning_1 = __importDefault(require("./withIosWarning"));
11
- const withPodsLinking_1 = __importDefault(require("./withPodsLinking"));
12
- const withPushNotifications_1 = __importDefault(require("./withPushNotifications"));
13
- const withWidgetSourceFiles_1 = __importDefault(require("./withWidgetSourceFiles"));
14
- const withTargetXcodeProject_1 = __importDefault(require("./xcode/withTargetXcodeProject"));
7
+ const withAndroidWidgets_1 = __importDefault(require("./android/withAndroidWidgets"));
8
+ const withIosWidgets_1 = __importDefault(require("./ios/withIosWidgets"));
15
9
  const pkg = require('../../package.json');
16
10
  const withWidgets = (config, props) => {
17
- let plugins = [];
18
- const deploymentTarget = '16.4';
19
- const targetName = 'ExpoWidgetsTarget';
20
- let bundleIdentifier = props?.bundleIdentifier;
21
- if (!bundleIdentifier) {
22
- bundleIdentifier = `${config.ios?.bundleIdentifier}.${targetName}`;
23
- plugins.push([
24
- withIosWarning_1.default,
25
- {
26
- property: 'bundleIdentifier',
27
- warning: `Expo Widgets: No bundle identifier provided, using fallback: ${bundleIdentifier}.`,
28
- },
29
- ]);
30
- }
31
- let groupIdentifier = props?.groupIdentifier;
32
- if (!groupIdentifier) {
33
- if (!config.ios?.bundleIdentifier) {
34
- throw new Error('iOS bundle identifier is required. Please set `ios.bundleIdentifier` in `app.json` or `app.config.js`');
35
- }
36
- groupIdentifier = `group.${config.ios.bundleIdentifier}`;
37
- plugins.push([
38
- withIosWarning_1.default,
39
- {
40
- property: 'groupIdentifier',
41
- warning: `Expo Widgets: No group identifier provided, using fallback: ${groupIdentifier}.`,
42
- },
43
- ]);
44
- }
45
11
  const widgets = props?.widgets ?? [];
46
- const enablePushNotifications = props?.enablePushNotifications ?? false;
47
- const frequentUpdates = props?.frequentUpdates ?? false;
48
- let sharedFiles = [];
49
- const setFiles = (files) => {
50
- sharedFiles = [...sharedFiles, ...files];
51
- };
52
- const getFileUris = () => sharedFiles;
53
- plugins = [
54
- ...plugins,
55
- [withEasConfig_1.default, { targetName, bundleIdentifier, groupIdentifier }],
56
- [withPodsLinking_1.default, { targetName }],
57
- [withWidgetSourceFiles_1.default, { targetName, widgets, groupIdentifier, onFilesGenerated: setFiles }],
58
- [withAppInfoPlist_1.default, { frequentUpdates, groupIdentifier }],
59
- [withPushNotifications_1.default, { enablePushNotifications }],
60
- [withAppGroupEntitlements_1.default, { groupIdentifier }],
61
- [
62
- withTargetXcodeProject_1.default,
63
- {
64
- targetName,
65
- bundleIdentifier,
66
- deploymentTarget,
67
- appleTeamId: config.ios?.appleTeamId,
68
- getFileUris,
69
- },
70
- ],
71
- ];
72
- return (0, config_plugins_1.withPlugins)(config, plugins);
12
+ return (0, config_plugins_1.withPlugins)(config, [
13
+ [withAndroidWidgets_1.default, { widgets }],
14
+ [withIosWidgets_1.default, { ...(props ?? {}), widgets }],
15
+ ]);
73
16
  };
74
17
  exports.default = (0, config_plugins_1.createRunOncePlugin)(withWidgets, pkg.name, pkg.version);
@@ -0,0 +1,29 @@
1
+ import { WidgetConfig } from '../types/WidgetConfig.type';
2
+
3
+ const toAndroidResourceName = (name: string): string => {
4
+ const resourceName = name
5
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
6
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
7
+ .replace(/[^A-Za-z0-9_]/g, '_')
8
+ .replace(/_+/g, '_')
9
+ .replace(/^_+|_+$/g, '')
10
+ .toLowerCase();
11
+
12
+ return /^[a-z]/.test(resourceName) ? resourceName : `widget_${resourceName}`;
13
+ };
14
+
15
+ export const getProviderClassName = (widget: WidgetConfig): string => {
16
+ return `${widget.name}Provider`;
17
+ };
18
+
19
+ export const getWidgetInfoResourceName = (widget: WidgetConfig): string => {
20
+ return `${toAndroidResourceName(widget.name)}_info`;
21
+ };
22
+
23
+ export const getWidgetDisplayNameResourceName = (widget: WidgetConfig): string => {
24
+ return `${toAndroidResourceName(widget.name)}_display_name`;
25
+ };
26
+
27
+ export const getWidgetDescriptionResourceName = (widget: WidgetConfig): string => {
28
+ return `${toAndroidResourceName(widget.name)}_description`;
29
+ };
@@ -0,0 +1,143 @@
1
+ import { AndroidConfig, ConfigPlugin, withDangerousMod } from 'expo/config-plugins';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ import {
6
+ getProviderClassName,
7
+ getWidgetDescriptionResourceName,
8
+ getWidgetDisplayNameResourceName,
9
+ getWidgetInfoResourceName,
10
+ } from './resourceNames';
11
+ import { WidgetConfig } from '../types/WidgetConfig.type';
12
+
13
+ const withAndroidWidgetFiles: ConfigPlugin<WidgetConfig[]> = (config, widgets) => {
14
+ return withDangerousMod(config, [
15
+ 'android',
16
+ async (config) => {
17
+ const androidPackage = AndroidConfig.Package.getPackage(config);
18
+ if (
19
+ typeof androidPackage !== 'string' ||
20
+ !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(androidPackage)
21
+ ) {
22
+ throw new Error(
23
+ 'Android package is required for expo-widgets. Please set `android.package` in `app.json` or `app.config.js`.'
24
+ );
25
+ }
26
+
27
+ const projectRoot = config.modRequest.platformProjectRoot;
28
+ const packageDirectory = path.join(
29
+ projectRoot,
30
+ 'app/src/main/java',
31
+ ...androidPackage.split('.')
32
+ );
33
+ const xmlDirectory = path.join(projectRoot, 'app/src/main/res/xml');
34
+ const valuesDirectory = path.join(projectRoot, 'app/src/main/res/values');
35
+ const widgetsXmlPath = path.join(xmlDirectory, 'expo_widgets.xml');
36
+
37
+ fs.mkdirSync(packageDirectory, { recursive: true });
38
+ fs.mkdirSync(xmlDirectory, { recursive: true });
39
+ fs.mkdirSync(valuesDirectory, { recursive: true });
40
+
41
+ if (fs.existsSync(widgetsXmlPath)) {
42
+ fs.rmSync(widgetsXmlPath);
43
+ }
44
+ fs.writeFileSync(widgetsXmlPath, createWidgetStringsXml(widgets));
45
+
46
+ for (const widget of widgets) {
47
+ const providerPath = path.join(packageDirectory, `${getProviderClassName(widget)}.kt`);
48
+ if (fs.existsSync(providerPath)) {
49
+ fs.rmSync(providerPath);
50
+ }
51
+ fs.writeFileSync(
52
+ path.join(packageDirectory, `${getProviderClassName(widget)}.kt`),
53
+ createWidgetProviderKt(androidPackage, widget)
54
+ );
55
+
56
+ const widgetInfoPath = path.join(xmlDirectory, `${getWidgetInfoResourceName(widget)}.xml`);
57
+ if (fs.existsSync(widgetInfoPath)) {
58
+ fs.rmSync(widgetInfoPath);
59
+ }
60
+ fs.writeFileSync(
61
+ path.join(xmlDirectory, `${getWidgetInfoResourceName(widget)}.xml`),
62
+ createWidgetInfoXml(widget)
63
+ );
64
+ }
65
+
66
+ return config;
67
+ },
68
+ ]);
69
+ };
70
+
71
+ const createWidgetProviderKt = (androidPackage: string, widget: WidgetConfig): string => {
72
+ return `package ${androidPackage}
73
+
74
+ import expo.modules.widgets.ExpoWidgetsAppWidgetProvider
75
+
76
+ class ${getProviderClassName(widget)} : ExpoWidgetsAppWidgetProvider(${JSON.stringify(widget.name)})
77
+ `;
78
+ };
79
+
80
+ const createWidgetInfoXml = (widget: WidgetConfig): string => {
81
+ const android = getAndroidWidgetConfig(widget);
82
+
83
+ return `<?xml version="1.0" encoding="utf-8"?>
84
+ <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
85
+ android:minWidth="${android.minWidth}dp"
86
+ android:minHeight="${android.minHeight}dp"
87
+ android:targetCellWidth="${android.targetCellWidth}"
88
+ android:targetCellHeight="${android.targetCellHeight}"
89
+ android:updatePeriodMillis="0"
90
+ android:initialLayout="@layout/glance_default_loading_layout"
91
+ android:description="@string/${getWidgetDescriptionResourceName(widget)}"
92
+ android:resizeMode="${getWidgetResizeMode(widget)}"
93
+ android:widgetCategory="home_screen" />
94
+ `;
95
+ };
96
+
97
+ const getWidgetResizeMode = (widget: WidgetConfig): string => {
98
+ const resizeMode = widget.android?.resizeMode;
99
+ switch (resizeMode) {
100
+ case 'none':
101
+ return 'none';
102
+ case 'horizontal':
103
+ return 'horizontal';
104
+ case 'vertical':
105
+ return 'vertical';
106
+ case 'both':
107
+ default:
108
+ return 'horizontal|vertical';
109
+ }
110
+ };
111
+
112
+ const getAndroidWidgetConfig = (widget: WidgetConfig): NonNullable<WidgetConfig['android']> => {
113
+ return {
114
+ minWidth: widget.android?.minWidth ?? 180,
115
+ minHeight: widget.android?.minHeight ?? 110,
116
+ targetCellWidth: widget.android?.targetCellWidth ?? 4,
117
+ targetCellHeight: widget.android?.targetCellHeight ?? 2,
118
+ };
119
+ };
120
+
121
+ const escapeXmlSpecialChars = (value: string): string => {
122
+ return value
123
+ .replace(/&/g, '&amp;')
124
+ .replace(/"/g, '&quot;')
125
+ .replace(/'/g, '&apos;')
126
+ .replace(/</g, '&lt;')
127
+ .replace(/>/g, '&gt;');
128
+ };
129
+
130
+ const createWidgetStringsXml = (widgets: WidgetConfig[]): string => {
131
+ return `<?xml version="1.0" encoding="utf-8"?>
132
+ <resources>
133
+ ${widgets
134
+ .map((widget) => {
135
+ return ` <string name="${getWidgetDisplayNameResourceName(widget)}">${escapeXmlSpecialChars(widget.displayName)}</string>
136
+ <string name="${getWidgetDescriptionResourceName(widget)}">${escapeXmlSpecialChars(widget.description)}</string>`;
137
+ })
138
+ .join('\n')}
139
+ </resources>
140
+ `;
141
+ };
142
+
143
+ export default withAndroidWidgetFiles;
@@ -0,0 +1,94 @@
1
+ import { AndroidConfig, ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
2
+
3
+ import {
4
+ getProviderClassName,
5
+ getWidgetDisplayNameResourceName,
6
+ getWidgetInfoResourceName,
7
+ } from './resourceNames';
8
+ import { WidgetConfig } from '../types/WidgetConfig.type';
9
+
10
+ const EXPO_WIDGETS_INTERACTION_ACTION = 'expo.modules.widgets.ACTION_WIDGET_INTERACTION';
11
+ const EXPO_WIDGETS_NAME_METADATA = 'expo.modules.widgets.NAME';
12
+
13
+ type ManifestReceiver = {
14
+ $: {
15
+ 'android:name': string;
16
+ 'android:exported'?: 'true' | 'false';
17
+ 'android:label'?: string;
18
+ [key: string]: string | undefined;
19
+ };
20
+ 'intent-filter'?: {
21
+ action?: {
22
+ $: {
23
+ 'android:name': string;
24
+ };
25
+ }[];
26
+ }[];
27
+ 'meta-data'?: {
28
+ $: {
29
+ 'android:name': string;
30
+ 'android:value'?: string;
31
+ 'android:resource'?: string;
32
+ };
33
+ }[];
34
+ };
35
+
36
+ const createWidgetReceiver = (widget: WidgetConfig): ManifestReceiver => {
37
+ return {
38
+ $: {
39
+ 'android:name': `.${getProviderClassName(widget)}`,
40
+ 'android:exported': 'true',
41
+ 'android:label': `@string/${getWidgetDisplayNameResourceName(widget)}`,
42
+ },
43
+ 'intent-filter': [
44
+ {
45
+ action: [
46
+ {
47
+ $: {
48
+ 'android:name': 'android.appwidget.action.APPWIDGET_UPDATE',
49
+ },
50
+ },
51
+ {
52
+ $: {
53
+ 'android:name': EXPO_WIDGETS_INTERACTION_ACTION,
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ 'meta-data': [
60
+ {
61
+ $: {
62
+ 'android:name': 'android.appwidget.provider',
63
+ 'android:resource': `@xml/${getWidgetInfoResourceName(widget)}`,
64
+ },
65
+ },
66
+ {
67
+ $: {
68
+ 'android:name': EXPO_WIDGETS_NAME_METADATA,
69
+ 'android:value': widget.name,
70
+ },
71
+ },
72
+ ],
73
+ };
74
+ };
75
+
76
+ const withAndroidWidgetManifest: ConfigPlugin<WidgetConfig[]> = (config, widgets) => {
77
+ return withAndroidManifest(config, (config) => {
78
+ const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
79
+ const receivers = (mainApplication.receiver ?? []) as ManifestReceiver[];
80
+ const isNotWidgetReceiver = (receiver: ManifestReceiver) =>
81
+ !receiver['meta-data']?.some(
82
+ (metaData) => metaData.$['android:name'] === EXPO_WIDGETS_NAME_METADATA
83
+ );
84
+
85
+ mainApplication.receiver = [
86
+ ...receivers.filter(isNotWidgetReceiver),
87
+ ...widgets.map(createWidgetReceiver),
88
+ ];
89
+
90
+ return config;
91
+ });
92
+ };
93
+
94
+ export default withAndroidWidgetManifest;
@@ -0,0 +1,21 @@
1
+ import { ConfigPlugin } from 'expo/config-plugins';
2
+
3
+ import withAndroidWidgetFiles from './withAndroidWidgetFiles';
4
+ import withAndroidWidgetManifest from './withAndroidWidgetManifest';
5
+ import { WidgetConfig } from '../types/WidgetConfig.type';
6
+
7
+ const withAndroidWidgets: ConfigPlugin<{ widgets: WidgetConfig[] }> = (config, { widgets }) => {
8
+ if (widgets.length === 0) {
9
+ return config;
10
+ }
11
+
12
+ const androidWidgets = widgets.filter((widget) => widget.android !== null);
13
+ let nextConfig = config;
14
+ if (androidWidgets.length > 0) {
15
+ nextConfig = withAndroidWidgetFiles(nextConfig, androidWidgets);
16
+ }
17
+
18
+ return withAndroidWidgetManifest(nextConfig, androidWidgets);
19
+ };
20
+
21
+ export default withAndroidWidgets;
@@ -0,0 +1,99 @@
1
+ import { ConfigPlugin, StaticPlugin, withPlugins } from 'expo/config-plugins';
2
+
3
+ import withAppGroupEntitlements from './withAppGroupEntitlements';
4
+ import withAppInfoPlist from './withAppInfoPlist';
5
+ import withEasConfig from './withEasConfig';
6
+ import withIosWarning from './withIosWarning';
7
+ import withPodsLinking from './withPodsLinking';
8
+ import withPushNotifications from './withPushNotifications';
9
+ import withWidgetSourceFiles from './withWidgetSourceFiles';
10
+ import { WidgetConfig } from '../types/WidgetConfig.type';
11
+ import withTargetXcodeProject from './xcode/withTargetXcodeProject';
12
+
13
+ type IosWidgetsProps = {
14
+ bundleIdentifier?: string;
15
+ groupIdentifier?: string;
16
+ enablePushNotifications?: boolean;
17
+ frequentUpdates?: boolean;
18
+ widgets: WidgetConfig[];
19
+ };
20
+
21
+ const withIosWidgets: ConfigPlugin<IosWidgetsProps> = (config, props) => {
22
+ const shouldConfigureIos =
23
+ !!config.ios?.bundleIdentifier ||
24
+ !!props.bundleIdentifier ||
25
+ !!props.groupIdentifier ||
26
+ props.enablePushNotifications === true ||
27
+ props.frequentUpdates === true;
28
+
29
+ if (!shouldConfigureIos) {
30
+ return config;
31
+ }
32
+
33
+ let plugins: (StaticPlugin | ConfigPlugin | string)[] = [];
34
+ const deploymentTarget = config.ios?.deploymentTarget ?? '16.4';
35
+ const targetName = 'ExpoWidgetsTarget';
36
+ const widgets = props.widgets.filter((widget) => widget.ios !== null);
37
+
38
+ const enablePushNotifications = props.enablePushNotifications ?? false;
39
+ const frequentUpdates = props.frequentUpdates ?? false;
40
+
41
+ let bundleIdentifier = props.bundleIdentifier;
42
+ if (!bundleIdentifier) {
43
+ bundleIdentifier = `${config.ios?.bundleIdentifier}.${targetName}`;
44
+ plugins.push([
45
+ withIosWarning,
46
+ {
47
+ property: 'bundleIdentifier',
48
+ warning: `Expo Widgets: No bundle identifier provided, using fallback: ${bundleIdentifier}.`,
49
+ },
50
+ ]);
51
+ }
52
+
53
+ let groupIdentifier = props.groupIdentifier;
54
+ if (!groupIdentifier) {
55
+ if (!config.ios?.bundleIdentifier) {
56
+ throw new Error(
57
+ 'iOS bundle identifier is required. Please set `ios.bundleIdentifier` in `app.json` or `app.config.js`'
58
+ );
59
+ }
60
+ groupIdentifier = `group.${config.ios.bundleIdentifier}`;
61
+ plugins.push([
62
+ withIosWarning,
63
+ {
64
+ property: 'groupIdentifier',
65
+ warning: `Expo Widgets: No group identifier provided, using fallback: ${groupIdentifier}.`,
66
+ },
67
+ ]);
68
+ }
69
+
70
+ let sharedFiles: string[] = [];
71
+ const setFiles = (files: string[]) => {
72
+ sharedFiles = [...sharedFiles, ...files];
73
+ };
74
+ const getFileUris = () => sharedFiles;
75
+
76
+ plugins = [
77
+ ...plugins,
78
+ [withEasConfig, { targetName, bundleIdentifier, groupIdentifier }],
79
+ [withPodsLinking, { targetName }],
80
+ [withWidgetSourceFiles, { targetName, widgets, groupIdentifier, onFilesGenerated: setFiles }],
81
+ [withAppInfoPlist, { frequentUpdates, groupIdentifier }],
82
+ [withPushNotifications, { enablePushNotifications }],
83
+ [withAppGroupEntitlements, { groupIdentifier }],
84
+ [
85
+ withTargetXcodeProject,
86
+ {
87
+ targetName,
88
+ bundleIdentifier,
89
+ deploymentTarget,
90
+ appleTeamId: config.ios?.appleTeamId,
91
+ getFileUris,
92
+ },
93
+ ],
94
+ ];
95
+
96
+ return withPlugins(config, plugins);
97
+ };
98
+
99
+ export default withIosWidgets;
@@ -3,8 +3,8 @@ import { ConfigPlugin, withDangerousMod } from 'expo/config-plugins';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
 
6
- import { WidgetConfig } from './types/WidgetConfig.type';
7
- import { WidgetFamily } from './types/WidgetFamily.type';
6
+ import { WidgetConfig } from '../types/WidgetConfig.type';
7
+ import { WidgetFamily } from '../types/WidgetFamily.type';
8
8
 
9
9
  type WidgetSourceFilesProps = {
10
10
  targetName: string;
@@ -31,11 +31,13 @@ function assertWidgetFamily(value: unknown): asserts value is WidgetFamily {
31
31
 
32
32
  function validateWidget(widget: WidgetConfig): void {
33
33
  assertSwiftIdentifier(widget.name, 'widget name');
34
- for (const family of widget.supportedFamilies) {
34
+ const supportedFamilies = widget.ios?.supportedFamilies ?? widget.supportedFamilies ?? [];
35
+ for (const family of supportedFamilies) {
35
36
  assertWidgetFamily(family);
36
37
  }
37
- if (widget.configuration) {
38
- for (const [paramName, param] of Object.entries(widget.configuration.parameters)) {
38
+ const configuration = widget.ios?.configuration ?? widget.configuration;
39
+ if (configuration) {
40
+ for (const [paramName, param] of Object.entries(configuration.parameters)) {
39
41
  assertSwiftIdentifier(paramName, 'parameter name');
40
42
  if (param.type === 'number' && typeof param.default !== 'number') {
41
43
  throw new Error(`Invalid default for ${JSON.stringify(paramName)}: must be a number.`);
@@ -161,7 +163,7 @@ struct ExportWidgets${index}: WidgetBundle {
161
163
  var body: some Widget {
162
164
  ${widgets
163
165
  .map((widget) => {
164
- if (widget.configuration) {
166
+ if (widget.ios?.configuration ?? widget.configuration) {
165
167
  return `if #available(iOS 17.0, *) {
166
168
  ${widget.name}()
167
169
  }`;
@@ -174,7 +176,11 @@ struct ExportWidgets${index}: WidgetBundle {
174
176
  }`;
175
177
 
176
178
  const widgetSwift = (widget: WidgetConfig): string => {
177
- if (!widget.configuration)
179
+ const configuration = widget.ios?.configuration ?? widget.configuration;
180
+ const supportedFamilies = widget.ios?.supportedFamilies ?? widget.supportedFamilies ?? [];
181
+ const contentMarginsDisabled =
182
+ widget.ios?.contentMarginsDisabled ?? widget.contentMarginsDisabled;
183
+ if (!configuration)
178
184
  return `import WidgetKit
179
185
  import SwiftUI
180
186
  internal import ExpoWidgets
@@ -188,7 +194,7 @@ struct ${widget.name}: Widget {
188
194
  }
189
195
  .configurationDisplayName(${JSON.stringify(widget.displayName)})
190
196
  .description(${JSON.stringify(widget.description)})
191
- .supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
197
+ .supportedFamilies([.${supportedFamilies.join(', .')}])${contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
192
198
  }
193
199
  }`;
194
200
  return `import WidgetKit
@@ -198,9 +204,9 @@ internal import ExpoWidgets
198
204
 
199
205
  // AppIntent
200
206
  struct ${widget.name}ConfigurationAppIntent: WidgetConfigurationIntent {
201
- static var title: LocalizedStringResource = ${JSON.stringify(`${widget.configuration?.title ?? widget.displayName} Configuration`)}
202
- ${widget.configuration?.description ? ` static var description: LocalizedStringResource = ${JSON.stringify(widget.configuration.description)}\n` : ''}
203
- ${Object.entries(widget.configuration?.parameters ?? {})
207
+ static var title: LocalizedStringResource = ${JSON.stringify(`${configuration?.title ?? widget.displayName} Configuration`)}
208
+ ${configuration?.description ? ` static var description: LocalizedStringResource = ${JSON.stringify(configuration.description)}\n` : ''}
209
+ ${Object.entries(configuration?.parameters ?? {})
204
210
  .map(([name, param]) => {
205
211
  let paramType: string;
206
212
  switch (param.type) {
@@ -227,7 +233,7 @@ ${Object.entries(widget.configuration?.parameters ?? {})
227
233
  return .result()
228
234
  }
229
235
  }
230
- ${Object.entries(widget.configuration?.parameters ?? {})
236
+ ${Object.entries(configuration?.parameters ?? {})
231
237
  .map(([name, param]) => {
232
238
  if (param.type !== 'enum') return '';
233
239
  const paramTypeName = `${widget.name}${name[0]?.toUpperCase() + name.slice(1)}Enum`;
@@ -307,7 +313,7 @@ struct ${widget.name}EntryView: View {
307
313
  var env: [String: Any] = getWidgetEnvironment(environment: environment)
308
314
  env["timestamp"] = Int(entry.date.timeIntervalSince1970 * 1000)
309
315
  env["configuration"] = [
310
- ${Object.entries(widget.configuration?.parameters ?? {})
316
+ ${Object.entries(configuration?.parameters ?? {})
311
317
  .map(([name, param]) => {
312
318
  return ` "${name}": entry.configuration.${name}${param.type === 'enum' ? '.rawValue' : ''}`;
313
319
  })
@@ -346,7 +352,7 @@ struct ${widget.name}: Widget {
346
352
  }
347
353
  .configurationDisplayName(${JSON.stringify(widget.displayName)})
348
354
  .description(${JSON.stringify(widget.description)})
349
- .supportedFamilies([.${widget.supportedFamilies.join(', .')}])${widget.contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
355
+ .supportedFamilies([.${supportedFamilies.join(', .')}])${contentMarginsDisabled ? '\n .contentMarginsDisabled()' : ''}
350
356
  }
351
357
  }`;
352
358
  };
@@ -2,15 +2,34 @@ import { WidgetFamily } from './WidgetFamily.type';
2
2
 
3
3
  export type WidgetConfig = {
4
4
  name: string;
5
- supportedFamilies: WidgetFamily[];
6
5
  displayName: string;
7
6
  description: string;
7
+ // @deprecated: use `ios.supportedFamilies` instead.
8
+ supportedFamilies: WidgetFamily[];
9
+ // @deprecated: use `ios.contentMarginsDisabled` instead.
8
10
  contentMarginsDisabled: boolean;
11
+ // @deprecated: use `ios.configuration` instead.
9
12
  configuration?: {
10
13
  title: string;
11
14
  description?: string;
12
15
  parameters: Record<string, WidgetParameter>;
13
16
  };
17
+ ios?: {
18
+ supportedFamilies: WidgetFamily[];
19
+ contentMarginsDisabled?: boolean;
20
+ configuration?: {
21
+ title: string;
22
+ description?: string;
23
+ parameters: Record<string, WidgetParameter>;
24
+ };
25
+ } | null;
26
+ android?: {
27
+ minWidth?: number;
28
+ minHeight?: number;
29
+ targetCellWidth?: number;
30
+ targetCellHeight?: number;
31
+ resizeMode?: 'none' | 'horizontal' | 'vertical' | 'both';
32
+ } | null;
14
33
  };
15
34
 
16
35
  export type WidgetParameterString = {
@@ -1,9 +1,11 @@
1
- export enum WidgetFamily {
2
- systemSmall = 'systemSmall',
3
- systemMedium = 'systemMedium',
4
- systemLarge = 'systemLarge',
5
- systemExtraLarge = 'systemExtraLarge',
6
- accessoryCircular = 'accessoryCircular',
7
- accessoryRectangular = 'accessoryRectangular',
8
- accessoryInline = 'accessoryInline',
9
- }
1
+ export const WidgetFamily = {
2
+ systemSmall: 'systemSmall',
3
+ systemMedium: 'systemMedium',
4
+ systemLarge: 'systemLarge',
5
+ systemExtraLarge: 'systemExtraLarge',
6
+ accessoryCircular: 'accessoryCircular',
7
+ accessoryRectangular: 'accessoryRectangular',
8
+ accessoryInline: 'accessoryInline',
9
+ } as const;
10
+
11
+ export type WidgetFamily = (typeof WidgetFamily)[keyof typeof WidgetFamily];