const { withAndroidManifest, withDangerousMod, withAppDelegate, withInfoPlist, withAppBuildGradle, } = require('@expo/config-plugins'); const fs = require('fs'); const path = require('path'); const withJPush = (config, options = {}) => { const { appKey, channel = 'developer-default' } = options; if (!appKey) { throw new Error('JPush appKey is required in plugin options'); } // 1. Android: Inject manifestPlaceholders into app/build.gradle config = withAppBuildGradle(config, (config) => { const contents = config.modResults.contents; const requiredPlaceholders = { JPUSH_APPKEY: appKey, JPUSH_CHANNEL: channel, }; const manifestPlaceholderRegex = /manifestPlaceholders\s*=\s*\[([\s\S]*?)\]/; const match = contents.match(manifestPlaceholderRegex); if (match) { const existingBlock = match[1]; const existingEntries = {}; const entryRegex = /(\w+)\s*:\s*"([^"]*)"/g; let entryMatch; while ((entryMatch = entryRegex.exec(existingBlock)) !== null) { existingEntries[entryMatch[1]] = entryMatch[2]; } for (const [key, value] of Object.entries(requiredPlaceholders)) { existingEntries[key] = value; } const entriesStr = Object.entries(existingEntries) .map(([k, v]) => `${k}: "${v}"`) .join(',\n '); const newBlock = `\n manifestPlaceholders = [\n ${entriesStr}\n ]`; config.modResults.contents = contents.replace(manifestPlaceholderRegex, newBlock.trim()); } else if (contents.includes('REACT_NATIVE_RELEASE_LEVEL')) { const entriesStr = Object.entries(requiredPlaceholders) .map(([k, v]) => `${k}: "${v}"`) .join(',\n '); const block = '\n manifestPlaceholders = [\n ' + entriesStr + '\n ]'; const lines = contents.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes('REACT_NATIVE_RELEASE_LEVEL')) { lines.splice(i + 1, 0, block); break; } } config.modResults.contents = lines.join('\n'); } return config; }); // 2. Android: Add JPush metadata and permissions to AndroidManifest config = withAndroidManifest(config, (config) => { const manifest = config.modResults; if (!manifest.manifest['uses-permission']) { manifest.manifest['uses-permission'] = []; } const requiredPermissions = [ 'android.permission.RECEIVE_USER_PRESENT', 'android.permission.POST_NOTIFICATIONS', ]; requiredPermissions.forEach((permission) => { const exists = manifest.manifest['uses-permission'].find( (p) => p.$['android:name'] === permission ); if (!exists) { manifest.manifest['uses-permission'].push({ $: { 'android:name': permission }, }); } }); return config; }); // 3. iOS: Add JPush initialization to AppDelegate config = withAppDelegate(config, (config) => { const { contents } = config.modResults; if (contents.includes('JPush initialization') && contents.includes('JPUSHService.registerDeviceToken')) { return config; } const isSwift = contents.includes('class AppDelegate') || contents.includes('@main'); if (isSwift) { let newContents = contents; // Add UNUserNotifications import if needed if (!newContents.includes('import UserNotifications')) { if (newContents.includes('import UIKit')) { newContents = newContents.replace( /(import UIKit\n)/, `$1import UserNotifications\n` ); } else { newContents = newContents.replace( /(internal import Expo\n)/, `$1import UserNotifications\n` ); } } // Add UNUserNotificationCenterDelegate conformance if needed if (!newContents.includes('UNUserNotificationCenterDelegate')) { newContents = newContents.replace( /class AppDelegate: ExpoAppDelegate \{/, 'class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate {' ); } // Swift: Add JPush init in application(_:didFinishLaunchingWithOptions:) // Expo SDK 56+ uses JPUSHService.setup (not JCoreModule.setup) if (!newContents.includes('JPUSHService.setup') && !newContents.includes('JCoreModule.setup')) { const jpushInitBlock = ` // JPush initialization #if DEBUG JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: false) #else JPUSHService.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: true) #endif UNUserNotificationCenter.current().delegate = self`; // Insert after the opening brace of didFinishLaunchingWithOptions // The { may be on the same line as didFinishLaunchingWithOptions or on a subsequent line if (newContents.includes('didFinishLaunchingWithOptions')) { const lines = newContents.split('\n'); let foundDidFinish = false; for (let i = 0; i < lines.length; i++) { if (lines[i].includes('didFinishLaunchingWithOptions')) { foundDidFinish = true; } // Insert after the line that contains the opening brace of this method if (foundDidFinish && lines[i].includes('{')) { const indent = lines[i].match(/^(\s*)/)[1]; const initLines = jpushInitBlock.split('\n').map(l => l ? indent + l.trimStart() : l); lines.splice(i + 1, 0, ...initLines); newContents = lines.join('\n'); break; } } } } else if (newContents.includes('JCoreModule.setup') && !newContents.includes('JPUSHService.setup')) { // Migrate from JCoreModule.setup to JPUSHService.setup newContents = newContents.replace(/JCoreModule\.setup/g, 'JPUSHService.setup'); } // Add delegate = self after init insertion if not present if (!newContents.includes('UNUserNotificationCenter.current().delegate')) { newContents = newContents.replace( /(#endif\n)(\s+let delegate)/, `$1 UNUserNotificationCenter.current().delegate = self\n$2` ); } // Swift: Add JPush notification delegate methods to AppDelegate class (before closing brace) // These go in AppDelegate, NOT ReactNativeDelegate const jpushSwiftMethods = ` // JPush: Register for remote notifications override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { JPUSHService.registerDeviceToken(deviceToken) } override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { NSLog("did fail to register for remote notifications with error: %@", error.localizedDescription) } // JPush: Handle remote notification override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { JPUSHService.handleRemoteNotification(userInfo) completionHandler(.newData) } // UNUserNotificationCenter delegate: foreground notification display func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .badge, .sound]) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { JPUSHService.handleRemoteNotification(response.notification.request.content.userInfo) completionHandler() } `; if (!newContents.includes('JPUSHService.registerDeviceToken')) { // Insert before the closing brace of the AppDelegate class // Find the last '}' that closes the AppDelegate class (before ReactNativeDelegate) const classEndPattern = /\n\}\n\nclass ReactNativeDelegate/; if (classEndPattern.test(newContents)) { newContents = newContents.replace( classEndPattern, `${jpushSwiftMethods}}\n\nclass ReactNativeDelegate` ); } else { // Fallback: insert before the last closing brace const lastBraceIndex = newContents.lastIndexOf('}'); if (lastBraceIndex !== -1) { newContents = newContents.slice(0, lastBraceIndex) + jpushSwiftMethods + '\n' + newContents.slice(lastBraceIndex); } } } config.modResults.contents = newContents; } else { // ObjC AppDelegate let newContents = contents; if (!newContents.includes('#import ')) { newContents = newContents.replace( /(#import "AppDelegate.h")/, `$1\n#import ` ); } if (!newContents.includes('#import ')) { newContents = newContents.replace( /(#import "AppDelegate.h")/, `$1\n#import ` ); } const didFinishLaunchPattern = /(- \(BOOL\)application:\(UIApplication \*\)application didFinishLaunchingWithOptions:\(NSDictionary \*\)launchOptions\s*\{)/; if (didFinishLaunchPattern.test(newContents)) { newContents = newContents.replace( didFinishLaunchPattern, `$1\n // JPush initialization\n #if DEBUG\n [JCoreModule setupWithOption:launchOptions appKey:@"${appKey}" channel:@"${channel}" apsForProduction:NO];\n #else\n [JCoreModule setupWithOption:launchOptions appKey:@"${appKey}" channel:@"${channel}" apsForProduction:YES];\n #endif\n` ); } const jpushMethods = ` // JPush: Register for remote notifications - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [JCoreModule registerDeviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { NSLog(@"did fail to register for remote notifications with error: %@", error); } // JPush: Handle remote notification - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [JCoreModule didReceiveRemoteNotification:userInfo]; completionHandler(UIBackgroundFetchResultNewData); } // UNUserNotificationCenter delegate - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound); } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { [JCoreModule didReceiveRemoteNotification:response.notification.request.content.userInfo]; completionHandler(); } `; if (!newContents.includes('JCoreModule registerDeviceToken')) { newContents = newContents.replace( /@end\s*$/, `${jpushMethods}\n@end\n` ); } config.modResults.contents = newContents; } return config; }); // 4. iOS: Add bridging header for Swift if needed config = withDangerousMod(config, [ 'ios', async (config) => { const appDelegatePath = path.join(config.modRequest.platformProjectRoot, 'app', 'AppDelegate.swift'); if (fs.existsSync(appDelegatePath)) { const bridgingHeaderSearch = path.join(config.modRequest.platformProjectRoot, 'app', 'app-Bridging-Header.h'); if (fs.existsSync(bridgingHeaderSearch)) { let bridgingContent = fs.readFileSync(bridgingHeaderSearch, 'utf-8'); if (!bridgingContent.includes('JCoreModule')) { bridgingContent += '\n#import "JPUSHService.h"\n#import \n'; fs.writeFileSync(bridgingHeaderSearch, bridgingContent); } } } return config; }, ]); // 5. iOS: Add required capabilities and permissions to Info.plist config = withInfoPlist(config, (config) => { config.modResults.UIBackgroundModes = config.modResults.UIBackgroundModes || []; if (!config.modResults.UIBackgroundModes.includes('remote-notification')) { config.modResults.UIBackgroundModes.push('remote-notification'); } return config; }); return config; }; module.exports = withJPush;