Files
frontend/plugins/withJPush.js
lan 52d2581dda
All checks were successful
Frontend CI / ota-ios (push) Successful in 1m52s
Frontend CI / ota-android (push) Successful in 3m54s
Frontend CI / build-and-push-web (push) Successful in 5m14s
Frontend CI / build-android-apk (push) Successful in 24m39s
refactor(plugins): update JPush config and improve SmartImage loading
- Update `withJPush` plugin to support Expo SDK 56+ by using `JPUSHService.setup` instead of `JCoreModule.setup` and adding `UNUserNotificationCenterDelegate` conformance.
- Improve `SmartImage` component by adding a ref to prevent loading state flickering when transitioning from preview to original image.
- Upgrade `@shopify/flash-list` dependency.
- Add transition prop to `SmartImage` for smoother image loading.
2026-06-02 01:02:41 +08:00

321 lines
13 KiB
JavaScript

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 <UserNotifications/UserNotifications.h>')) {
newContents = newContents.replace(
/(#import "AppDelegate.h")/,
`$1\n#import <UserNotifications/UserNotifications.h>`
);
}
if (!newContents.includes('#import <jcore-react-native/JCoreModule.h>')) {
newContents = newContents.replace(
/(#import "AppDelegate.h")/,
`$1\n#import <jcore-react-native/JCoreModule.h>`
);
}
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 <UserNotifications/UserNotifications.h>\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;