├── .eslintrc.js
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── npm-publish.yml
├── .gitignore
├── .npmignore
├── README.md
├── app.plugin.js
├── bun.lockb
├── example
├── .gitignore
├── App.tsx
├── SampleWidgetExtension
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Knights.imageset
│ │ │ ├── Contents.json
│ │ │ ├── Knight.png
│ │ │ ├── Knight@2x.png
│ │ │ └── Knight@3x.png
│ │ ├── Pirates.imageset
│ │ │ ├── Contents.json
│ │ │ ├── Pirates.png
│ │ │ ├── Pirates@2x.png
│ │ │ └── Pirates@3x.png
│ │ ├── WidgetBackground.colorset
│ │ │ └── Contents.json
│ │ └── league.imageset
│ │ │ ├── Asset.png
│ │ │ ├── Asset@2x.png
│ │ │ ├── Asset@3x.png
│ │ │ └── Contents.json
│ ├── Attributes.swift
│ ├── Info.plist
│ ├── Module.swift
│ ├── SampleWidgetExtension.intentdefinition
│ ├── SampleWidgetExtension.swift
│ ├── SampleWidgetExtensionBundle.swift
│ └── SportsScoreLiveActivity.swift
├── app.json
├── assets
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── bun.lockb
├── eas.json
├── metro.config.js
├── package.json
├── tsconfig.json
└── webpack.config.js
├── expo-module.config.json
├── ios
├── .gitignore
└── ReactNativeWidgetExtension.podspec
├── package.json
├── plugin
├── src
│ ├── index.ts
│ ├── lib
│ │ ├── getWidgetExtensionEntitlements.ts
│ │ └── getWidgetFiles.ts
│ ├── withConfig.ts
│ ├── withPodfile.ts
│ ├── withWidgetExtensionEntitlements.ts
│ ├── withXcode.ts
│ └── xcode
│ │ ├── addBuildPhases.ts
│ │ ├── addPbxGroup.ts
│ │ ├── addProductFile.ts
│ │ ├── addTargetDependency.ts
│ │ ├── addToPbxNativeTargetSection.ts
│ │ ├── addToPbxProjectSection.ts
│ │ └── addXCConfigurationList.ts
└── tsconfig.json
├── src
├── ReactNativeWidgetExtensionModule.ts
└── index.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ["universe/native", "universe/web"],
4 | ignorePatterns: ["build"],
5 | };
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [bndkt]
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | schedule:
5 | interval: "weekly"
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | jobs:
9 | CI:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v3
14 | - name: Setup Bun
15 | uses: oven-sh/setup-bun@v1
16 | - name: Install dependencies
17 | run: bun install
18 | - name: Build react-native-widget-extension
19 | run: bun run build
20 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | publish-npm:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v3
16 | - name: Setup Bun
17 | uses: oven-sh/setup-bun@v1
18 | - name: Install dependencies
19 | run: bun install
20 | - name: Build package
21 | run: bun run build
22 | - uses: actions/setup-node@v3
23 | with:
24 | node-version: "18.x"
25 | registry-url: "https://registry.npmjs.org"
26 | - run: npm publish
27 | env:
28 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
29 |
--------------------------------------------------------------------------------
/.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 |
40 | # Cocoapods
41 | #
42 | example/ios/Pods
43 |
44 | # Ruby
45 | example/vendor/
46 |
47 | # node.js
48 | #
49 | node_modules/
50 | npm-debug.log
51 | yarn-debug.log
52 | yarn-error.log
53 |
54 | # BUCK
55 | buck-out/
56 | \.buckd/
57 | android/app/libs
58 | android/keystores/debug.keystore
59 |
60 | # Expo
61 | .expo/*
62 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-widget-extension
2 |
3 | Expo config plugin to add widgets and live activities to a React Native app
4 |
5 | The widgets still need to be written in Swift (I think there's no way around that). But you can simply add a folder with Swift files to a React Native project and then add the plugin via:
6 |
7 | ```sh
8 | npx expo install react-native-widget-extension
9 | ```
10 |
11 | And add the following config to app.json (where `widgetsFolder` is the path to the folder with the Swift files):
12 |
13 | ```json
14 | "expo": {
15 | "name": "my-app",
16 | "plugins": [
17 | [
18 | "react-native-widget-extension",
19 | { "frequentUpdates": true, "widgetsFolder": "SampleWidgetExtension" },
20 | ],
21 | ]
22 | }
23 | ```
24 |
25 | Then in React land, you can use the following:
26 |
27 | ```typescript
28 | import {
29 | areActivitiesEnabled,
30 | startActivity,
31 | updateActivity,
32 | endActivity,
33 | } from "react-native-widget-extension";
34 |
35 | startActivity(3, "4343", "$32.23", driverName, 47, 43);
36 | ```
37 |
38 | ## Plugin configuration options
39 |
40 | - `frequentUpdates` (boolean, default: false): Depending on this param, NSSupportsLiveActivitiesFrequentUpdates will be set
41 | - `widgetsFolder` (string, default: "widgets"): Path from the project root to the folder containing the Swift widget files
42 | - `deploymentTarget` (string, default: "16.2"): The minimum deployment target for the app
43 | - `groupIdentifier` (string): The app group identifier which is required for communication with the main app. Must start with `group.`
44 |
48 |
49 | ## Example
50 |
51 | For a minimal example app, see the folder **example**. Example code for a widget and live activity can be found in **example/SampleWidgetExtension**. To run the example, you'll need to `bun run build` in the root directory.
52 |
53 | Some background on how the **PizzaDelivery** example works:
54 |
55 | - Assets.xcassets and Info.plist: Automatically created by Xcode when you create a widget
56 | - Attributes.swift: The ActivityAttributes for the Live Activity are defined here. By default, this file should be named "Attributes.swift".
57 | - Module.swift: This file defined the native module that can then be used from React land to start/stop/update the live activity. By default, this file should be named "Module.swift".
58 | - The rest of the folder can be Swift files that define widgets, views, etc. and can be named and divided between files however you want.
59 |
60 | ## Deployment Target
61 |
62 | By default, this module adds a minimum deployment target of iOS 16.2, because otherwise Swift compilation fails if you try to use Live Activities. If you want to support earliert versions of iOS, you can manually set the deployment target via plugin config:
63 |
64 | ```json
65 | "expo": {
66 | "name": "my-app",
67 | "plugins": [
68 | [
69 | "react-native-widget-extension",
70 | { "deploymentTarget": "14.0" },
71 | ],
72 | ]
73 | }
74 | ```
75 |
76 | If you do this and you still use Live Activities in Swift, you have to make sure to guard the code that can only run on iOS 16.2 and later like this:
77 |
78 | ```swift
79 | import SwiftUI
80 | import WidgetKit
81 |
82 | @main
83 | struct PizzaDeliveryWidgetBundle: WidgetBundle {
84 | var body: some Widget {
85 | PizzaDeliveryWidgets()
86 |
87 | if #available(iOS 16.2, *) {
88 | PizzaDeliveryLiveActivity()
89 | }
90 | }
91 | }
92 | ```
93 |
94 | and
95 |
96 | ```swift
97 | @available(iOS 16.2, *)
98 | struct LockScreenLiveActivityView: View {
99 | ...
100 | }
101 | ```
102 |
--------------------------------------------------------------------------------
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./plugin/build");
2 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/bun.lockb
--------------------------------------------------------------------------------
/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 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | ios/
38 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import { Button, StyleSheet, View } from "react-native";
2 | import * as ReactNativeWidgetExtension from "react-native-widget-extension";
3 |
4 | export default function App() {
5 | const quarter = 1;
6 | const scoreLeft = 2;
7 | const scoreRight = 3;
8 | const bottomText = "Hello, world!";
9 |
10 | return (
11 |
12 | {ReactNativeWidgetExtension.areActivitiesEnabled() ? (
13 | <>
14 |
50 | );
51 | }
52 |
53 | const styles = StyleSheet.create({
54 | container: {
55 | flex: 1,
56 | backgroundColor: "#fff",
57 | alignItems: "center",
58 | justifyContent: "center",
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/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 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Knight.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Knight@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "Knight@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight@2x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Knights.imageset/Knight@3x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Pirates.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Pirates@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "Pirates@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates@2x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/Pirates.imageset/Pirates@3x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/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 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset@2x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Asset@3x.png
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Assets.xcassets/league.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Asset.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "Asset@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "Asset@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Attributes.swift:
--------------------------------------------------------------------------------
1 | import ActivityKit
2 | import WidgetKit
3 | import SwiftUI
4 |
5 | struct SportsLiveActivityAttributes: ActivityAttributes {
6 | public struct ContentState: Codable, Hashable {
7 | var quarter: Int
8 | var scoreLeft: Int
9 | var scoreRight: Int
10 | var bottomText: String
11 | }
12 |
13 | var timer: ClosedRange
14 | var imageLeft: String // Knight
15 | var teamNameLeft: String // Kinghts
16 | var imageRight: String // Pirates
17 | var teamNameRight: String // Pirates
18 | var gameName: String // "Western Conference Round 1"
19 | }
20 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/Module.swift:
--------------------------------------------------------------------------------
1 | import ExpoModulesCore
2 | import ActivityKit
3 |
4 | internal class MissingCurrentWindowSceneException: Exception {
5 | override var reason: String {
6 | "Cannot determine the current window scene in which to present the modal for requesting a review."
7 | }
8 | }
9 |
10 | public class ReactNativeWidgetExtensionModule: Module {
11 | public func definition() -> ModuleDefinition {
12 | Name("ReactNativeWidgetExtension")
13 |
14 | Function("areActivitiesEnabled") { () -> Bool in
15 | let logger = Logger()
16 | logger.info("areActivitiesEnabled()")
17 |
18 | if #available(iOS 16.2, *) {
19 | return ActivityAuthorizationInfo().areActivitiesEnabled
20 | } else {
21 | return false
22 | }
23 | }
24 |
25 | Function("startActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in
26 | let logger = Logger()
27 | logger.info("startActivity()")
28 |
29 | if #available(iOS 16.2, *) {
30 | let future = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
31 | let attributes = SportsLiveActivityAttributes(timer: Date.now...future, imageLeft: "Knights", teamNameLeft: "Knights", imageRight: "Pirates", teamNameRight: "Pirates", gameName: "Western Conference Round 1")
32 | let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText)
33 |
34 | let activityContent = ActivityContent(state: contentState, staleDate: Calendar.current.date(byAdding: .minute, value: 30, to: Date())!)
35 |
36 | do {
37 | let activity = try Activity.request(attributes: attributes, content: activityContent)
38 | logger.info("Requested a Live Activity \(String(describing: activity.id)).")
39 | } catch (let error) {
40 | logger.info("Error requesting Live Activity \(error.localizedDescription).")
41 | }
42 | }
43 | }
44 |
45 | Function("updateActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in
46 | let logger = Logger()
47 | logger.info("updateActivity()")
48 |
49 | if #available(iOS 16.2, *) {
50 | let future = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
51 | let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText)
52 | let alertConfiguration = AlertConfiguration(title: "Score update", body: "This is the alert text", sound: .default)
53 | let updatedContent = ActivityContent(state: contentState, staleDate: nil)
54 |
55 | Task {
56 | for activity in Activity.activities {
57 | await activity.update(updatedContent, alertConfiguration: alertConfiguration)
58 | logger.info("Updated the Live Activity: \(activity.id)")
59 | }
60 | }
61 | }
62 | }
63 |
64 | Function("endActivity") { (quarter: Int, scoreLeft: Int, scoreRight: Int, bottomText: String) -> Void in
65 | let logger = Logger()
66 | logger.info("endActivity()")
67 |
68 | if #available(iOS 16.2, *) {
69 | let contentState = SportsLiveActivityAttributes.ContentState(quarter: quarter, scoreLeft: scoreLeft, scoreRight: scoreRight, bottomText: bottomText)
70 | let finalContent = ActivityContent(state: contentState, staleDate: nil)
71 |
72 | Task {
73 | for activity in Activity.activities {
74 | await activity.end(finalContent, dismissalPolicy: .default)
75 | logger.info("Ending the Live Activity: \(activity.id)")
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/SampleWidgetExtension.intentdefinition:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | INEnums
6 |
7 | INIntentDefinitionModelVersion
8 | 1.2
9 | INIntentDefinitionNamespace
10 | 88xZPY
11 | INIntentDefinitionSystemVersion
12 | 20A294
13 | INIntentDefinitionToolsBuildVersion
14 | 12A6144
15 | INIntentDefinitionToolsVersion
16 | 12.0
17 | INIntents
18 |
19 |
20 | INIntentCategory
21 | information
22 | INIntentDescriptionID
23 | tVvJ9c
24 | INIntentEligibleForWidgets
25 |
26 | INIntentIneligibleForSuggestions
27 |
28 | INIntentName
29 | Configuration
30 | INIntentResponse
31 |
32 | INIntentResponseCodes
33 |
34 |
35 | INIntentResponseCodeName
36 | success
37 | INIntentResponseCodeSuccess
38 |
39 |
40 |
41 | INIntentResponseCodeName
42 | failure
43 |
44 |
45 |
46 | INIntentTitle
47 | Configuration
48 | INIntentTitleID
49 | gpCwrM
50 | INIntentType
51 | Custom
52 | INIntentVerb
53 | View
54 |
55 |
56 | INTypes
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/SampleWidgetExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleWidgetExtension.swift
3 | // SampleWidgetExtension
4 | //
5 | // Created by William Shepherd on 5/22/23.
6 | //
7 |
8 | import WidgetKit
9 | import SwiftUI
10 | import Intents
11 |
12 | struct Provider: IntentTimelineProvider {
13 | func placeholder(in context: Context) -> SimpleEntry {
14 | SimpleEntry(date: Date(), configuration: ConfigurationIntent())
15 | }
16 |
17 | func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
18 | let entry = SimpleEntry(date: Date(), configuration: configuration)
19 | completion(entry)
20 | }
21 |
22 | func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
23 | var entries: [SimpleEntry] = []
24 |
25 | // Generate a timeline consisting of five entries an hour apart, starting from the current date.
26 | let currentDate = Date()
27 | for hourOffset in 0 ..< 5 {
28 | let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
29 | let entry = SimpleEntry(date: entryDate, configuration: configuration)
30 | entries.append(entry)
31 | }
32 |
33 | let timeline = Timeline(entries: entries, policy: .atEnd)
34 | completion(timeline)
35 | }
36 | }
37 |
38 | struct SimpleEntry: TimelineEntry {
39 | let date: Date
40 | let configuration: ConfigurationIntent
41 | }
42 |
43 | struct SampleWidgetExtensionEntryView : View {
44 | var entry: Provider.Entry
45 |
46 | var body: some View {
47 | Text(entry.date, style: .time)
48 | }
49 | }
50 |
51 | struct SampleWidgetExtension: Widget {
52 | let kind: String = "SampleWidgetExtension"
53 |
54 | var body: some WidgetConfiguration {
55 | IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
56 | SampleWidgetExtensionEntryView(entry: entry)
57 | }
58 | .configurationDisplayName("My Widget")
59 | .description("This is an example widget.")
60 | }
61 | }
62 |
63 | struct SampleWidgetExtension_Previews: PreviewProvider {
64 | static var previews: some View {
65 | SampleWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
66 | .previewContext(WidgetPreviewContext(family: .systemSmall))
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/SampleWidgetExtensionBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleWidgetExtensionBundle.swift
3 | // SampleWidgetExtension
4 | //
5 | // Created by William Shepherd on 5/22/23.
6 | //
7 |
8 | import WidgetKit
9 | import SwiftUI
10 |
11 | @main
12 | struct SampleWidgetExtensionBundle: WidgetBundle {
13 | var body: some Widget {
14 | SampleWidgetExtension()
15 | SportsLiveActivity()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/SampleWidgetExtension/SportsScoreLiveActivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OneSignalWidgetLiveActivity.swift
3 | // OneSignalWidget
4 | //
5 | // Created by Henry Boswell on 12/5/22.
6 | //
7 |
8 | import ActivityKit
9 | import WidgetKit
10 | import SwiftUI
11 |
12 | struct SportsLiveActivity: Widget {
13 | var body: some WidgetConfiguration {
14 | ActivityConfiguration(for: SportsLiveActivityAttributes.self) { context in
15 | VStack(alignment: .center) {
16 | HStack {
17 | HStack {
18 | VStack(alignment: .center) {
19 | Image(context.attributes.imageLeft)
20 | .resizable()
21 | .aspectRatio(contentMode: .fill)
22 | .frame(width: 35, height: 35)
23 | Text(context.attributes.teamNameLeft).font(.system(size: 12, weight: .regular)).foregroundColor(.black)
24 | }
25 | //100
26 | Text(String(context.state.scoreLeft)).font(.system(size: 24, weight: .regular)).padding(.leading, 24).foregroundColor(.black)
27 | }.padding(.leading, 10)
28 | Spacer()
29 | VStack(alignment: .center) {
30 | HStack {
31 | Circle()
32 | .fill(.red)
33 | .frame(width: 7, height: 7)
34 | Text("Q" + String(context.state.quarter)).font(.system(size: 14, weight: .bold))
35 | }
36 | Text(timerInterval: context.attributes.timer, countsDown: true)
37 | .multilineTextAlignment(.center)
38 | .frame(width: 40)
39 | .font(.caption2).foregroundColor(.black)
40 | }
41 | Spacer()
42 | HStack {
43 | Text(String(context.state.scoreRight)).font(.system(size: 24, weight: .regular)).padding(.trailing, 24).foregroundColor(.black)
44 | VStack(alignment: .center) {
45 | Image(context.attributes.imageRight)
46 | .resizable()
47 | .aspectRatio(contentMode: .fill)
48 | .frame(width: 35, height: 35)
49 | Text(context.attributes.teamNameRight).font(.system(size: 12, weight: .regular)).foregroundColor(.black)
50 | }.padding(.trailing, 10)
51 | }
52 |
53 | }.padding(5)
54 | HStack {
55 | Image("league")
56 | .resizable()
57 | .aspectRatio(contentMode: .fill)
58 | .frame(width: 35, height: 35).padding(.leading, 10).padding(.top, 10).padding(.bottom, 10)
59 | VStack(alignment: .leading) {
60 | Text(context.attributes.gameName).font(.system(size: 14, weight: .bold)).foregroundColor(.black)
61 | Text(context.state.bottomText).font(.system(size: 12, weight: .regular)).foregroundColor(.black)
62 | }
63 | Spacer()
64 | Capsule().fill(.red)
65 | .frame(width: 2, height: 30).padding(.trailing, 10)
66 |
67 | }.padding(5).background(Color.init(red: 0.898, green: 0.91, blue: 0.922), in: Rectangle())
68 | }.padding(.top,15).activityBackgroundTint(.white).activitySystemActionForegroundColor(.black)
69 | } dynamicIsland: { context in
70 | DynamicIsland {
71 | // Expanded UI goes here. Compose the expanded UI through
72 | // various regions, like leading/trailing/center/bottom
73 | DynamicIslandExpandedRegion(.leading) {
74 | Text("Leading")
75 | }
76 | DynamicIslandExpandedRegion(.trailing) {
77 | Text("Trailing")
78 | }
79 | DynamicIslandExpandedRegion(.bottom) {
80 | Text("Bottom")
81 | // more content
82 | }
83 | } compactLeading: {
84 | Text("L")
85 | } compactTrailing: {
86 | Text("T")
87 | } minimal: {
88 | Text("Min")
89 | }
90 | .widgetURL(URL(string: "http://www.apple.com"))
91 | .keylineTint(Color.red)
92 | }
93 | }
94 | }
95 |
96 | struct SportsLiveActivityPreviews: PreviewProvider {
97 | static let future = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
98 | static let attributes = SportsLiveActivityAttributes(timer: Date.now...future, imageLeft: "Knights", teamNameLeft: "Knights", imageRight: "Pirates", teamNameRight: "Pirates", gameName: "Western Conference Round 1")
99 | static let contentState = SportsLiveActivityAttributes.ContentState(quarter: 1, scoreLeft: 0, scoreRight: 0, bottomText: "The game has started!")
100 |
101 | static var previews: some View {
102 | attributes
103 | .previewContext(contentState, viewKind: .dynamicIsland(.compact))
104 | .previewDisplayName("Island Compact")
105 | attributes
106 | .previewContext(contentState, viewKind: .dynamicIsland(.expanded))
107 | .previewDisplayName("Island Expanded")
108 | attributes
109 | .previewContext(contentState, viewKind: .dynamicIsland(.minimal))
110 | .previewDisplayName("Minimal")
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-widget-extension-example",
4 | "slug": "react-native-widget-extension-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.widgetextension.example"
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | },
24 | "package": "expo.modules.widgetextension.example"
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | },
29 | "plugins": [
30 | [
31 | "../app.plugin.js",
32 | { "frequentUpdates": true, "widgetsFolder": "SampleWidgetExtension" }
33 | ]
34 | ],
35 | "extra": {
36 | "eas": {
37 | "projectId": "0c1b21bc-7a01-4cc7-bd3b-25ea351f95dc"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/assets/splash.png
--------------------------------------------------------------------------------
/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-extension": path.join(
14 | __dirname,
15 | "..",
16 | "src",
17 | "index.ts"
18 | ),
19 | },
20 | },
21 | ],
22 | ],
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/example/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bndkt/react-native-widget-extension/f6a2437f859579ade4ba20499f580e00353a021f/example/bun.lockb
--------------------------------------------------------------------------------
/example/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 4.1.2",
4 | "promptToConfigurePushNotifications": false
5 | },
6 | "build": {
7 | "development": {
8 | "developmentClient": true,
9 | "distribution": "internal"
10 | },
11 | "simulator": {
12 | "extends": "development",
13 | "ios": {
14 | "simulator": true
15 | }
16 | },
17 | "preview": {
18 | "distribution": "internal",
19 | "channel": "preview"
20 | },
21 | "production": {
22 | "channel": "production"
23 | }
24 | },
25 | "submit": {
26 | "production": {}
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/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;
30 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-widget-extension-example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "doc": "bun x expo-doctor@latest",
7 | "start": "expo start",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "web": "expo start --web",
11 | "prebuild": "expo prebuild --platform ios --clean",
12 | "build:simulator": "eas build --profile simulator --platform ios --local",
13 | "build:development": "eas build --profile development --platform ios --local"
14 | },
15 | "dependencies": {
16 | "expo": "~49.0.10",
17 | "react": "18.2.0",
18 | "react-native": "0.72.4",
19 | "expo-dev-client": "~2.4.8",
20 | "expo-splash-screen": "~0.20.5",
21 | "expo-status-bar": "~1.6.0"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.20.0",
25 | "@types/react": "~18.2.14",
26 | "typescript": "^5.1.3"
27 | },
28 | "private": true,
29 | "expo": {
30 | "autolinking": {
31 | "nativeModulesDir": ".."
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "react-native-widget-extension": ["../src/index"],
7 | "react-native-widget-extension/*": ["../src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/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-extension"],
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 |
--------------------------------------------------------------------------------
/expo-module.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platforms": ["ios"],
3 | "ios": {
4 | "modules": ["ReactNativeWidgetExtensionModule"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | Attributes.swift
2 | Module.swift
3 |
--------------------------------------------------------------------------------
/ios/ReactNativeWidgetExtension.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 = 'ReactNativeWidgetExtension'
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/bndkt/react-native-widget-extension' }
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-widget-extension",
3 | "version": "0.0.6",
4 | "description": "Expo config plugin to add widgets and live activities to a React Native app",
5 | "main": "build/index.js",
6 | "types": "build/index.d.ts",
7 | "files": [
8 | "ios",
9 | "plugin/build",
10 | "build",
11 | "README.md",
12 | "expo-module.config.json",
13 | "app.plugin.js"
14 | ],
15 | "scripts": {
16 | "ncu": "bun x npm-check-updates -p bun",
17 | "build": "bun x tsc --build ./ ./plugin",
18 | "dev": "bun x tsc --build ./ ./plugin --watch"
19 | },
20 | "keywords": [
21 | "react-native",
22 | "expo",
23 | "react-native-widget-extension",
24 | "ReactNativeWidgetExtension"
25 | ],
26 | "repository": "https://github.com/bndkt/react-native-widget-extension",
27 | "bugs": {
28 | "url": "https://github.com/bndkt/react-native-widget-extension/issues"
29 | },
30 | "author": "Benedikt Müller <91166910+bndkt@users.noreply.github.com> (https://github.com/bndkt)",
31 | "license": "MIT",
32 | "homepage": "https://github.com/bndkt/react-native-widget-extension#readme",
33 | "dependencies": {
34 | "@expo/config-plugins": "^7.2.5"
35 | },
36 | "devDependencies": {
37 | "@types/node": "20.6.0",
38 | "@types/react": "~18.2.21",
39 | "@types/react-native": "0.72.2",
40 | "expo-module-scripts": "^3.1.0",
41 | "expo-modules-core": "^1.5.11",
42 | "prettier": "^3.0.3",
43 | "react": "18.2.0",
44 | "react-native": "0.72.4",
45 | "typescript": "^5.2.2"
46 | },
47 | "peerDependencies": {
48 | "expo": "*",
49 | "react": "*",
50 | "react-native": "*"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, IOSConfig, withPlugins } from "@expo/config-plugins";
2 | import { withConfig } from "./withConfig";
3 | import { withPodfile } from "./withPodfile";
4 |
5 | import { withXcode } from "./withXcode";
6 | import { withWidgetExtensionEntitlements } from "./withWidgetExtensionEntitlements";
7 |
8 | const withWidgetsAndLiveActivities: ConfigPlugin<{
9 | frequentUpdates?: boolean;
10 | widgetsFolder?: string;
11 | deploymentTarget?: string;
12 | moduleFileName?: string;
13 | attributesFileName?: string;
14 | groupIdentifier?: string;
15 | }> = (
16 | config,
17 | {
18 | frequentUpdates = false,
19 | widgetsFolder = "widgets",
20 | deploymentTarget = "16.2",
21 | moduleFileName = "Module.swift",
22 | attributesFileName = "Attributes.swift",
23 | groupIdentifier,
24 | }
25 | ) => {
26 | const targetName = `${IOSConfig.XcodeUtils.sanitizedName(
27 | config.name
28 | )}Widgets`;
29 | const bundleIdentifier = `${config.ios?.bundleIdentifier}.${targetName}`;
30 |
31 | config.ios = {
32 | ...config.ios,
33 | infoPlist: {
34 | ...config.ios?.infoPlist,
35 | NSSupportsLiveActivities: true,
36 | NSSupportsLiveActivitiesFrequentUpdates: frequentUpdates,
37 | },
38 | };
39 |
40 | config = withPlugins(config, [
41 | [
42 | withXcode,
43 | {
44 | targetName,
45 | bundleIdentifier,
46 | deploymentTarget,
47 | widgetsFolder,
48 | moduleFileName,
49 | attributesFileName,
50 | },
51 | ],
52 | [withWidgetExtensionEntitlements, { targetName, groupIdentifier }],
53 | [withPodfile, { targetName }],
54 | [withConfig, { targetName, bundleIdentifier, groupIdentifier }],
55 | ]);
56 |
57 | return config;
58 | };
59 |
60 | export default withWidgetsAndLiveActivities;
61 |
--------------------------------------------------------------------------------
/plugin/src/lib/getWidgetExtensionEntitlements.ts:
--------------------------------------------------------------------------------
1 | import { ExportedConfig, InfoPlist } from "@expo/config-plugins";
2 |
3 | export function getWidgetExtensionEntitlements(
4 | iosConfig: ExportedConfig["ios"],
5 | {
6 | groupIdentifier,
7 | }: {
8 | groupIdentifier?: string;
9 | }
10 | ) {
11 | const entitlements: InfoPlist = {};
12 |
13 | addApplicationGroupsEntitlement(entitlements, groupIdentifier);
14 |
15 | return entitlements;
16 | }
17 |
18 | export function addApplicationGroupsEntitlement(entitlements: InfoPlist, groupIdentifier?: string) {
19 | if (groupIdentifier) {
20 | const existingApplicationGroups = (entitlements["com.apple.security.application-groups"] as string[]) ?? [];
21 |
22 | entitlements["com.apple.security.application-groups"] = [groupIdentifier, ...existingApplicationGroups];
23 | }
24 |
25 | return entitlements;
26 | }
27 |
--------------------------------------------------------------------------------
/plugin/src/lib/getWidgetFiles.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | export type WidgetFiles = {
5 | swiftFiles: string[];
6 | entitlementFiles: string[];
7 | plistFiles: string[];
8 | assetDirectories: string[];
9 | intentFiles: string[];
10 | otherFiles: string[];
11 | };
12 |
13 | export function getWidgetFiles(
14 | widgetsPath: string,
15 | targetPath: string,
16 | moduleFileName: string,
17 | attributesFileName: string
18 | ) {
19 | const widgetFiles: WidgetFiles = {
20 | swiftFiles: [],
21 | entitlementFiles: [],
22 | plistFiles: [],
23 | assetDirectories: [],
24 | intentFiles: [],
25 | otherFiles: [],
26 | };
27 |
28 | if (!fs.existsSync(targetPath)) {
29 | fs.mkdirSync(targetPath, { recursive: true });
30 | }
31 |
32 | if (fs.lstatSync(widgetsPath).isDirectory()) {
33 | const files = fs.readdirSync(widgetsPath);
34 |
35 | files.forEach((file) => {
36 | const fileExtension = file.split(".").pop();
37 |
38 | if (fileExtension === "swift") {
39 | if (file !== moduleFileName) {
40 | widgetFiles.swiftFiles.push(file);
41 | }
42 | } else if (fileExtension === "entitlements") {
43 | widgetFiles.entitlementFiles.push(file);
44 | } else if (fileExtension === "plist") {
45 | widgetFiles.plistFiles.push(file);
46 | } else if (fileExtension === "xcassets") {
47 | widgetFiles.assetDirectories.push(file);
48 | } else if (fileExtension === "intentdefinition") {
49 | widgetFiles.intentFiles.push(file);
50 | } else {
51 | widgetFiles.otherFiles.push(file);
52 | }
53 | });
54 | }
55 |
56 | // Copy files
57 | [
58 | ...widgetFiles.swiftFiles,
59 | ...widgetFiles.entitlementFiles,
60 | ...widgetFiles.plistFiles,
61 | ...widgetFiles.intentFiles,
62 | ...widgetFiles.otherFiles,
63 | ].forEach((file) => {
64 | const source = path.join(widgetsPath, file);
65 | copyFileSync(source, targetPath);
66 | });
67 |
68 | // Copy Module.swift and Attributes.swift
69 | const modulePath = path.join(__dirname, "../../../ios");
70 | copyFileSync(
71 | path.join(widgetsPath, moduleFileName),
72 | path.join(modulePath, "Module.swift")
73 | );
74 | copyFileSync(
75 | path.join(widgetsPath, attributesFileName),
76 | path.join(modulePath, "Attributes.swift")
77 | );
78 |
79 | // Copy directories
80 | widgetFiles.assetDirectories.forEach((directory) => {
81 | const imagesXcassetsSource = path.join(widgetsPath, directory);
82 | copyFolderRecursiveSync(imagesXcassetsSource, targetPath);
83 | });
84 |
85 | return widgetFiles;
86 | }
87 |
88 | export function copyFileSync(source: string, target: string) {
89 | let targetFile = target;
90 |
91 | if (fs.existsSync(target) && fs.lstatSync(target).isDirectory()) {
92 | targetFile = path.join(target, path.basename(source));
93 | }
94 |
95 | fs.writeFileSync(targetFile, fs.readFileSync(source));
96 | }
97 |
98 | function copyFolderRecursiveSync(source: string, target: string) {
99 | const targetPath = path.join(target, path.basename(source));
100 | if (!fs.existsSync(targetPath)) {
101 | fs.mkdirSync(targetPath, { recursive: true });
102 | }
103 |
104 | if (fs.lstatSync(source).isDirectory()) {
105 | const files = fs.readdirSync(source);
106 | files.forEach((file) => {
107 | const currentPath = path.join(source, file);
108 | if (fs.lstatSync(currentPath).isDirectory()) {
109 | copyFolderRecursiveSync(currentPath, targetPath);
110 | } else {
111 | copyFileSync(currentPath, targetPath);
112 | }
113 | });
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/plugin/src/withConfig.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin } from "@expo/config-plugins";
2 |
3 | import { addApplicationGroupsEntitlement, getWidgetExtensionEntitlements } from "./lib/getWidgetExtensionEntitlements";
4 |
5 | export const withConfig: ConfigPlugin<{
6 | bundleIdentifier: string;
7 | targetName: string;
8 | groupIdentifier?: string;
9 | }> = (config, { bundleIdentifier, targetName, groupIdentifier }) => {
10 | let configIndex: null | number = null;
11 | config.extra?.eas?.build?.experimental?.ios?.appExtensions?.forEach((ext: any, index: number) => {
12 | if (ext.targetName === targetName) {
13 | configIndex = index;
14 | }
15 | });
16 |
17 | if (!configIndex) {
18 | config.extra = {
19 | ...config.extra,
20 | eas: {
21 | ...config.extra?.eas,
22 | build: {
23 | ...config.extra?.eas?.build,
24 | experimental: {
25 | ...config.extra?.eas?.build?.experimental,
26 | ios: {
27 | ...config.extra?.eas?.build?.experimental?.ios,
28 | appExtensions: [
29 | ...(config.extra?.eas?.build?.experimental?.ios?.appExtensions ?? []),
30 | {
31 | targetName,
32 | bundleIdentifier,
33 | },
34 | ],
35 | },
36 | },
37 | },
38 | },
39 | };
40 | configIndex = 0;
41 | }
42 |
43 | if (configIndex != null && config.extra) {
44 | const widgetsExtensionConfig = config.extra.eas.build.experimental.ios.appExtensions[configIndex];
45 |
46 | widgetsExtensionConfig.entitlements = {
47 | ...widgetsExtensionConfig.entitlements,
48 | ...getWidgetExtensionEntitlements(config.ios, {
49 | groupIdentifier,
50 | }),
51 | };
52 |
53 | config.ios = {
54 | ...config.ios,
55 | entitlements: {
56 | ...addApplicationGroupsEntitlement(config.ios?.entitlements ?? {}, groupIdentifier),
57 | },
58 | };
59 | }
60 |
61 | return config;
62 | };
63 |
--------------------------------------------------------------------------------
/plugin/src/withPodfile.ts:
--------------------------------------------------------------------------------
1 | import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
2 | import { ConfigPlugin, withDangerousMod } from "@expo/config-plugins";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | export const withPodfile: ConfigPlugin<{ targetName: string }> = (
7 | config,
8 | { targetName }
9 | ) => {
10 | return withDangerousMod(config, [
11 | "ios",
12 | (config) => {
13 | const podFilePath = path.join(
14 | config.modRequest.platformProjectRoot,
15 | "Podfile"
16 | );
17 | let podFileContent = fs.readFileSync(podFilePath).toString();
18 |
19 | /* podFileContent = mergeContents({
20 | tag: "withWidgetExtensionPodfile1999999999",
21 | src: podFileContent,
22 | newSrc: ` target '${targetName}' do\n \n end`,
23 | anchor: /post_install/,
24 | offset: 0,
25 | comment: "#",
26 | }).contents; */
27 |
28 | /* podFileContent = podFileContent.replace(
29 | /use_expo_modules!/,
30 | `use_expo_modules!(searchPaths: ["./node_modules", "../../node_modules", "../../../WidgetExtension"])`
31 | ); */
32 |
33 | podFileContent = mergeContents({
34 | tag: "react-native-widget-extension-1",
35 | src: podFileContent,
36 | newSrc: `installer.pods_project.targets.each do |target|
37 | target.build_configurations.each do |config|
38 | # Sentry has build errors unless configured as 'YES' for the Sentry target: https://github.com/bndkt/react-native-widget-extension/issues/24
39 | config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = target.name == 'Sentry' ? 'YES' : 'No'
40 | end
41 | end`,
42 | anchor:
43 | /installer.target_installation_results.pod_target_installation_results/,
44 | offset: 0,
45 | comment: "#",
46 | }).contents;
47 |
48 | /* podFileContent = mergeContents({
49 | tag: "react-native-widget-extension-2",
50 | src: podFileContent,
51 | newSrc: `pod 'WidgetExtension', :path => '../WidgetExtension/ios'`,
52 | anchor: /use_react_native/,
53 | offset: -1,
54 | comment: "#",
55 | }).contents; */
56 |
57 | podFileContent = podFileContent
58 | .concat(`\n\n# >>> Inserted by react-native-widget-extension\n`)
59 | .concat(
60 | `target '${targetName}' do
61 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
62 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
63 | end`
64 | )
65 | .concat(`\n# >>> Inserted by react-native-widget-extension`);
66 |
67 | fs.writeFileSync(podFilePath, podFileContent);
68 |
69 | return config;
70 | },
71 | ]);
72 | };
73 |
--------------------------------------------------------------------------------
/plugin/src/withWidgetExtensionEntitlements.ts:
--------------------------------------------------------------------------------
1 | import plist from "@expo/plist";
2 | import { ConfigPlugin, withInfoPlist } from "@expo/config-plugins";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | import { getWidgetExtensionEntitlements } from "./lib/getWidgetExtensionEntitlements";
7 |
8 | export const withWidgetExtensionEntitlements: ConfigPlugin<{
9 | targetName: string;
10 | targetPath: string;
11 | groupIdentifier: string;
12 | appleSignin: boolean;
13 | }> = (config, { targetName, groupIdentifier }) => {
14 | return withInfoPlist(config, (config) => {
15 | const targetPath = path.join(config.modRequest.platformProjectRoot, targetName);
16 | const filePath = path.join(targetPath, `${targetName}.entitlements`);
17 |
18 | const appClipEntitlements = getWidgetExtensionEntitlements(config.ios, {
19 | groupIdentifier,
20 | });
21 |
22 | fs.mkdirSync(path.dirname(filePath), { recursive: true });
23 | fs.writeFileSync(filePath, plist.build(appClipEntitlements));
24 |
25 | return config;
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/plugin/src/withXcode.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withXcodeProject } from "@expo/config-plugins";
2 | import * as path from "path";
3 |
4 | import { addXCConfigurationList } from "./xcode/addXCConfigurationList";
5 | import { addProductFile } from "./xcode/addProductFile";
6 | import { addToPbxNativeTargetSection } from "./xcode/addToPbxNativeTargetSection";
7 | import { addToPbxProjectSection } from "./xcode/addToPbxProjectSection";
8 | import { addTargetDependency } from "./xcode/addTargetDependency";
9 | import { addPbxGroup } from "./xcode/addPbxGroup";
10 | import { addBuildPhases } from "./xcode/addBuildPhases";
11 | import { getWidgetFiles } from "./lib/getWidgetFiles";
12 |
13 | export const withXcode: ConfigPlugin<{
14 | targetName: string;
15 | bundleIdentifier: string;
16 | deploymentTarget: string;
17 | widgetsFolder: string;
18 | moduleFileName: string;
19 | attributesFileName: string;
20 | }> = (
21 | config,
22 | {
23 | targetName,
24 | bundleIdentifier,
25 | deploymentTarget,
26 | widgetsFolder,
27 | moduleFileName,
28 | attributesFileName,
29 | }
30 | ) => {
31 | return withXcodeProject(config, (config) => {
32 | const xcodeProject = config.modResults;
33 | const widgetsPath = path.join(config.modRequest.projectRoot, widgetsFolder);
34 |
35 | const targetUuid = xcodeProject.generateUuid();
36 | const groupName = "Embed Foundation Extensions";
37 | const { platformProjectRoot } = config.modRequest;
38 | const marketingVersion = config.version;
39 |
40 | const targetPath = path.join(platformProjectRoot, targetName);
41 |
42 | const widgetFiles = getWidgetFiles(
43 | widgetsPath,
44 | targetPath,
45 | moduleFileName,
46 | attributesFileName
47 | );
48 |
49 | const xCConfigurationList = addXCConfigurationList(xcodeProject, {
50 | targetName,
51 | currentProjectVersion: config.ios!.buildNumber || "1",
52 | bundleIdentifier,
53 | deploymentTarget,
54 | marketingVersion,
55 | });
56 |
57 | const productFile = addProductFile(xcodeProject, {
58 | targetName,
59 | groupName,
60 | });
61 |
62 | const target = addToPbxNativeTargetSection(xcodeProject, {
63 | targetName,
64 | targetUuid,
65 | productFile,
66 | xCConfigurationList,
67 | });
68 |
69 | addToPbxProjectSection(xcodeProject, target);
70 |
71 | addTargetDependency(xcodeProject, target);
72 |
73 | addBuildPhases(xcodeProject, {
74 | targetUuid,
75 | groupName,
76 | productFile,
77 | widgetFiles,
78 | });
79 |
80 | addPbxGroup(xcodeProject, {
81 | targetName,
82 | widgetFiles,
83 | });
84 |
85 | return config;
86 | });
87 | };
88 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addBuildPhases.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 | import * as util from "util";
3 |
4 | import { WidgetFiles } from "../lib/getWidgetFiles";
5 |
6 | export function addBuildPhases(
7 | xcodeProject: XcodeProject,
8 | {
9 | targetUuid,
10 | groupName,
11 | productFile,
12 | widgetFiles,
13 | }: {
14 | targetUuid: string;
15 | groupName: string;
16 | productFile: {
17 | uuid: string;
18 | target: string;
19 | basename: string;
20 | group: string;
21 | };
22 | widgetFiles: WidgetFiles;
23 | }
24 | ) {
25 | const buildPath = `""`;
26 | const folderType = "app_extension";
27 |
28 | const {
29 | swiftFiles,
30 | intentFiles,
31 | assetDirectories,
32 | entitlementFiles,
33 | plistFiles,
34 | } = widgetFiles;
35 |
36 | // Sources build phase
37 | xcodeProject.addBuildPhase(
38 | [...swiftFiles, ...intentFiles],
39 | "PBXSourcesBuildPhase",
40 | groupName,
41 | targetUuid,
42 | folderType,
43 | buildPath
44 | );
45 |
46 | // Copy files build phase
47 | xcodeProject.addBuildPhase(
48 | [],
49 | "PBXCopyFilesBuildPhase",
50 | groupName,
51 | xcodeProject.getFirstTarget().uuid,
52 | folderType,
53 | buildPath
54 | );
55 |
56 | xcodeProject
57 | .buildPhaseObject("PBXCopyFilesBuildPhase", groupName, productFile.target)
58 | .files.push({
59 | value: productFile.uuid,
60 | comment: util.format("%s in %s", productFile.basename, productFile.group), // longComment(file);
61 | });
62 | xcodeProject.addToPbxBuildFileSection(productFile);
63 |
64 | // Frameworks build phase
65 | xcodeProject.addBuildPhase(
66 | [],
67 | "PBXFrameworksBuildPhase",
68 | groupName,
69 | targetUuid,
70 | folderType,
71 | buildPath
72 | );
73 |
74 | // Resources build phase
75 | xcodeProject.addBuildPhase(
76 | [...assetDirectories],
77 | "PBXResourcesBuildPhase",
78 | groupName,
79 | targetUuid,
80 | folderType,
81 | buildPath
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addPbxGroup.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | import { WidgetFiles } from "../lib/getWidgetFiles";
4 |
5 | export function addPbxGroup(
6 | xcodeProject: XcodeProject,
7 | {
8 | targetName,
9 | widgetFiles,
10 | }: {
11 | targetName: string;
12 | widgetFiles: WidgetFiles;
13 | }
14 | ) {
15 | const {
16 | swiftFiles,
17 | intentFiles,
18 | otherFiles,
19 | assetDirectories,
20 | entitlementFiles,
21 | plistFiles,
22 | } = widgetFiles;
23 |
24 | // Add PBX group
25 | const { uuid: pbxGroupUuid } = xcodeProject.addPbxGroup(
26 | [
27 | ...swiftFiles,
28 | ...intentFiles,
29 | ...otherFiles,
30 | ...entitlementFiles,
31 | ...plistFiles,
32 | ...assetDirectories,
33 | `${targetName}.entitlements`,
34 | ],
35 | targetName,
36 | targetName
37 | );
38 |
39 | // Add PBXGroup to top level group
40 | const groups = xcodeProject.hash.project.objects["PBXGroup"];
41 | if (pbxGroupUuid) {
42 | Object.keys(groups).forEach(function (key) {
43 | if (groups[key].name === undefined && groups[key].path === undefined) {
44 | xcodeProject.addToPbxGroup(pbxGroupUuid, key);
45 | }
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addProductFile.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export function addProductFile(
4 | xcodeProject: XcodeProject,
5 | { targetName, groupName }: { targetName: string; groupName: string }
6 | ) {
7 | const options = {
8 | basename: `${targetName}.appex`,
9 | // fileRef: xcodeProject.generateUuid(),
10 | // uuid: xcodeProject.generateUuid(),
11 | group: groupName,
12 | explicitFileType: "wrapper.app-extension",
13 | /* fileEncoding: 4, */
14 | settings: {
15 | ATTRIBUTES: ["RemoveHeadersOnCopy"],
16 | },
17 | includeInIndex: 0,
18 | path: `${targetName}.appex`,
19 | sourceTree: "BUILT_PRODUCTS_DIR",
20 | };
21 |
22 | const productFile = xcodeProject.addProductFile(targetName, options);
23 |
24 | return productFile;
25 | }
26 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addTargetDependency.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export function addTargetDependency(
4 | xcodeProject: XcodeProject,
5 | target: { uuid: string }
6 | ) {
7 | if (!xcodeProject.hash.project.objects["PBXTargetDependency"]) {
8 | xcodeProject.hash.project.objects["PBXTargetDependency"] = {};
9 | }
10 | if (!xcodeProject.hash.project.objects["PBXContainerItemProxy"]) {
11 | xcodeProject.hash.project.objects["PBXContainerItemProxy"] = {};
12 | }
13 |
14 | xcodeProject.addTargetDependency(xcodeProject.getFirstTarget().uuid, [
15 | target.uuid,
16 | ]);
17 | }
18 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addToPbxNativeTargetSection.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export function addToPbxNativeTargetSection(
4 | xcodeProject: XcodeProject,
5 | {
6 | targetName,
7 | targetUuid,
8 | productFile,
9 | xCConfigurationList,
10 | }: {
11 | targetName: string;
12 | targetUuid: string;
13 | productFile: { fileRef: string };
14 | xCConfigurationList: { uuid: string };
15 | }
16 | ) {
17 | const target = {
18 | uuid: targetUuid,
19 | pbxNativeTarget: {
20 | isa: "PBXNativeTarget",
21 | name: targetName,
22 | productName: targetName,
23 | productReference: productFile.fileRef,
24 | productType: `"com.apple.product-type.app-extension"`,
25 | buildConfigurationList: xCConfigurationList.uuid,
26 | buildPhases: [],
27 | buildRules: [],
28 | dependencies: [],
29 | },
30 | };
31 |
32 | xcodeProject.addToPbxNativeTargetSection(target);
33 |
34 | const frameworksGroup = xcodeProject.findPBXGroupKey({ name: "Frameworks" });
35 | const file1 = xcodeProject.addFile("WidgetKit.framework", frameworksGroup);
36 | const file2 = xcodeProject.addFile("SwiftUI.framework", frameworksGroup);
37 | const frameworksBuildPhaseObj = xcodeProject.pbxFrameworksBuildPhaseObj(
38 | target.uuid
39 | );
40 | /* console.log(
41 | { file1, file2, frameworksBuildPhaseObj },
42 | frameworksBuildPhaseObj.files
43 | ); */
44 |
45 | return target;
46 | }
47 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addToPbxProjectSection.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export function addToPbxProjectSection(
4 | xcodeProject: XcodeProject,
5 | target: { uuid: string }
6 | ) {
7 | xcodeProject.addToPbxProjectSection(target);
8 |
9 | // Add target attributes to project section
10 | if (
11 | !xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid]
12 | .attributes.TargetAttributes
13 | ) {
14 | xcodeProject.pbxProjectSection()[
15 | xcodeProject.getFirstProject().uuid
16 | ].attributes.TargetAttributes = {};
17 | }
18 | xcodeProject.pbxProjectSection()[
19 | xcodeProject.getFirstProject().uuid
20 | ].attributes.TargetAttributes[target.uuid] = {
21 | LastSwiftMigration: 1250,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/plugin/src/xcode/addXCConfigurationList.ts:
--------------------------------------------------------------------------------
1 | import { XcodeProject } from "@expo/config-plugins";
2 |
3 | export function addXCConfigurationList(
4 | xcodeProject: XcodeProject,
5 | {
6 | targetName,
7 | currentProjectVersion,
8 | bundleIdentifier,
9 | deploymentTarget,
10 | marketingVersion,
11 | }: {
12 | targetName: string;
13 | currentProjectVersion: string;
14 | bundleIdentifier: string;
15 | deploymentTarget: string;
16 | marketingVersion?: string;
17 | }
18 | ) {
19 | const commonBuildSettings: any = {
20 | /* ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
21 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
22 | CLANG_ANALYZER_NONNULL = YES;
23 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
24 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
25 | CLANG_ENABLE_OBJC_WEAK = YES;
26 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
27 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
28 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
29 | CODE_SIGN_STYLE = Automatic;
30 | DEBUG_INFORMATION_FORMAT = dwarf;
31 | DEVELOPMENT_TEAM = G76836P2D4;
32 | GCC_C_LANGUAGE_STANDARD = gnu11;
33 |
34 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
35 | MARKETING_VERSION = 1.0;
36 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
37 | MTL_FAST_MATH = YES;
38 | SKIP_INSTALL = YES;
39 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
40 | SWIFT_EMIT_LOC_STRINGS = YES;
41 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; */
42 |
43 | PRODUCT_NAME: `"$(TARGET_NAME)"`,
44 | SWIFT_VERSION: "5.0",
45 | TARGETED_DEVICE_FAMILY: `"1,2"`,
46 | INFOPLIST_FILE: `${targetName}/Info.plist`,
47 | CURRENT_PROJECT_VERSION: `"${currentProjectVersion}"`,
48 | IPHONEOS_DEPLOYMENT_TARGET: `"${deploymentTarget}"`,
49 | PRODUCT_BUNDLE_IDENTIFIER: `"${bundleIdentifier}"`,
50 | GENERATE_INFOPLIST_FILE: `"YES"`,
51 | INFOPLIST_KEY_CFBundleDisplayName: targetName,
52 | INFOPLIST_KEY_NSHumanReadableCopyright: `""`,
53 | MARKETING_VERSION: `"${marketingVersion}"`,
54 | SWIFT_OPTIMIZATION_LEVEL: `"-Onone"`,
55 | CODE_SIGN_ENTITLEMENTS: `"${targetName}/${targetName}.entitlements"`,
56 | // DEVELOPMENT_TEAM: `"G76836P2D4"`,
57 | };
58 |
59 | const buildConfigurationsList = [
60 | {
61 | name: "Debug",
62 | isa: "XCBuildConfiguration",
63 | buildSettings: {
64 | ...commonBuildSettings,
65 | },
66 | },
67 | {
68 | name: "Release",
69 | isa: "XCBuildConfiguration",
70 | buildSettings: {
71 | ...commonBuildSettings,
72 | },
73 | },
74 | ];
75 |
76 | const xCConfigurationList = xcodeProject.addXCConfigurationList(
77 | buildConfigurationsList,
78 | "Release",
79 | `Build configuration list for PBXNativeTarget "${targetName}"`
80 | );
81 |
82 | return xCConfigurationList;
83 | }
84 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build",
4 | "lib": ["ES2020"],
5 | "module": "CommonJS",
6 | "target": "ES2020",
7 | "declaration": true,
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "moduleResolution": "node"
13 | },
14 | "include": ["src"],
15 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__stories__/*"]
16 | }
17 |
--------------------------------------------------------------------------------
/src/ReactNativeWidgetExtensionModule.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("ReactNativeWidgetExtension");
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import ReactNativeWidgetExtension from "./ReactNativeWidgetExtensionModule";
2 |
3 | export function areActivitiesEnabled(): boolean {
4 | return ReactNativeWidgetExtension.areActivitiesEnabled();
5 | }
6 |
7 | export function startActivity(...args: any): void {
8 | return ReactNativeWidgetExtension.startActivity(...args);
9 | }
10 |
11 | export function updateActivity(...args: any): void {
12 | return ReactNativeWidgetExtension.updateActivity(...args);
13 | }
14 |
15 | export function endActivity(...args: any): void {
16 | return ReactNativeWidgetExtension.endActivity(...args);
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.base",
3 | "compilerOptions": {
4 | "outDir": "./build"
5 | },
6 | "include": ["./src"],
7 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
8 | }
9 |
--------------------------------------------------------------------------------