diff --git a/packages/react-native-voip-push-notification/.eslintrc.js b/packages/react-native-voip-push-notification/.eslintrc.js new file mode 100644 index 00000000..27201978 --- /dev/null +++ b/packages/react-native-voip-push-notification/.eslintrc.js @@ -0,0 +1,2 @@ +// @generated by expo-module-scripts +module.exports = require('expo-module-scripts/eslintrc.base.js'); diff --git a/packages/react-native-voip-push-notification/CHANGELOG.md b/packages/react-native-voip-push-notification/CHANGELOG.md new file mode 100644 index 00000000..e6f91294 --- /dev/null +++ b/packages/react-native-voip-push-notification/CHANGELOG.md @@ -0,0 +1,3 @@ +### @config-plugins/react-native-voip-push-notification 1.0.0 + +- Expo SDK 53 support diff --git a/packages/react-native-voip-push-notification/README.md b/packages/react-native-voip-push-notification/README.md new file mode 100644 index 00000000..e0f55e63 --- /dev/null +++ b/packages/react-native-voip-push-notification/README.md @@ -0,0 +1,21 @@ +# config-plugins/react-native-voip-push-notification + +Config plugin to auto-configure `react-native-voip-push-notification` when the native code is generated (`npx expo prebuild`). + +## Versioning + +Ensure you use versions that work together! + +| `expo` | `react-native-voip-push-notification` | `@config-plugins/react-native-voip-push-notification` | +| ------ |---------------------------------------|-------------------------------------------------------| +| 53.0.0 | 3.3.3 | 1.0.0 | + +### Add the package to your npm dependencies + +> This package cannot be used in the "Expo Go" app because [it requires custom native code](https://docs.expo.io/workflow/customizing/). + +First install the package with yarn, npm, or [`npx expo install`](https://docs.expo.io/workflow/expo-cli/#expo-install). + +``` +npx expo install react-native-voip-push-notification @config-plugins/react-native-voip-push-notification +``` diff --git a/packages/react-native-voip-push-notification/app.plugin.js b/packages/react-native-voip-push-notification/app.plugin.js new file mode 100644 index 00000000..a9e9554e --- /dev/null +++ b/packages/react-native-voip-push-notification/app.plugin.js @@ -0,0 +1 @@ +module.exports = require("./build/withVoipPushNotification"); diff --git a/packages/react-native-voip-push-notification/jest.config.js b/packages/react-native-voip-push-notification/jest.config.js new file mode 100644 index 00000000..e539b3b1 --- /dev/null +++ b/packages/react-native-voip-push-notification/jest.config.js @@ -0,0 +1 @@ +module.exports = require('expo-module-scripts/jest-preset-plugin'); diff --git a/packages/react-native-voip-push-notification/package.json b/packages/react-native-voip-push-notification/package.json new file mode 100644 index 00000000..7b2d80d9 --- /dev/null +++ b/packages/react-native-voip-push-notification/package.json @@ -0,0 +1,35 @@ +{ + "name": "@config-plugins/react-native-voip-push-notification", + "version": "1.0.0", + "description": "Config plugin to auto configure react-native-voip-push-notification on prebuild", + "main": "build/withVoipPushNotification.js", + "types": "build/withVoipPushNotification.d.ts", + "sideEffects": false, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/expo/config-plugins.git", + "directory": "packages/react-native-voip-push-notification" + }, + "files": [ + "build", + "app.plugin.js" + ], + "scripts": { + "build": "expo-module build", + "clean": "expo-module clean", + "lint": "expo-module lint", + "test": "expo-module test", + "prepare": "expo-module prepare", + "prepublishOnly": "expo-module prepublishOnly", + "expo-module": "expo-module" + }, + "keywords": [ + "react-native", + "expo" + ], + "peerDependencies": { + "expo": "^53" + }, + "upstreamPackage": "react-native-voip-push-notification" +} diff --git a/packages/react-native-voip-push-notification/src/withVoipPushNotification.ts b/packages/react-native-voip-push-notification/src/withVoipPushNotification.ts new file mode 100644 index 00000000..da657d0a --- /dev/null +++ b/packages/react-native-voip-push-notification/src/withVoipPushNotification.ts @@ -0,0 +1,251 @@ +import { + type ConfigPlugin, + withAppDelegate, + withXcodeProject, + withInfoPlist, +} from "@expo/config-plugins"; +import { + addSwiftImports, + insertContentsInsideSwiftClassBlock, + insertContentsInsideSwiftFunctionBlock, + findSwiftFunctionCodeBlock, + addObjcImports, +} from "@expo/config-plugins/build/ios/codeMod"; +import fs from "fs"; +import path from "path"; + +const withVoipAppDelegate: ConfigPlugin = (configuration) => { + return withAppDelegate(configuration, (config) => { + if (config.modResults.language !== "swift") { + console.warn( + "react-native-voip-push-notification: Objective-C AppDelegate not supported, use Swift", + ); + + return config; + } + + try { + // Add PKPushRegistryDelegate to AppDelegate class + if (!config.modResults.contents.includes("PKPushRegistryDelegate")) { + config.modResults.contents = config.modResults.contents.replace( + /public class AppDelegate: ExpoAppDelegate/, + "public class AppDelegate: ExpoAppDelegate, PKPushRegistryDelegate", + ); + } + + addToSwiftBridgingHeaderFile( + config.modRequest.projectRoot, + config.modRequest.projectName, + (headerFileContents: string) => + addObjcImports(headerFileContents, [ + '"RNVoipPushNotificationManager.h"', + '"RNCallKeep.h"', + ]), + ); + + config.modResults.contents = addSwiftImports(config.modResults.contents, [ + "PushKit", + ]); + + config.modResults.contents = addDidFinishLaunchingWithOptionsRingingSwift( + config.modResults.contents, + ); + + config.modResults.contents = addDidUpdatePushCredentialsSwift( + config.modResults.contents, + ); + + config.modResults.contents = addDidReceiveIncomingPushCallbackSwift( + config.modResults.contents, + ); + + return config; + } catch (error) { + throw new Error( + `Cannot setup react-native-voip-push-notification: ${error}`, + ); + } + }); +}; + +function addDidFinishLaunchingWithOptionsRingingSwift(contents: string) { + const functionSelector = "application(_:didFinishLaunchingWithOptions:)"; + + // call the setup of voip push notification + const voipSetupMethod = "RNVoipPushNotificationManager.voipRegistration()"; + + if (!contents.includes(voipSetupMethod)) { + contents = insertContentsInsideSwiftFunctionBlock( + contents, + functionSelector, + " " /* indentation */ + voipSetupMethod, + { position: "head" }, + ); + } + + return contents; +} + +function addDidUpdatePushCredentialsSwift(contents: string) { + const updatedPushCredentialsMethod = + "RNVoipPushNotificationManager.didUpdate(credentials, forType: type.rawValue)"; + + if (!contents.includes(updatedPushCredentialsMethod)) { + const functionSelector = "pushRegistry(_:didUpdate:for:)"; + + const codeBlock = findSwiftFunctionCodeBlock(contents, functionSelector); + + if (!codeBlock) { + return insertContentsInsideSwiftClassBlock( + contents, + "class AppDelegate", + ` + public func pushRegistry( + _ registry: PKPushRegistry, + didUpdate credentials: PKPushCredentials, + for type: PKPushType + ) { + ${updatedPushCredentialsMethod} + } + `, + { position: "tail" }, + ); + } else { + return insertContentsInsideSwiftFunctionBlock( + contents, + functionSelector, + updatedPushCredentialsMethod, + { position: "tail" }, + ); + } + } + return contents; +} + +function addDidReceiveIncomingPushCallbackSwift(contents: string) { + const onIncomingPush = ` + let uuid = payload.dictionaryPayload["uuid"] as? String ?? UUID().uuidString + let handle = payload.dictionaryPayload["handle"] as? String ?? "Unknown" + let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown" + let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool ?? true + + RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) + RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) + + RNCallKeep.reportNewIncomingCall(uuid, + handle: handle, + handleType: "generic", + hasVideo: hasVideo, + localizedCallerName: callerName, + supportsHolding: false, + supportsDTMF: false, + supportsGrouping: false, + supportsUngrouping: false, + fromPushKit: true, + payload: nil, + withCompletionHandler: nil)`; + if ( + !contents.includes("RNVoipPushNotificationManager.didReceiveIncomingPush") + ) { + const functionSelector = + "pushRegistry(_:didReceiveIncomingPushWith:for:completion:)"; + + const codeBlock = findSwiftFunctionCodeBlock(contents, functionSelector); + + if (!codeBlock) { + return insertContentsInsideSwiftClassBlock( + contents, + "class AppDelegate", + ` + public func pushRegistry( + _ registry: PKPushRegistry, + didReceiveIncomingPushWith payload: PKPushPayload, + for type: PKPushType, + completion: @escaping () -> Void + ) { + ${onIncomingPush} + } + `, + { position: "tail" }, + ); + } else { + return insertContentsInsideSwiftFunctionBlock( + contents, + functionSelector, + onIncomingPush, + { position: "tail" }, + ); + } + } + + return contents; +} + +function addToSwiftBridgingHeaderFile( + projectRoot: string, + projectName: string | undefined, + action: (contents: string) => string, +) { + if (!projectName) { + console.error("No project name provided"); + return; + } + + const bridgingHeaderPath = path.join( + projectRoot, + "ios", + projectName, + `${projectName}-Bridging-Header.h`, + ); + + if (!fs.existsSync(bridgingHeaderPath)) { + console.error(`File not found at: ${bridgingHeaderPath}`); + return; + } + + const bridgingHeaderContents = fs.readFileSync(bridgingHeaderPath, "utf8"); + const newBridgingHeaderContents = action(bridgingHeaderContents); + + fs.writeFileSync(bridgingHeaderPath, newBridgingHeaderContents); +} + +const withDisabledPrecompileBridgingHeader: ConfigPlugin = (config) => { + return withXcodeProject(config, (config) => { + const xcodeProject = config.modResults; + + const configurations = xcodeProject.pbxXCBuildConfigurationSection(); + + for (const key in configurations) { + if (typeof configurations[key].buildSettings !== "undefined") { + const buildSettings = configurations[key].buildSettings; + + buildSettings.SWIFT_PRECOMPILE_BRIDGING_HEADER = "NO"; + } + } + + return config; + }); +}; + +const withVoipBackgroundModes: ConfigPlugin = (config) => { + return withInfoPlist(config, (config) => { + if (!config.modResults.UIBackgroundModes) { + config.modResults.UIBackgroundModes = []; + } + + if (!config.modResults.UIBackgroundModes.includes("voip")) { + config.modResults.UIBackgroundModes.push("voip"); + } + + return config; + }); +}; + +const withVoipPushNotification: ConfigPlugin = (config) => { + config = withDisabledPrecompileBridgingHeader(config); + config = withVoipBackgroundModes(config); + config = withVoipAppDelegate(config); + return config; +}; + +export default withVoipPushNotification; diff --git a/packages/react-native-voip-push-notification/tsconfig.json b/packages/react-native-voip-push-notification/tsconfig.json new file mode 100644 index 00000000..354bddb4 --- /dev/null +++ b/packages/react-native-voip-push-notification/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "expo-module-scripts/tsconfig.plugin", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "include": ["./src"], + "exclude": ["**/__mocks__/*", "**/__tests__/*"] +}