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 block = '\n manifestPlaceholders = [\n' + ' JPUSH_APPKEY: "' + appKey + '",\n' + ' JPUSH_CHANNEL: "' + channel + '"\n' + ' ]'; const contents = config.modResults.contents; if (contents.includes('manifestPlaceholders')) { config.modResults.contents = contents.replace( /manifestPlaceholders\s*=\s*\[[\s\S]*?\]/, block.trim() ); } else if (contents.includes('REACT_NATIVE_RELEASE_LEVEL')) { 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') || contents.includes('JCoreModule')) { return config; } const isSwift = contents.includes('class AppDelegate') || contents.includes('@main'); if (isSwift) { let newContents = contents; if (!newContents.includes('import JPushRN')) { 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` ); } } // Swift: Add JPush init in application(_:didFinishLaunchingWithOptions:) // Match various Swift signatures const swiftDidFinishPatterns = [ /public override func application\(\s*_ application: UIApplication,\s*didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\? = nil\s*\) -> Bool \{/, /func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\?\?\) -> Bool \{/, ]; let didInsertInit = false; for (const pattern of swiftDidFinishPatterns) { if (pattern.test(newContents)) { newContents = newContents.replace( pattern, `$&\n // JPush initialization\n #if DEBUG\n JCoreModule.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: false)\n #else\n JCoreModule.setup(withOption: launchOptions, appKey: "${appKey}", channel: "${channel}", apsForProduction: true)\n #endif` ); didInsertInit = true; break; } } // Fallback: find the didFinishLaunchingWithOptions method start and insert after the opening brace if (!didInsertInit && newContents.includes('didFinishLaunchingWithOptions')) { const lines = newContents.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes('didFinishLaunchingWithOptions') && lines[i].includes('{')) { lines.splice(i + 1, 0, ' // 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' ); newContents = lines.join('\n'); didInsertInit = true; break; } } } // Also add delegate = self after regex-based insertion if not already present if (didInsertInit && !newContents.includes('UNUserNotificationCenter.current().delegate')) { newContents = newContents.replace( /(#endif\n)(\s+let delegate)/, `$1 UNUserNotificationCenter.current().delegate = self\n$2` ); } // Swift: Add delegate methods before the closing brace of the class 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')) { // Find the closing brace of the AppDelegate class 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;