├── app.plugin.js
├── android
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── expo
│ │ └── modules
│ │ └── widgetsync
│ │ └── ReactNativeWidgetSyncModule.kt
└── build.gradle
├── plugin
├── jest.config.js
├── src
│ ├── @types
│ │ └── index.d.ts
│ ├── ios
│ │ ├── static
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── Contents.json
│ │ │ │ ├── AccentColor.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── WidgetBackground.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ └── Contents.json
│ │ │ └── widget.swift
│ │ ├── withWidgetPlist.ts
│ │ ├── withWidgetIos.ts
│ │ ├── withWidgetEntitlements.ts
│ │ ├── withWidgetSourceFiles.ts
│ │ └── withWidgetXcodeTarget.ts
│ ├── android
│ │ ├── static
│ │ │ ├── res
│ │ │ │ ├── drawable-nodpi
│ │ │ │ │ └── example_appwidget_preview.png
│ │ │ │ ├── values
│ │ │ │ │ ├── strings_widget.xml
│ │ │ │ │ ├── colors_widget.xml
│ │ │ │ │ ├── attrs_widget.xml
│ │ │ │ │ ├── dimens_widget.xml
│ │ │ │ │ ├── styles_widget.xml
│ │ │ │ │ └── themes_widget.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ ├── app_widget_background.xml
│ │ │ │ │ └── app_widget_inner_view_background.xml
│ │ │ │ ├── values-night-v31
│ │ │ │ │ └── themes_widget.xml
│ │ │ │ ├── values-v31
│ │ │ │ │ ├── themes_widget.xml
│ │ │ │ │ └── styles_widget.xml
│ │ │ │ ├── xml
│ │ │ │ │ └── sample_widget_info.xml
│ │ │ │ ├── values-v21
│ │ │ │ │ └── styles_widget.xml
│ │ │ │ └── layout
│ │ │ │ │ └── sample_widget.xml
│ │ │ └── java
│ │ │ │ └── package_name
│ │ │ │ └── SampleWidget.kt
│ │ ├── withWidgetProjectBuildGradle.ts
│ │ ├── withWidgetAndroid.ts
│ │ ├── withWidgetAppBuildGradle.ts
│ │ ├── withWidgetManifest.ts
│ │ └── withWidgetSourceCodes.ts
│ ├── scripts
│ │ └── copy.sh
│ └── index.ts
└── tsconfig.json
├── example
├── assets
│ ├── icon.png
│ ├── splash.png
│ ├── favicon.png
│ └── adaptive-icon.png
├── tsconfig.json
├── babel.config.js
├── webpack.config.js
├── .gitignore
├── package.json
├── metro.config.js
├── app.json
└── App.tsx
├── .eslintrc.js
├── .npmignore
├── expo-module.config.json
├── tsconfig.json
├── src
├── ReactNativeWidgetSyncModule.ts
└── index.ts
├── e2e
├── android.yml
└── ios.yml
├── .gitignore
├── ios
├── ReactNativeWidgetSync.podspec
└── ReactNativeWidgetSyncModule.swift
├── package.json
└── README.md
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./plugin/build");
2 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/plugin/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require("expo-module-scripts/jest-preset-plugin");
2 |
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pafry7/react-native-widget-sync/HEAD/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pafry7/react-native-widget-sync/HEAD/example/assets/splash.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pafry7/react-native-widget-sync/HEAD/example/assets/favicon.png
--------------------------------------------------------------------------------
/plugin/src/@types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "xcode";
2 |
3 | type WithWidgetProps = {
4 | devTeamId: string;
5 | };
6 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pafry7/react-native-widget-sync/HEAD/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/plugin/src/ios/static/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "author": "xcode",
4 | "version": 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['universe/native', 'universe/web'],
4 | ignorePatterns: ['build'],
5 | };
6 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/drawable-nodpi/example_appwidget_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pafry7/react-native-widget-sync/HEAD/plugin/src/android/static/res/drawable-nodpi/example_appwidget_preview.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Exclude all top-level hidden directories by convention
2 | /.*/
3 |
4 | __mocks__
5 | __tests__
6 |
7 | /babel.config.js
8 | /android/src/androidTest/
9 | /android/src/test/
10 | /android/build/
11 | /example/
12 | e2e/
--------------------------------------------------------------------------------
/plugin/src/ios/static/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "idiom": "universal"
5 | }
6 | ],
7 | "info": {
8 | "author": "xcode",
9 | "version": 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/plugin/src/ios/static/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "idiom": "universal"
5 | }
6 | ],
7 | "info": {
8 | "author": "xcode",
9 | "version": 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/expo-module.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platforms": ["ios", "android", "web"],
3 | "ios": {
4 | "modules": ["ReactNativeWidgetSyncModule"]
5 | },
6 | "android": {
7 | "modules": ["expo.modules.widgetsync.ReactNativeWidgetSyncModule"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.plugin",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "rootDir": "src"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "react-native-widget-sync": ["../src/index"],
7 | "react-native-widget-sync/*": ["../src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/strings_widget.xml:
--------------------------------------------------------------------------------
1 |
2 | EXAMPLE
3 | Add widget
4 | This is an app widget description
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | {
3 | "extends": "expo-module-scripts/tsconfig.base",
4 | "compilerOptions": {
5 | "outDir": "./build"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__stories__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/colors_widget.xml:
--------------------------------------------------------------------------------
1 |
2 | #FFE1F5FE
3 | #FF81D4FA
4 | #FF039BE5
5 | #FF01579B
6 |
--------------------------------------------------------------------------------
/plugin/src/scripts/copy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ios files
4 | for FILE in ./plugin/src/ios/static
5 | do
6 | cp -R $FILE ./plugin/build/ios/static/
7 | done
8 |
9 | # android files
10 | for FILE in ./plugin/src/android/static
11 | do
12 | cp -R $FILE ./plugin/build/android/static/
13 | done
--------------------------------------------------------------------------------
/src/ReactNativeWidgetSyncModule.ts:
--------------------------------------------------------------------------------
1 | import { requireNativeModule } from 'expo-modules-core';
2 |
3 | // It loads the native module object from the JSI or falls back to
4 | // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5 | export default requireNativeModule('ReactNativeWidgetSync');
6 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/attrs_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/dimens_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | 0dp
9 |
10 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/drawable-v21/app_widget_background.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/drawable-v21/app_widget_inner_view_background.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/e2e/android.yml:
--------------------------------------------------------------------------------
1 | appId: "expo.modules.widgetsync.example"
2 | ---
3 | - longPressOn: "react-native-widget-sync-example"
4 | - tapOn: "Widżety"
5 | - longPressOn:
6 | id: "com.google.android.apps.nexuslauncher:id/widget_preview"
7 | - launchApp: "expo.modules.widgetsync.example"
8 | - tapOn: "Set value"
9 | - pressKey: "Home"
10 | - assertVisible: "Hello from App.tsx"
11 | - launchApp: "expo.modules.widgetsync.example"
12 | - tapOn: "Clear"
13 | - pressKey: "Home"
14 | - assertNotVisible: "Hello from App.tsx"
15 |
16 |
17 |
--------------------------------------------------------------------------------
/e2e/ios.yml:
--------------------------------------------------------------------------------
1 | appId: "expo.modules.widgetsync.example"
2 | ---
3 | - longPressOn:
4 | id: "Home screen icons"
5 | - tapOn: "Add Widget"
6 | - tapOn: "Search Widgets"
7 | - inputText: "react-native"
8 | - tapOn: "react-native-widget-sync-example"
9 | - tapOn: " Add Widget"
10 | - launchApp: "expo.modules.widgetsync.example"
11 | - tapOn: "Set value"
12 | - pressKey: "Home"
13 | - assertVisible: "Hello from App.tsx"
14 | - launchApp: "expo.modules.widgetsync.example"
15 | - tapOn: "Clear"
16 | - pressKey: "Home"
17 | - assertNotVisible: "Hello from App.tsx"
18 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = function (api) {
3 | api.cache(true);
4 | return {
5 | presets: ['babel-preset-expo'],
6 | plugins: [
7 | [
8 | 'module-resolver',
9 | {
10 | extensions: ['.tsx', '.ts', '.js', '.json'],
11 | alias: {
12 | // For development, we want to alias the library to the source
13 | 'react-native-widget-sync': path.join(__dirname, '..', 'src', 'index.ts'),
14 | },
15 | },
16 | ],
17 | ],
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const createConfigAsync = require('@expo/webpack-config');
2 | const path = require('path');
3 |
4 | module.exports = async (env, argv) => {
5 | const config = await createConfigAsync(
6 | {
7 | ...env,
8 | babel: {
9 | dangerouslyAddModulePathsToTranspile: ['react-native-widget-sync'],
10 | },
11 | },
12 | argv
13 | );
14 | config.resolve.modules = [
15 | path.resolve(__dirname, './node_modules'),
16 | path.resolve(__dirname, '../node_modules'),
17 | ];
18 |
19 | return config;
20 | };
21 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values-night-v31/themes_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
10 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | ios/
20 | android/
21 |
22 | # Metro
23 | .metro-health-check*
24 |
25 | # debug
26 | npm-debug.*
27 | yarn-debug.*
28 | yarn-error.*
29 |
30 | # macOS
31 | .DS_Store
32 | *.pem
33 |
34 | # local env files
35 | .env*.local
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
--------------------------------------------------------------------------------
/plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withPlugins } from "@expo/config-plugins";
2 |
3 | import { withWidgetAndroid } from "./android/withWidgetAndroid";
4 | import { withWidgetIos } from "./ios/withWidgetIos";
5 |
6 | export interface Props {
7 | widgetName: string;
8 | ios: {
9 | devTeamId: string;
10 | appGroupIdentifier: string;
11 | topLevelFiles?: string[];
12 | };
13 | }
14 |
15 | const withAppConfigs: ConfigPlugin = (config, options) => {
16 | return withPlugins(config, [
17 | [withWidgetAndroid, options],
18 | [withWidgetIos, options],
19 | ]);
20 | };
21 |
22 | export default withAppConfigs;
23 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/styles_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values-v31/themes_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/xml/sample_widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values-v21/styles_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-widget-sync-example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "expo": "~49.0.5",
13 | "react": "18.2.0",
14 | "react-native": "0.72.3",
15 | "expo-splash-screen": "~0.20.4",
16 | "expo-status-bar": "~1.6.0"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.20.0",
20 | "@types/react": "~18.0.14",
21 | "typescript": "^5.1.3"
22 | },
23 | "private": true,
24 | "expo": {
25 | "autolinking": {
26 | "nativeModulesDir": ".."
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/plugin/src/android/withWidgetProjectBuildGradle.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withProjectBuildGradle } from "@expo/config-plugins";
2 |
3 | /**
4 | * Add configuration of kotlin-gradle-plugin
5 | * @param config
6 | * @returns
7 | */
8 | export const withWidgetProjectBuildGradle: ConfigPlugin = (config) => {
9 | return withProjectBuildGradle(config, async (newConfig) => {
10 | const buildGradle = newConfig.modResults.contents;
11 |
12 | const search = /dependencies\s?{/;
13 | const replace = `dependencies {
14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:\${project.ext.kotlinVersion}"`;
15 | const newBuildGradle = buildGradle.replace(search, replace);
16 | newConfig.modResults.contents = newBuildGradle;
17 | return newConfig;
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # VSCode
6 | .vscode/
7 | jsconfig.json
8 |
9 | # Xcode
10 | #
11 | build/
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata
21 | *.xccheckout
22 | *.moved-aside
23 | DerivedData
24 | *.hmap
25 | *.ipa
26 | *.xcuserstate
27 | project.xcworkspace
28 |
29 | # Android/IJ
30 | #
31 | .classpath
32 | .cxx
33 | .gradle
34 | .idea
35 | .project
36 | .settings
37 | local.properties
38 | android.iml
39 | android/app/libs
40 | android/keystores/debug.keystore
41 |
42 | # Cocoapods
43 | #
44 | example/ios/Pods
45 |
46 | # Ruby
47 | example/vendor/
48 |
49 | # node.js
50 | #
51 | node_modules/
52 | npm-debug.log
53 | yarn-debug.log
54 | yarn-error.log
55 |
56 | # Expo
57 | .expo/*
58 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values/themes_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
17 |
--------------------------------------------------------------------------------
/plugin/src/android/withWidgetAndroid.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin } from "@expo/config-plugins";
2 |
3 | import { withWidgetAppBuildGradle } from "./withWidgetAppBuildGradle";
4 | import { withWidgetManifest } from "./withWidgetManifest";
5 | import { withWidgetProjectBuildGradle } from "./withWidgetProjectBuildGradle";
6 | import { withWidgetSourceCodes } from "./withWidgetSourceCodes";
7 | import { Props } from "..";
8 |
9 | export const withWidgetAndroid: ConfigPlugin = (
10 | config,
11 | { widgetName, ios: { appGroupIdentifier } }
12 | ) => {
13 | config = withWidgetManifest(config, { widgetName });
14 | config = withWidgetProjectBuildGradle(config);
15 | config = withWidgetAppBuildGradle(config);
16 | config = withWidgetSourceCodes(config, {
17 | widgetName,
18 | appGroupName: appGroupIdentifier,
19 | });
20 | return config;
21 | };
22 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/values-v31/styles_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/plugin/src/android/static/res/layout/sample_widget.xml:
--------------------------------------------------------------------------------
1 |
6 |
18 |
--------------------------------------------------------------------------------
/plugin/src/android/withWidgetAppBuildGradle.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withAppBuildGradle } from "@expo/config-plugins";
2 |
3 | /**
4 | * Add "apply plugin: kotlin-android" to app build.gradle
5 | * @param config
6 | * @returns
7 | */
8 | export const withWidgetAppBuildGradle: ConfigPlugin = (config) => {
9 | return withAppBuildGradle(config, async (newConfig) => {
10 | const buildGradle = newConfig.modResults.contents;
11 | const search = /(apply plugin: "com\.android\.application"\n)/gm;
12 | const replace = `$1apply plugin: "kotlin-android"\n`;
13 | let newBuildGradle = buildGradle.replace(search, replace);
14 |
15 | newBuildGradle = newBuildGradle.replace(
16 | /dependencies\s?{/,
17 | `dependencies {
18 | implementation 'com.google.code.gson:gson:2.10.1'`
19 | );
20 |
21 | newConfig.modResults.contents = newBuildGradle;
22 | return newConfig;
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/ios/ReactNativeWidgetSync.podspec:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = 'ReactNativeWidgetSync'
7 | s.version = package['version']
8 | s.summary = package['description']
9 | s.description = package['description']
10 | s.license = package['license']
11 | s.author = package['author']
12 | s.homepage = package['homepage']
13 | s.platform = :ios, '13.0'
14 | s.swift_version = '5.4'
15 | s.source = { git: 'https://github.com/pafry7/react-native-widget-sync' }
16 | s.static_framework = true
17 |
18 | s.dependency 'ExpoModulesCore'
19 |
20 | # Swift/Objective-C compatibility
21 | s.pod_target_xcconfig = {
22 | 'DEFINES_MODULE' => 'YES',
23 | 'SWIFT_COMPILATION_MODE' => 'wholemodule'
24 | }
25 |
26 | s.source_files = "**/*.{h,m,swift}"
27 | end
28 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config');
3 | const path = require('path');
4 |
5 | const config = getDefaultConfig(__dirname);
6 |
7 | // npm v7+ will install ../node_modules/react-native because of peerDependencies.
8 | // To prevent the incompatible react-native bewtween ./node_modules/react-native and ../node_modules/react-native,
9 | // excludes the one from the parent folder when bundling.
10 | config.resolver.blockList = [
11 | ...Array.from(config.resolver.blockList ?? []),
12 | new RegExp(path.resolve('..', 'node_modules', 'react-native')),
13 | ];
14 |
15 | config.resolver.nodeModulesPaths = [
16 | path.resolve(__dirname, './node_modules'),
17 | path.resolve(__dirname, '../node_modules'),
18 | ];
19 |
20 | config.watchFolders = [path.resolve(__dirname, '..')];
21 |
22 | config.transformer.getTransformOptions = async () => ({
23 | transform: {
24 | experimentalImportSupport: false,
25 | inlineRequires: true,
26 | },
27 | });
28 |
29 | module.exports = config;
--------------------------------------------------------------------------------
/plugin/src/ios/withWidgetPlist.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConfigPlugin,
3 | InfoPlist,
4 | withDangerousMod,
5 | } from "@expo/config-plugins";
6 | import plist from "@expo/plist";
7 | import * as fs from "fs";
8 | import * as path from "path";
9 |
10 | interface Props {
11 | targetName: string;
12 | }
13 |
14 | export const withWidgetPlist: ConfigPlugin = (
15 | config,
16 | { targetName }
17 | ) => {
18 | return withDangerousMod(config, [
19 | "ios",
20 | async (config) => {
21 | const extensionRootPath = path.join(
22 | config.modRequest.platformProjectRoot,
23 | targetName
24 | );
25 | const extensionPlistPath = path.join(extensionRootPath, "Info.plist");
26 |
27 | const extensionPlist: InfoPlist = {
28 | NSExtension: {
29 | NSExtensionPointIdentifier: "com.apple.widgetkit-extension",
30 | },
31 | };
32 |
33 | await fs.promises.mkdir(path.dirname(extensionPlistPath), {
34 | recursive: true,
35 | });
36 | await fs.promises.writeFile(
37 | extensionPlistPath,
38 | plist.build(extensionPlist)
39 | );
40 |
41 | return config;
42 | },
43 | ]);
44 | };
45 |
--------------------------------------------------------------------------------
/plugin/src/ios/withWidgetIos.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin } from "@expo/config-plugins";
2 | import { withWidgetEntitlements } from "./withWidgetEntitlements";
3 | import { withWidgetSourceFiles } from "./withWidgetSourceFiles";
4 | import { withWidgetPlist } from "./withWidgetPlist";
5 | import { withWidgetXcodeTarget } from "./withWidgetXcodeTarget";
6 | import { Props } from "..";
7 |
8 | // make defaults
9 | export const DEFAULT_WIDGET_TARGET_NAME = "widget";
10 | export const DEFAULT_TOP_LEVEL_FILES = ["Assets.xcassets", "widget.swift"];
11 |
12 | export const withWidgetIos: ConfigPlugin = (
13 | config,
14 | { widgetName, ios }
15 | ) => {
16 | const { appGroupIdentifier, devTeamId } = ios;
17 | const targetName = widgetName ?? DEFAULT_WIDGET_TARGET_NAME;
18 | const topLevelFiles = ios.topLevelFiles ?? DEFAULT_TOP_LEVEL_FILES;
19 |
20 | config = withWidgetEntitlements(config, { targetName, appGroupIdentifier });
21 | config = withWidgetSourceFiles(config, { targetName, appGroupIdentifier });
22 | config = withWidgetPlist(config, { targetName });
23 | config = withWidgetXcodeTarget(config, {
24 | devTeamId,
25 | targetName,
26 | topLevelFiles,
27 | });
28 | return config;
29 | };
30 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Import the native module. On web, it will be resolved to RefreshWidget.web.ts
2 | // and on native platforms to RefreshWidget.ts
3 | import RefreshWidgetModule from "./ReactNativeWidgetSyncModule";
4 |
5 | export function reloadAll(): void {
6 | return RefreshWidgetModule.reloadAll();
7 | }
8 | export function setItem(appGroup: string, key: string, value: any): void;
9 | export function setItem(
10 | appGroup: string,
11 | key?: string,
12 | value?: any
13 | ): (key: string, value: any) => void;
14 |
15 | export function setItem(appGroup: string, key?: string, value?: any) {
16 | if (typeof key !== "undefined" && typeof value !== "undefined") {
17 | return RefreshWidgetModule.setItem(value, key, appGroup);
18 | }
19 | return (key: string, value: any) =>
20 | RefreshWidgetModule.setItem(value, key, appGroup);
21 | }
22 |
23 | export function getItem(appGroup: string, key: string): string;
24 | export function getItem(
25 | appGroup: string,
26 | key?: string
27 | ): (key: string) => string;
28 |
29 | export function getItem(appGroup: string, key?: string) {
30 | if (typeof key !== "undefined") {
31 | return RefreshWidgetModule.getItem(key, appGroup);
32 | }
33 | return (key: string) => RefreshWidgetModule.getItem(key, appGroup);
34 | }
35 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-widget-sync-example",
4 | "slug": "react-native-widget-sync-example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": ["**/*"],
15 | "ios": {
16 | "supportsTablet": true,
17 | "bundleIdentifier": "expo.modules.widgetsync.example",
18 | "entitlements": {
19 | "com.apple.security.application-groups": [
20 | "group.expo.modules.widgetsync.example"
21 | ]
22 | }
23 | },
24 | "android": {
25 | "adaptiveIcon": {
26 | "foregroundImage": "./assets/adaptive-icon.png",
27 | "backgroundColor": "#ffffff"
28 | },
29 | "package": "expo.modules.widgetsync.example"
30 | },
31 | "web": {
32 | "favicon": "./assets/favicon.png"
33 | },
34 | "plugins": [
35 | [
36 | "../app.plugin.js",
37 | {
38 | "widgetName": "widget",
39 | "ios": {
40 | "devTeamId": "",
41 | "appGroupIdentifier": "group.expo.modules.widgetsync.example"
42 | }
43 | }
44 | ]
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-widget-sync",
3 | "version": "0.1.0",
4 | "description": "The module allows refreshing and sharing of data between the main application and the widget.",
5 | "main": "build/index.js",
6 | "types": "build/index.d.ts",
7 | "scripts": {
8 | "clean": "expo-module clean plugin",
9 | "lint": "expo-module lint",
10 | "test": "expo-module test",
11 | "prepublishOnly": "expo-module prepublishOnly",
12 | "expo-module": "expo-module",
13 | "open:ios": "open -a \"Xcode\" example/ios",
14 | "open:android": "open -a \"Android Studio\" example/android",
15 | "build:plugin": "yarn clean && EXPO_NONINTERACTIVE=1 expo-module build plugin && ./plugin/src/scripts/copy.sh",
16 | "prepare": "yarn build:plugin"
17 | },
18 | "keywords": [
19 | "react-native",
20 | "expo",
21 | "widget",
22 | "react-native-widget-sync",
23 | "ReactNativeWidgetSync"
24 | ],
25 | "repository": "https://github.com/pafry7/react-native-widget-sync",
26 | "bugs": {
27 | "url": "https://github.com/pafry7/react-native-widget-sync/issues"
28 | },
29 | "author": "Patryk Fryda ()",
30 | "license": "MIT",
31 | "homepage": "https://github.com/pafry7/react-native-widget-sync#readme",
32 | "devDependencies": {
33 | "@types/react": "^18.0.25",
34 | "expo-module-scripts": "^3.0.11",
35 | "expo-modules-core": "^1.5.7"
36 | },
37 | "peerDependencies": {
38 | "expo": "*",
39 | "react": "*",
40 | "react-native": "*"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/plugin/src/android/withWidgetManifest.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AndroidConfig,
3 | ConfigPlugin,
4 | withAndroidManifest,
5 | } from "@expo/config-plugins";
6 |
7 | export const withWidgetManifest: ConfigPlugin<{ widgetName: string }> = (
8 | config,
9 | { widgetName }
10 | ) => {
11 | return withAndroidManifest(config, async (newConfig) => {
12 | const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
13 | newConfig.modResults
14 | );
15 | const widgetReceivers = await buildWidgetsReceivers(widgetName);
16 | mainApplication.receiver = widgetReceivers;
17 |
18 | AndroidConfig.Manifest.addMetaDataItemToMainApplication(
19 | mainApplication,
20 | "WIDGET_NAME",
21 | widgetName
22 | );
23 |
24 | return newConfig;
25 | });
26 | };
27 |
28 | async function buildWidgetsReceivers(widgetName: string) {
29 | return [
30 | {
31 | $: {
32 | "android:name": `.${widgetName}`,
33 | "android:exported": "false" as const,
34 | },
35 | "intent-filter": [
36 | {
37 | action: [
38 | {
39 | $: {
40 | "android:name": "android.appwidget.action.APPWIDGET_UPDATE",
41 | },
42 | },
43 | ],
44 | },
45 | ],
46 | "meta-data": [
47 | {
48 | $: {
49 | "android:name": "android.appwidget.provider",
50 | "android:resource": "@xml/sample_widget_info",
51 | },
52 | },
53 | ],
54 | },
55 | ];
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > The development of the npm module has been temporarily postponed.
2 |
3 | # react-native-widget-sync
4 |
5 | The module allows refreshing and sharing of data between the main application and the widget.
6 |
7 | ## Usage in expo managed project
8 |
9 | See https://github.com/pafry7/widget-expo-example
10 |
11 | ## Setup
12 |
13 | 1. Clone repo
14 | ```
15 | git clone git@github.com:pafry7/react-native-widget-sync.git
16 | ```
17 |
18 | 2. Install dependencies
19 |
20 | ```
21 | cd react-native-widget-sync
22 | yarn
23 | cd example
24 | yarn
25 | ```
26 |
27 | 3. Configure expo plugin in `/example`
28 | Add in `app.json`
29 |
30 | ```
31 | "ios": {
32 | ...
33 | "entitlements": {
34 | "appGroupIdentifier":
35 | },
36 | },
37 | "plugins": {
38 | [
39 | "react-native-widget-sync",
40 | {
41 | "widgetName":
42 | "ios": {
43 | "devTeamId":
44 | "appGroupIdentifier": "
45 | }
46 | }
47 | ],
48 | }
49 |
50 | ```
51 | ## Running the example
52 |
53 | Run
54 |
55 | ```
56 | npx expo prebuild
57 | ```
58 |
59 | and then
60 |
61 | ```
62 | npx expo run:[ios|android]
63 | ```
64 |
65 | ## How does it work
66 |
67 | When you run expo prebuild for the first time, it generates a folder containing the source files for widgets and then copies them to both the ios and android directories. Remember to run prebuild each time you make changes to the widget's source files.
68 |
69 | ## Video
70 |
71 | https://github.com/pafry7/react-native-widget-sync/assets/41058200/f0dcf601-31a3-4ead-9eaf-0ae8a67346c6
72 |
73 |
74 | ## Sources
75 | - https://github.com/gaishimo/eas-widget-example
76 | - https://github.com/matallui/demo-screen-capture
77 |
78 |
--------------------------------------------------------------------------------
/plugin/src/ios/withWidgetEntitlements.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConfigPlugin,
3 | InfoPlist,
4 | withXcodeProject,
5 | } from "@expo/config-plugins";
6 | import plist from "@expo/plist";
7 | import * as fs from "fs";
8 | import * as path from "path";
9 |
10 | interface Props {
11 | appGroupIdentifier: string;
12 | targetName: string;
13 | }
14 |
15 | export const withWidgetEntitlements: ConfigPlugin = (
16 | config,
17 | { appGroupIdentifier, targetName }
18 | ) => {
19 | return withXcodeProject(config, async (config) => {
20 | const entitlementsFilename = `${targetName}.entitlements`;
21 | const extensionRootPath = path.join(
22 | config.modRequest.platformProjectRoot,
23 | targetName
24 | );
25 | const entitlementsPath = path.join(extensionRootPath, entitlementsFilename);
26 |
27 | const extensionEntitlements: InfoPlist = {
28 | "com.apple.security.application-groups": [appGroupIdentifier],
29 | };
30 |
31 | // create file
32 | await fs.promises.mkdir(path.dirname(entitlementsPath), {
33 | recursive: true,
34 | });
35 | await fs.promises.writeFile(
36 | entitlementsPath,
37 | plist.build(extensionEntitlements)
38 | );
39 |
40 | // add file to extension group
41 | const proj = config.modResults;
42 | const targetUuid = proj.findTargetKey(targetName);
43 | const groupUuid = proj.findPBXGroupKey({ name: targetName });
44 |
45 | proj.addFile(entitlementsFilename, groupUuid, {
46 | target: targetUuid,
47 | lastKnownFileType: "text.plist.entitlements",
48 | });
49 |
50 | // update build properties
51 | proj.updateBuildProperty(
52 | "CODE_SIGN_ENTITLEMENTS",
53 | `${targetName}/${entitlementsFilename}`,
54 | null,
55 | targetName
56 | );
57 |
58 | return config;
59 | });
60 | };
61 |
--------------------------------------------------------------------------------
/ios/ReactNativeWidgetSyncModule.swift:
--------------------------------------------------------------------------------
1 | import ExpoModulesCore
2 | import WidgetKit
3 |
4 | internal class VersionException: Exception {
5 | override var reason: String {
6 | "Function is available only on iOS 14 and higher"
7 | }
8 | }
9 |
10 | public class ReactNativeWidgetSyncModule: Module {
11 | // Each module class must implement the definition function. The definition consists of components
12 | // that describes the module's functionality and behavior.
13 | // See https://docs.expo.dev/modules/module-api for more details about available components.
14 | public func definition() -> ModuleDefinition {
15 | // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
16 | // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
17 | // The module will be accessible from `requireNativeModule('RefreshWidget')` in JavaScript.
18 | Name("ReactNativeWidgetSync")
19 |
20 | Function("reloadAll") { () -> Void in
21 | if #available(iOS 14, *) {
22 | WidgetCenter.shared.reloadAllTimelines()
23 | } else {
24 | throw VersionException()
25 | }
26 | }
27 |
28 | Function("setItem") { (value: String, key: String, appGroup: String) -> Void in
29 | if let userDefaults = UserDefaults(suiteName: appGroup){
30 | userDefaults.set(value, forKey: key )
31 | }
32 | }
33 |
34 | Function("getItem") { ( key: String, appGroup: String) -> String? in
35 | if let userDefaults = UserDefaults(suiteName: appGroup){
36 | return userDefaults.string(forKey: key)
37 | }
38 | return nil
39 | }
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/plugin/src/ios/static/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "iphone",
5 | "scale": "2x",
6 | "size": "20x20"
7 | },
8 | {
9 | "idiom": "iphone",
10 | "scale": "3x",
11 | "size": "20x20"
12 | },
13 | {
14 | "idiom": "iphone",
15 | "scale": "2x",
16 | "size": "29x29"
17 | },
18 | {
19 | "idiom": "iphone",
20 | "scale": "3x",
21 | "size": "29x29"
22 | },
23 | {
24 | "idiom": "iphone",
25 | "scale": "2x",
26 | "size": "40x40"
27 | },
28 | {
29 | "idiom": "iphone",
30 | "scale": "3x",
31 | "size": "40x40"
32 | },
33 | {
34 | "idiom": "iphone",
35 | "scale": "2x",
36 | "size": "60x60"
37 | },
38 | {
39 | "idiom": "iphone",
40 | "scale": "3x",
41 | "size": "60x60"
42 | },
43 | {
44 | "idiom": "ipad",
45 | "scale": "1x",
46 | "size": "20x20"
47 | },
48 | {
49 | "idiom": "ipad",
50 | "scale": "2x",
51 | "size": "20x20"
52 | },
53 | {
54 | "idiom": "ipad",
55 | "scale": "1x",
56 | "size": "29x29"
57 | },
58 | {
59 | "idiom": "ipad",
60 | "scale": "2x",
61 | "size": "29x29"
62 | },
63 | {
64 | "idiom": "ipad",
65 | "scale": "1x",
66 | "size": "40x40"
67 | },
68 | {
69 | "idiom": "ipad",
70 | "scale": "2x",
71 | "size": "40x40"
72 | },
73 | {
74 | "idiom": "ipad",
75 | "scale": "2x",
76 | "size": "76x76"
77 | },
78 | {
79 | "idiom": "ipad",
80 | "scale": "2x",
81 | "size": "83.5x83.5"
82 | },
83 | {
84 | "idiom": "ios-marketing",
85 | "scale": "1x",
86 | "size": "1024x1024"
87 | }
88 | ],
89 | "info": {
90 | "author": "xcode",
91 | "version": 1
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/plugin/src/ios/static/widget.swift:
--------------------------------------------------------------------------------
1 | // widget.swift
2 | // widget
3 | //
4 |
5 | import WidgetKit
6 | import SwiftUI
7 | import Foundation
8 |
9 | struct Provider: TimelineProvider {
10 | func placeholder(in context: Context) -> SimpleEntry {
11 | SimpleEntry(date: Date(), text: "Placeholder")
12 | }
13 |
14 | func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
15 | let entry = SimpleEntry(date: Date(), text: "Snapshot")
16 | completion(entry)
17 | }
18 |
19 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
20 | let text = getItem()
21 |
22 | let entry = SimpleEntry(date: Date(), text: text)
23 |
24 | let timeline = Timeline(entries: [entry], policy: .never)
25 | completion(timeline)
26 | }
27 |
28 |
29 | private func getItem() -> String {
30 | let userDefaults = UserDefaults(suiteName: "group.com.example.widget")
31 | return userDefaults?.string(forKey: "savedData") ?? ""
32 | }
33 |
34 | }
35 |
36 | struct SimpleEntry: TimelineEntry {
37 | let date: Date
38 | let text: String
39 | }
40 |
41 | struct widgetEntryView : View {
42 | var entry: Provider.Entry
43 |
44 | var body: some View {
45 | Text(entry.text)
46 | }
47 |
48 |
49 | }
50 |
51 | @main
52 | struct widget: Widget {
53 | let kind: String = "widget"
54 |
55 | var body: some WidgetConfiguration {
56 | StaticConfiguration(kind: kind, provider: Provider()) { entry in
57 | widgetEntryView(entry: entry)
58 | }
59 | .configurationDisplayName("Widget name")
60 | .description("Widget description")
61 | .supportedFamilies([.systemSmall])
62 | }
63 | }
64 |
65 | struct widget_Previews: PreviewProvider {
66 | static var previews: some View {
67 | widgetEntryView(entry: SimpleEntry(date: Date(), text: "Preview"))
68 | .previewContext(WidgetPreviewContext(family: .systemSmall))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/plugin/src/android/static/java/package_name/SampleWidget.kt:
--------------------------------------------------------------------------------
1 | package com.example.app
2 |
3 | import android.appwidget.AppWidgetManager
4 | import android.appwidget.AppWidgetProvider
5 | import android.content.Context
6 | import android.widget.RemoteViews
7 | import android.content.SharedPreferences
8 | import android.os.SystemClock
9 | import android.content.ComponentName;
10 | import android.content.Intent;
11 | import android.app.PendingIntent;
12 | import android.net.Uri;
13 |
14 | /**
15 | * Implementation of App Widget functionality.
16 | */
17 | class SampleWidget : AppWidgetProvider() {
18 | override fun onUpdate(
19 | context: Context,
20 | appWidgetManager: AppWidgetManager,
21 | appWidgetIds: IntArray
22 | ) {
23 | // There may be multiple widgets active, so update all of them
24 | for (appWidgetId in appWidgetIds) {
25 | updateAppWidget(context, appWidgetManager, appWidgetId)
26 | }
27 | }
28 |
29 | override fun onEnabled(context: Context) {
30 | // Enter relevant functionality for when the first widget is created
31 | }
32 |
33 | override fun onDisabled(context: Context) {
34 | // Enter relevant functionality for when the last widget is disabled
35 | }
36 | }
37 |
38 | internal fun updateAppWidget(
39 | context: Context,
40 | appWidgetManager: AppWidgetManager,
41 | appWidgetId: Int
42 | ) {
43 | // change this to group value passed in app.json
44 | val text = getItem(context, "savedData", "group.com.example.widget") ?: ""
45 |
46 | // Construct the RemoteViews object
47 | val views = RemoteViews(context.packageName, R.layout.sample_widget)
48 | views.setTextViewText(R.id.appwidget_text, text)
49 |
50 | // Instruct the widget manager to update the widget
51 | appWidgetManager.updateAppWidget(appWidgetId, views)
52 | }
53 |
54 | internal fun getItem(
55 | context: Context,
56 | key: String,
57 | preferenceName: String
58 | ): String? {
59 | val preferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE)
60 | return preferences.getString(key, null)
61 | }
62 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'maven-publish'
4 |
5 | group = 'expo.modules.widgetsync'
6 | version = '0.1.0'
7 |
8 | buildscript {
9 | def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10 | if (expoModulesCorePlugin.exists()) {
11 | apply from: expoModulesCorePlugin
12 | applyKotlinExpoModulesCorePlugin()
13 | }
14 |
15 | // Simple helper that allows the root project to override versions declared by this library.
16 | ext.safeExtGet = { prop, fallback ->
17 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18 | }
19 |
20 | // Ensures backward compatibility
21 | ext.getKotlinVersion = {
22 | if (ext.has("kotlinVersion")) {
23 | ext.kotlinVersion()
24 | } else {
25 | ext.safeExtGet("kotlinVersion", "1.8.10")
26 | }
27 | }
28 |
29 | repositories {
30 | mavenCentral()
31 | }
32 |
33 | dependencies {
34 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
35 | }
36 | }
37 |
38 | afterEvaluate {
39 | publishing {
40 | publications {
41 | release(MavenPublication) {
42 | from components.release
43 | }
44 | }
45 | repositories {
46 | maven {
47 | url = mavenLocal().url
48 | }
49 | }
50 | }
51 | }
52 |
53 | android {
54 | compileSdkVersion safeExtGet("compileSdkVersion", 33)
55 |
56 | compileOptions {
57 | sourceCompatibility JavaVersion.VERSION_11
58 | targetCompatibility JavaVersion.VERSION_11
59 | }
60 |
61 | kotlinOptions {
62 | jvmTarget = JavaVersion.VERSION_11.majorVersion
63 | }
64 |
65 | namespace "expo.modules.widgetsync"
66 | defaultConfig {
67 | minSdkVersion safeExtGet("minSdkVersion", 21)
68 | targetSdkVersion safeExtGet("targetSdkVersion", 33)
69 | versionCode 1
70 | versionName "0.1.0"
71 | }
72 | lintOptions {
73 | abortOnError false
74 | }
75 | publishing {
76 | singleVariant("release") {
77 | withSourcesJar()
78 | }
79 | }
80 | }
81 |
82 | repositories {
83 | mavenCentral()
84 | }
85 |
86 | dependencies {
87 | implementation project(':expo-modules-core')
88 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
89 | }
90 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Pressable, StyleSheet, Text, View } from "react-native";
3 | import { getItem, reloadAll, setItem } from "react-native-widget-sync";
4 |
5 | const GROUP_NAME = "group.expo.modules.widgetsync.example";
6 |
7 | const getSharedData = getItem(GROUP_NAME);
8 | const setSharedData = setItem(GROUP_NAME);
9 |
10 | function Button({ onPress, title }: { title: string; onPress: () => void }) {
11 | return (
12 |
13 | {title}
14 |
15 | );
16 | }
17 |
18 | export default function App() {
19 | const [value, setValue] = useState(getSharedData(GROUP_NAME) ?? "");
20 |
21 | useEffect(() => {
22 | setSharedData("savedData", value);
23 | reloadAll();
24 | }, [value]);
25 |
26 | const onPress = () => {
27 | setValue("Hello from App.tsx");
28 | };
29 |
30 | const clear = () => {
31 | setValue("");
32 | };
33 |
34 | return (
35 |
36 |
37 | Current value
38 | {value}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | container: {
49 | flex: 1,
50 | backgroundColor: "#fff",
51 | alignItems: "center",
52 | justifyContent: "center",
53 | gap: 24,
54 | },
55 | textContainer: {
56 | alignItems: "center",
57 | justifyContent: "center",
58 | gap: 4,
59 | width: "100%",
60 | },
61 | description: {
62 | fontSize: 14,
63 | color: "dimgray",
64 | },
65 | value: {
66 | fontSize: 20,
67 | fontWeight: "500",
68 | },
69 | button: {
70 | alignItems: "center",
71 | justifyContent: "center",
72 | paddingVertical: 12,
73 | paddingHorizontal: 24,
74 | borderRadius: 4,
75 | elevation: 3,
76 | backgroundColor: "black",
77 | },
78 | buttonText: {
79 | fontSize: 14,
80 | lineHeight: 18,
81 | fontWeight: "bold",
82 | letterSpacing: 0.25,
83 | color: "white",
84 | },
85 | spacer: {
86 | height: 10,
87 | },
88 | });
89 |
--------------------------------------------------------------------------------
/plugin/src/ios/withWidgetSourceFiles.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withXcodeProject } from "@expo/config-plugins";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | interface Props {
6 | targetName: string;
7 | appGroupIdentifier: string;
8 | }
9 |
10 | export const withWidgetSourceFiles: ConfigPlugin = (
11 | config,
12 | { targetName, appGroupIdentifier }
13 | ) => {
14 | return withXcodeProject(config, async (config) => {
15 | const extensionRootPath = path.join(
16 | config.modRequest.platformProjectRoot,
17 | targetName
18 | );
19 | const projectPath = config.modRequest.projectRoot;
20 | const widgetSourceDirPath = path.join(projectPath, targetName, "ios");
21 | if (!fs.existsSync(widgetSourceDirPath)) {
22 | await fs.promises.mkdir(widgetSourceDirPath, { recursive: true });
23 | const widgetStaticSourceDirPath = path.join(__dirname, "static");
24 | await fs.promises.copyFile(
25 | path.join(widgetStaticSourceDirPath, "widget.swift"),
26 | path.join(widgetSourceDirPath, "widget.swift")
27 | );
28 | await fs.promises.cp(
29 | path.join(widgetStaticSourceDirPath, "Assets.xcassets"),
30 | path.join(widgetSourceDirPath, "Assets.xcassets"),
31 | { recursive: true }
32 | );
33 |
34 | const widgetSourceFilePath = path.join(
35 | widgetSourceDirPath,
36 | "widget.swift" // use to targetName
37 | );
38 | const content = fs.readFileSync(widgetSourceFilePath, "utf8");
39 | const newContent = content.replace(
40 | /group.com.example.widget/,
41 | `${appGroupIdentifier}`
42 | );
43 |
44 | fs.writeFileSync(widgetSourceFilePath, newContent);
45 | }
46 | await fs.promises.mkdir(extensionRootPath, { recursive: true });
47 | await fs.promises.copyFile(
48 | path.join(widgetSourceDirPath, "widget.swift"),
49 | path.join(extensionRootPath, "widget.swift")
50 | );
51 | await fs.promises.cp(
52 | path.join(widgetSourceDirPath, "Assets.xcassets"),
53 | path.join(extensionRootPath, "Assets.xcassets"),
54 | { recursive: true }
55 | );
56 |
57 | const proj = config.modResults;
58 | const targetUuid = proj.findTargetKey(targetName);
59 | const groupUuid = proj.findPBXGroupKey({ name: targetName });
60 |
61 | if (!targetUuid) {
62 | return Promise.reject(null);
63 | }
64 | if (!groupUuid) {
65 | return Promise.reject(null);
66 | }
67 |
68 | proj.addSourceFile(
69 | "widget.swift",
70 | {
71 | target: targetUuid,
72 | },
73 | groupUuid
74 | );
75 |
76 | return config;
77 | });
78 | };
79 |
--------------------------------------------------------------------------------
/plugin/src/android/withWidgetSourceCodes.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { ConfigPlugin, withDangerousMod } from "@expo/config-plugins";
3 | import fs from "fs";
4 | import path from "path";
5 |
6 | export const withWidgetSourceCodes: ConfigPlugin<{
7 | widgetName: string;
8 | appGroupName: string;
9 | }> = (config, { widgetName, appGroupName }) => {
10 | return withDangerousMod(config, [
11 | "android",
12 | async (newConfig) => {
13 | const projectRoot = newConfig.modRequest.projectRoot;
14 | const platformRoot = newConfig.modRequest.platformProjectRoot;
15 | const widgetDir = path.join(projectRoot, widgetName);
16 | await copyResourceFiles(widgetDir, platformRoot);
17 |
18 | const packageName = config.android?.package;
19 | prepareSourceCodes(
20 | widgetDir,
21 | platformRoot,
22 | packageName!,
23 | widgetName,
24 | appGroupName
25 | );
26 |
27 | return newConfig;
28 | },
29 | ]);
30 | };
31 |
32 | async function copyResourceFiles(
33 | widgetSourceDir: string,
34 | platformRoot: string
35 | ) {
36 | const source = path.join(widgetSourceDir, "android", "src", "main", "res");
37 | const resDest = path.join(platformRoot, "app", "src", "main", "res");
38 |
39 | if (!fs.existsSync(widgetSourceDir)) {
40 | const templateFolder = path.join(__dirname, "static", "res");
41 | console.log({ templateFolder, source });
42 | await fs.promises.cp(templateFolder, source, { recursive: true });
43 | }
44 | await fs.promises.cp(source, resDest, { recursive: true });
45 | }
46 |
47 | async function prepareSourceCodes(
48 | widgetSourceDir: string,
49 | platformRoot: string,
50 | packageName: string,
51 | widgetClassName: string,
52 | appGroupName: string
53 | ) {
54 | const packageDirPath = packageName.replace(/\./g, "/");
55 | const source = path.join(
56 | widgetSourceDir,
57 | `android/src/main/java/package_name`
58 | );
59 | const widgetSourceFilePath = path.join(source, `${widgetClassName}.kt`);
60 | if (!fs.existsSync(source)) {
61 | const templateFolder = path.join(__dirname, "static", "java/package_name");
62 | await fs.promises.cp(templateFolder, source, { recursive: true });
63 | await fs.promises.rename(
64 | path.join(source, "SampleWidget.kt"),
65 | widgetSourceFilePath
66 | );
67 | const content = fs.readFileSync(widgetSourceFilePath, "utf8");
68 | let newContent = content.replace(/SampleWidget/, `${widgetClassName}`);
69 | newContent = newContent.replace(
70 | /group.com.example.widget/,
71 | `${appGroupName}`
72 | );
73 |
74 | fs.writeFileSync(widgetSourceFilePath, newContent);
75 | }
76 |
77 | const dest = path.join(platformRoot, "app/src/main/java", packageDirPath);
78 | const widgetDestFilePath = path.join(dest, `${widgetClassName}.kt`);
79 |
80 | await fs.promises.cp(source, dest, { recursive: true });
81 |
82 | const content = fs.readFileSync(widgetDestFilePath, "utf8");
83 | const newContent = content.replace(
84 | /^package .*\s/,
85 | `package ${packageName}\n`
86 | );
87 | fs.writeFileSync(widgetDestFilePath, newContent);
88 | }
89 |
--------------------------------------------------------------------------------
/android/src/main/java/expo/modules/widgetsync/ReactNativeWidgetSyncModule.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.widgetsync
2 |
3 | import expo.modules.kotlin.modules.Module
4 | import expo.modules.kotlin.modules.ModuleDefinition
5 | import android.content.Context
6 | import android.content.SharedPreferences
7 | import android.appwidget.AppWidgetManager;
8 | import android.appwidget.AppWidgetProviderInfo
9 | import android.content.ComponentName;
10 | import android.content.Intent;
11 | import android.content.pm.PackageManager
12 |
13 | class ReactNativeWidgetSyncModule : Module() {
14 | // Each module class must implement the definition function. The definition consists of components
15 | // that describes the module's functionality and behavior.
16 | // See https://docs.expo.dev/modules/module-api for more details about available components.
17 | override fun definition() = ModuleDefinition {
18 | // Sets the name of the module that JavaScript code will use to refer to the module. Takes a
19 | // string as an argument.
20 | // Can be inferred from module's class name, but it's recommended to set it explicitly for
21 | // clarity.
22 | // The module will be accessible from `requireNativeModule('RefreshWidget')` in JavaScript.
23 | Name("ReactNativeWidgetSync")
24 |
25 | // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
26 | Function("reloadAll") {
27 | val widgetName = getWidgetName()
28 | if (widgetName == null) {
29 | throw Exception("Couldn't read widgetName from app.json")
30 | }
31 | val widgetComponentName = getWidgetComponentName(widgetName)
32 | if (widgetComponentName == null) {
33 | throw Exception("Couldn't find widgetName component name")
34 | }
35 | val widgetManager = AppWidgetManager.getInstance(context);
36 | val appWidgetIds = widgetManager.getAppWidgetIds(widgetComponentName)
37 |
38 | val updateIntent =
39 | Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null).setComponent(
40 | widgetComponentName
41 | )
42 | updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
43 |
44 | context.sendBroadcast(updateIntent);
45 | }
46 | Function("setItem") { value: String, key: String, appGroup: String ->
47 | getPreferences(appGroup).edit().putString(key, value).commit()
48 | }
49 |
50 | Function("getItem") { key: String, appGroup: String ->
51 | return@Function getPreferences(appGroup).getString(key, "")
52 | }
53 | }
54 |
55 | private val context
56 | get() = requireNotNull(appContext.reactContext)
57 |
58 | private fun getPreferences(appGroup: String): SharedPreferences {
59 | return context.getSharedPreferences(appGroup, Context.MODE_PRIVATE)
60 | }
61 |
62 | private fun getWidgetName(): String? {
63 | val applicationInfo = context.packageManager?.getApplicationInfo(
64 | context.packageName.toString(),
65 | PackageManager.GET_META_DATA
66 | )
67 | return applicationInfo?.metaData?.getString("WIDGET_NAME")
68 | }
69 |
70 | private fun getWidgetComponentName(widgetName: String): ComponentName? {
71 | val widgetList = AppWidgetManager.getInstance(context).getInstalledProviders()
72 | for (providerInfo in widgetList) {
73 | if (providerInfo.provider.getPackageName().equals(context.getPackageName())
74 | && providerInfo.provider.getShortClassName().endsWith("." + widgetName)
75 | ) {
76 | return providerInfo.provider;
77 | }
78 | }
79 |
80 | return null
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/plugin/src/ios/withWidgetXcodeTarget.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConfigPlugin,
3 | IOSConfig,
4 | withXcodeProject,
5 | } from "@expo/config-plugins";
6 | import * as util from "util";
7 |
8 | interface Props {
9 | targetName: string;
10 | devTeamId: string;
11 | topLevelFiles: string[];
12 | }
13 |
14 | interface AddXcodeTargetParams {
15 | appName: string;
16 | extensionName: string;
17 | extensionBundleIdentifier: string;
18 | currentProjectVersion: string;
19 | marketingVersion: string;
20 | devTeamId: string;
21 | }
22 |
23 | const addBroadcastExtensionXcodeTarget = async (
24 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
25 | {
26 | appName,
27 | extensionName,
28 | extensionBundleIdentifier,
29 | currentProjectVersion,
30 | marketingVersion,
31 | devTeamId,
32 | topLevelFiles,
33 | }: AddXcodeTargetParams & { topLevelFiles: string[] }
34 | ) => {
35 | if (proj.getFirstProject().firstProject.targets?.length > 1) return;
36 |
37 | const targetUuid = proj.generateUuid();
38 | const groupName = "Embed App Extensions";
39 |
40 | const xCConfigurationList = addXCConfigurationList(proj, {
41 | extensionBundleIdentifier,
42 | currentProjectVersion,
43 | marketingVersion,
44 | extensionName,
45 | appName,
46 | devTeamId,
47 | });
48 |
49 | const productFile = addProductFile(proj, extensionName, groupName);
50 |
51 | const target = addToPbxNativeTargetSection(proj, {
52 | extensionName,
53 | targetUuid,
54 | productFile,
55 | xCConfigurationList,
56 | });
57 |
58 | addToPbxProjectSection(proj, target);
59 |
60 | addTargetDependency(proj, target);
61 |
62 | const frameworkFileWidgetKit = proj.addFramework("WidgetKit.framework", {
63 | target: target.uuid,
64 | link: false,
65 | });
66 | const frameworkFileSwiftUI = proj.addFramework("SwiftUI.framework", {
67 | target: target.uuid,
68 | link: false,
69 | });
70 |
71 | addBuildPhases(proj, {
72 | extensionName,
73 | groupName,
74 | productFile,
75 | targetUuid,
76 | frameworkPaths: [frameworkFileSwiftUI.path, frameworkFileWidgetKit.path],
77 | });
78 |
79 | addPbxGroup(proj, productFile, extensionName, topLevelFiles);
80 | };
81 |
82 | export function quoted(str: string) {
83 | return util.format(`"%s"`, str);
84 | }
85 |
86 | const addXCConfigurationList = (
87 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
88 | {
89 | extensionBundleIdentifier,
90 | currentProjectVersion,
91 | marketingVersion,
92 | extensionName,
93 | devTeamId,
94 | }: AddXcodeTargetParams
95 | ) => {
96 | const commonBuildSettings = {
97 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "AccentColor",
98 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME: "WidgetBackground",
99 | CLANG_ANALYZER_NONNULL: "YES",
100 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: "YES_AGGRESSIVE",
101 | CLANG_CXX_LANGUAGE_STANDARD: quoted("gnu++17"),
102 | CLANG_ENABLE_OBJC_WEAK: "YES",
103 | CLANG_WARN_DOCUMENTATION_COMMENTS: "YES",
104 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER: "YES",
105 | CLANG_WARN_UNGUARDED_AVAILABILITY: "YES_AGGRESSIVE",
106 | CODE_SIGN_STYLE: "Automatic",
107 | CURRENT_PROJECT_VERSION: currentProjectVersion,
108 | DEVELOPMENT_TEAM: devTeamId,
109 | GCC_C_LANGUAGE_STANDARD: "gnu11",
110 | GENERATE_INFOPLIST_FILE: "YES",
111 | INFOPLIST_FILE: `${extensionName}/Info.plist`,
112 | INFOPLIST_KEY_CFBundleDisplayName: `${extensionName}`,
113 | INFOPLIST_KEY_NSHumanReadableCopyright: quoted(""),
114 | IPHONEOS_DEPLOYMENT_TARGET: "14.0",
115 | LD_RUNPATH_SEARCH_PATHS: quoted(
116 | "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
117 | ),
118 | MARKETING_VERSION: marketingVersion,
119 | MTL_FAST_MATH: "YES",
120 | PRODUCT_BUNDLE_IDENTIFIER: quoted(extensionBundleIdentifier),
121 | PRODUCT_NAME: quoted("$(TARGET_NAME)"),
122 | SKIP_INSTALL: "YES",
123 | SWIFT_EMIT_LOC_STRINGS: "YES",
124 | SWIFT_VERSION: "5.0",
125 | TARGETED_DEVICE_FAMILY: quoted("1"),
126 | SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG",
127 | SWIFT_OPTIMIZATION_LEVEL: "-Onone",
128 | };
129 |
130 | const buildConfigurationsList = [
131 | {
132 | name: "Debug",
133 | isa: "XCBuildConfiguration",
134 | buildSettings: {
135 | ...commonBuildSettings,
136 | DEBUG_INFORMATION_FORMAT: "dwarf",
137 | MTL_ENABLE_DEBUG_INFO: "INCLUDE_SOURCE",
138 | SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG",
139 | SWIFT_OPTIMIZATION_LEVEL: quoted("-Onone"),
140 | },
141 | },
142 | {
143 | name: "Release",
144 | isa: "XCBuildConfiguration",
145 | buildSettings: {
146 | ...commonBuildSettings,
147 | COPY_PHASE_STRIP: "NO",
148 | DEBUG_INFORMATION_FORMAT: quoted("dwarf-with-dsym"),
149 | SWIFT_OPTIMIZATION_LEVEL: quoted("-Owholemodule"),
150 | },
151 | },
152 | ];
153 |
154 | const xCConfigurationList = proj.addXCConfigurationList(
155 | buildConfigurationsList,
156 | "Release",
157 | `Build configuration list for PBXNativeTarget ${quoted(extensionName)}`
158 | );
159 |
160 | // update other build properties
161 | proj.updateBuildProperty(
162 | "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES",
163 | "YES",
164 | null,
165 | proj.getFirstTarget().firstTarget.name
166 | );
167 |
168 | proj.updateBuildProperty("IPHONEOS_DEPLOYMENT_TARGET", "14.0");
169 |
170 | return xCConfigurationList;
171 | };
172 |
173 | const addProductFile = (
174 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
175 | extensionName: string,
176 | groupName: string
177 | ) => {
178 | const productFile = {
179 | basename: `${extensionName}.appex`,
180 | fileRef: proj.generateUuid(),
181 | uuid: proj.generateUuid(),
182 | group: groupName,
183 | explicitFileType: "wrapper.app-extension",
184 | settings: {
185 | ATTRIBUTES: ["RemoveHeadersOnCopy"],
186 | },
187 | includeInIndex: 0,
188 | path: `${extensionName}.appex`,
189 | sourceTree: "BUILT_PRODUCTS_DIR",
190 | };
191 |
192 | proj.addToPbxFileReferenceSection(productFile);
193 |
194 | proj.addToPbxBuildFileSection(productFile);
195 |
196 | return productFile;
197 | };
198 |
199 | const addToPbxNativeTargetSection = (
200 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
201 | {
202 | extensionName,
203 | targetUuid,
204 | productFile,
205 | xCConfigurationList,
206 | }: {
207 | extensionName: string;
208 | targetUuid: string;
209 | productFile: any;
210 | xCConfigurationList: any;
211 | }
212 | ) => {
213 | const target = {
214 | uuid: targetUuid,
215 | pbxNativeTarget: {
216 | isa: "PBXNativeTarget",
217 | buildConfigurationList: xCConfigurationList.uuid,
218 | buildPhases: [],
219 | buildRules: [],
220 | dependencies: [],
221 | name: extensionName,
222 | productName: extensionName,
223 | productReference: productFile.fileRef,
224 | productType: quoted("com.apple.product-type.app-extension"),
225 | },
226 | };
227 |
228 | proj.addToPbxNativeTargetSection(target);
229 |
230 | return target;
231 | };
232 |
233 | const addToPbxProjectSection = (
234 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
235 | target: any
236 | ) => {
237 | proj.addToPbxProjectSection(target);
238 |
239 | // Add target attributes to project section
240 | if (
241 | !proj.pbxProjectSection()[proj.getFirstProject().uuid].attributes
242 | .TargetAttributes
243 | ) {
244 | proj.pbxProjectSection()[
245 | proj.getFirstProject().uuid
246 | ].attributes.TargetAttributes = {};
247 | }
248 |
249 | proj.pbxProjectSection()[
250 | proj.getFirstProject().uuid
251 | ].attributes.LastSwiftUpdateCheck = 1340;
252 |
253 | proj.pbxProjectSection()[
254 | proj.getFirstProject().uuid
255 | ].attributes.TargetAttributes[target.uuid] = {
256 | CreatedOnToolsVersion: "13.4.1",
257 | ProvisioningStyle: "Automatic",
258 | };
259 | };
260 |
261 | const addTargetDependency = (
262 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
263 | target: any
264 | ) => {
265 | if (!proj.hash.project.objects["PBXTargetDependency"]) {
266 | proj.hash.project.objects["PBXTargetDependency"] = {};
267 | }
268 | if (!proj.hash.project.objects["PBXContainerItemProxy"]) {
269 | proj.hash.project.objects["PBXContainerItemProxy"] = {};
270 | }
271 |
272 | proj.addTargetDependency(proj.getFirstTarget().uuid, [target.uuid]);
273 | };
274 |
275 | type AddBuildPhaseParams = {
276 | groupName: string;
277 | productFile: any;
278 | targetUuid: string;
279 | extensionName: string;
280 | frameworkPaths: string[];
281 | };
282 |
283 | const addBuildPhases = (
284 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
285 | {
286 | productFile,
287 | targetUuid,
288 | frameworkPaths,
289 | extensionName,
290 | }: AddBuildPhaseParams
291 | ) => {
292 | const buildPath = quoted("");
293 |
294 | // Sources build phase
295 | proj.addBuildPhase(
296 | [`widget.swift`],
297 | "PBXSourcesBuildPhase",
298 | "Sources",
299 | targetUuid,
300 | extensionName,
301 | buildPath
302 | );
303 |
304 | // Copy files build phase
305 | proj.addBuildPhase(
306 | [productFile.path],
307 | "PBXCopyFilesBuildPhase",
308 | "Copy Files",
309 | proj.getFirstTarget().uuid,
310 | "app_extension",
311 | buildPath
312 | );
313 |
314 | // Frameworks build phase
315 | proj.addBuildPhase(
316 | frameworkPaths,
317 | "PBXFrameworksBuildPhase",
318 | "Frameworks",
319 | targetUuid,
320 | extensionName,
321 | buildPath
322 | );
323 |
324 | // Resources build phase
325 | proj.addBuildPhase(
326 | ["Assets.xcassets"],
327 | "PBXResourcesBuildPhase",
328 | "Resources",
329 | targetUuid,
330 | extensionName,
331 | buildPath
332 | );
333 | };
334 |
335 | const addPbxGroup = (
336 | proj: IOSConfig.XcodeUtils.NativeTargetSection,
337 | productFile: any,
338 | extensionName: string,
339 | topLevelFiles: string[]
340 | ) => {
341 | // Add PBX group
342 | const { uuid: pbxGroupUuid } = proj.addPbxGroup(
343 | topLevelFiles,
344 | extensionName,
345 | extensionName
346 | );
347 |
348 | // Add PBXGroup to top level group
349 | const groups = proj.hash.project.objects["PBXGroup"];
350 | if (pbxGroupUuid) {
351 | Object.keys(groups).forEach(function (key) {
352 | if (groups[key].name === undefined && groups[key].path === undefined) {
353 | proj.addToPbxGroup(pbxGroupUuid, key);
354 | } else if (groups[key].name === "Products") {
355 | proj.addToPbxGroup(productFile, key);
356 | }
357 | });
358 | }
359 | };
360 |
361 | export const withWidgetXcodeTarget: ConfigPlugin = (
362 | config,
363 | { devTeamId, targetName, topLevelFiles }
364 | ) => {
365 | return withXcodeProject(config, async (config) => {
366 | const appName = config.modRequest.projectName!;
367 | const extensionBundleIdentifier = `${config.ios!.bundleIdentifier!}.widget`;
368 | const currentProjectVersion = config.ios!.buildNumber || "1";
369 | const marketingVersion = config.version!;
370 |
371 | await addBroadcastExtensionXcodeTarget(config.modResults, {
372 | appName,
373 | extensionName: targetName,
374 | extensionBundleIdentifier,
375 | currentProjectVersion,
376 | marketingVersion,
377 | devTeamId,
378 | topLevelFiles,
379 | });
380 |
381 | return config;
382 | });
383 | };
384 |
--------------------------------------------------------------------------------