From 44a5742966c944791cfd77db4a0d8aa7c7fa850b Mon Sep 17 00:00:00 2001 From: Thomas Mengelatte Date: Tue, 21 Oct 2025 14:06:25 +0200 Subject: [PATCH 1/3] test: upgrade Jest and related dependencies to v29 --- eslint.config.mjs | 2 ++ jest.config.js | 9 +++++++++ package.json | 12 +++++++----- src/NativeRNBatchModule.ts | 2 -- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 51ca5bf..15a9d9f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -84,6 +84,8 @@ export default [...compat.extends( argsIgnorePattern: "^_", }], "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-wrapper-object-types": "off", "prettier/prettier": "warn", }, diff --git a/jest.config.js b/jest.config.js index 840b7ed..8b388d6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,4 +2,13 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testPathIgnorePatterns: ['/dist/', '/node_modules/'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { + isolatedModules: true, + tsconfig: { + esModuleInterop: true, + skipLibCheck: true + } + }] + } }; diff --git a/package.json b/package.json index 9a3e811..5433ea2 100644 --- a/package.json +++ b/package.json @@ -37,20 +37,22 @@ "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.7.0", - "@types/jest": "^27.0.2", + "@types/jest": "^29.5.12", "@types/react": "^17.0.33", "@types/react-native": "^0.73.0", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "eslint": "^9.7.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "^12.1.1", "expo-module-scripts": "^3.5.2", "globals": "^15.8.0", - "jest": "^27.3.1", + "jest": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-util": "^29.7.0", "prettier": "^3.3.3", "react-native": "^0.73.0", - "ts-jest": "^27.0.7", + "ts-jest": "^29.1.0", "typedoc": "^0.26.4", "typescript": "^5.5.3" }, diff --git a/src/NativeRNBatchModule.ts b/src/NativeRNBatchModule.ts index 9cd857e..0b5d636 100644 --- a/src/NativeRNBatchModule.ts +++ b/src/NativeRNBatchModule.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-types */ - import { TurboModule, TurboModuleRegistry } from 'react-native'; import { BatchInboxFetcher, BatchUserAttribute, IInboxNotification } from './Batch'; From 00a32c66e4a35f166abf7c27de91d2c2d17e59b2 Mon Sep 17 00:00:00 2001 From: Thomas Mengelatte Date: Tue, 21 Oct 2025 14:06:25 +0200 Subject: [PATCH 2/3] fix: add support for configurable Intent nullability in MainActivity Refactor MainActivity modification logic for Intent nullability Add comprehensive tests for MainActivity Intent nullability Add test fixtures for nullable Intent MainActivity --- .../__tests__/withReactNativeBatch.test.ts | 44 ++++++++++++ .../withReactNativeBatchMainActivity.test.ts | 25 ++++--- .../withReactNativeBatchMainActivity.ts | 29 ++++++-- plugin/src/fixtures/mainActivity.ts | 70 +++++++++++++++++++ plugin/src/withReactNativeBatch.ts | 8 ++- 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 plugin/src/__tests__/withReactNativeBatch.test.ts diff --git a/plugin/src/__tests__/withReactNativeBatch.test.ts b/plugin/src/__tests__/withReactNativeBatch.test.ts new file mode 100644 index 0000000..d9b1eaf --- /dev/null +++ b/plugin/src/__tests__/withReactNativeBatch.test.ts @@ -0,0 +1,44 @@ +import { Props } from '../withReactNativeBatch'; + +describe('withReactNativeBatch', () => { + describe('Props default values', () => { + it('should default shouldUseNonNullableIntent to false when props is undefined', () => { + const props = undefined; + const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false }; + + expect(_props.shouldUseNonNullableIntent).toBe(false); + }); + + it('should default shouldUseNonNullableIntent to false when props is provided without the property', () => { + const props: Props = { + androidApiKey: 'FAKE_ANDROID_API_KEY', + iosApiKey: 'FAKE_IOS_API_KEY', + }; + const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false }; + + expect(_props.shouldUseNonNullableIntent).toBeUndefined(); + }); + + it('should use provided shouldUseNonNullableIntent value when explicitly set to true', () => { + const props: Props = { + androidApiKey: 'FAKE_ANDROID_API_KEY', + iosApiKey: 'FAKE_IOS_API_KEY', + shouldUseNonNullableIntent: true, + }; + const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false }; + + expect(_props.shouldUseNonNullableIntent).toBe(true); + }); + + it('should use provided shouldUseNonNullableIntent value when explicitly set to false', () => { + const props: Props = { + androidApiKey: 'FAKE_ANDROID_API_KEY', + iosApiKey: 'FAKE_IOS_API_KEY', + shouldUseNonNullableIntent: false, + }; + const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false }; + + expect(_props.shouldUseNonNullableIntent).toBe(false); + }); + }); +}); diff --git a/plugin/src/__tests__/withReactNativeBatchMainActivity.test.ts b/plugin/src/__tests__/withReactNativeBatchMainActivity.test.ts index 563f3db..bc24f46 100644 --- a/plugin/src/__tests__/withReactNativeBatchMainActivity.test.ts +++ b/plugin/src/__tests__/withReactNativeBatchMainActivity.test.ts @@ -1,20 +1,29 @@ -import { modifyMainActivity } from '../android/withReactNativeBatchMainActivity'; +import { modifyMainJavaActivity, modifyMainKotlinActivity } from '../android/withReactNativeBatchMainActivity'; import { mainJavaActivityExpectedFixture, mainJavaActivityFixture, mainKotlinActivityExpectedFixture, + mainKotlinActivityExpectedFixtureNullable, mainKotlinActivityFixture, } from '../fixtures/mainActivity'; -describe(modifyMainActivity, () => { - it('should push on new intent in java main activity', () => { - const result = modifyMainActivity(mainJavaActivityFixture); - expect(result).toEqual(mainJavaActivityExpectedFixture); +describe('withReactNativeBatchMainActivity', () => { + describe('modifyMainJavaActivity', () => { + it('should push on new intent in java main activity', () => { + const result = modifyMainJavaActivity(mainJavaActivityFixture); + expect(result).toEqual(mainJavaActivityExpectedFixture); + }); }); - it('should push on new intent in kotlin main activity', () => { - const result = modifyMainActivity(mainKotlinActivityFixture); + describe('modifyMainKotlinActivity', () => { + it('should push on new intent in kotlin main activity with non-nullable Intent (SDK 54+)', () => { + const result = modifyMainKotlinActivity(mainKotlinActivityFixture, true); + expect(result).toEqual(mainKotlinActivityExpectedFixture); + }); - expect(result).toEqual(mainKotlinActivityExpectedFixture); + it('should push on new intent in kotlin main activity with nullable Intent (SDK 53-)', () => { + const result = modifyMainKotlinActivity(mainKotlinActivityFixture, false); + expect(result).toEqual(mainKotlinActivityExpectedFixtureNullable); + }); }); }); diff --git a/plugin/src/android/withReactNativeBatchMainActivity.ts b/plugin/src/android/withReactNativeBatchMainActivity.ts index f744693..19a78c8 100644 --- a/plugin/src/android/withReactNativeBatchMainActivity.ts +++ b/plugin/src/android/withReactNativeBatchMainActivity.ts @@ -1,4 +1,9 @@ -import { ConfigPlugin, withMainActivity } from '@expo/config-plugins'; +import { ConfigPlugin, ExportedConfigWithProps, withMainActivity } from '@expo/config-plugins'; +import { ApplicationProjectFile } from '@expo/config-plugins/build/android/Paths'; + +export type MainActivityProps = { + shouldUseNonNullableIntent?: boolean; +}; export const modifyMainJavaActivity = (content: string): string => { let newContent = content; @@ -44,7 +49,7 @@ import com.batch.android.Batch;` return newContent; }; -export const modifyMainKotlinActivity = (content: string): string => { +export const modifyMainKotlinActivity = (content: string, useNonNullableIntent: boolean): string => { let newContent = content; if (!newContent.includes('import android.content.Intent')) { @@ -68,9 +73,12 @@ import com.batch.android.Batch` const start = newContent.substring(0, lastBracketIndex); const end = newContent.substring(lastBracketIndex); + // Use non-nullable Intent for SDK 54+, nullable for SDK 53 and below + const intentType = useNonNullableIntent ? 'Intent' : 'Intent?'; + newContent = start + - `\n override fun onNewIntent(intent: Intent?) { + `\n override fun onNewIntent(intent: ${intentType}) { super.onNewIntent(intent) Batch.onNewIntent(this, intent) }\n` + @@ -86,21 +94,28 @@ import com.batch.android.Batch` return newContent; }; -export const modifyMainActivity = (content: string): string => { - return isKotlinMainActivity(content) ? modifyMainKotlinActivity(content) : modifyMainJavaActivity(content); +export const modifyMainActivity = ( + config: ExportedConfigWithProps, + shouldUseNonNullableIntent: boolean = false +): string => { + return isKotlinMainActivity(config.modResults.contents) + ? modifyMainKotlinActivity(config.modResults.contents, shouldUseNonNullableIntent) + : modifyMainJavaActivity(config.modResults.contents); }; const isKotlinMainActivity = (content: string): boolean => { return content.includes('class MainActivity : ReactActivity()'); }; -export const withReactNativeBatchMainActivity: ConfigPlugin = config => { +export const withReactNativeBatchMainActivity: ConfigPlugin = (config, props) => { + const shouldUseNonNullableIntent = props?.shouldUseNonNullableIntent ?? false; + return withMainActivity(config, config => { return { ...config, modResults: { ...config.modResults, - contents: modifyMainActivity(config.modResults.contents), + contents: modifyMainActivity(config, shouldUseNonNullableIntent), }, }; }); diff --git a/plugin/src/fixtures/mainActivity.ts b/plugin/src/fixtures/mainActivity.ts index 3ec63e4..b79d829 100644 --- a/plugin/src/fixtures/mainActivity.ts +++ b/plugin/src/fixtures/mainActivity.ts @@ -103,6 +103,7 @@ class MainActivity : ReactActivity() { } }`; +// Expected fixture for Expo SDK 54+ (non-nullable Intent) export const mainKotlinActivityExpectedFixture = `package com.arnaudr.expobeta50 import android.os.Build @@ -117,6 +118,75 @@ import com.batch.android.Batch import expo.modules.ReactActivityDelegateWrapper +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + Batch.onNewIntent(this, intent) + } +}`; + +// Expected fixture for Expo SDK 53 and below (nullable Intent) +export const mainKotlinActivityExpectedFixtureNullable = `package com.arnaudr.expobeta50 + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate +import android.content.Intent +import com.batch.android.Batch + +import expo.modules.ReactActivityDelegateWrapper + class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { // Set the theme to AppTheme BEFORE onCreate to support diff --git a/plugin/src/withReactNativeBatch.ts b/plugin/src/withReactNativeBatch.ts index 28b5676..379cb77 100644 --- a/plugin/src/withReactNativeBatch.ts +++ b/plugin/src/withReactNativeBatch.ts @@ -16,6 +16,7 @@ export type Props = { enableDefaultOptOut?: boolean; enableProfileCustomIDMigration?: boolean; enableProfileCustomDataMigration?: boolean; + shouldUseNonNullableIntent?: boolean; }; /** * Apply react-native-batch configuration for Expo SDK 42 projects. @@ -23,13 +24,18 @@ export type Props = { const withReactNativeBatch: ConfigPlugin = (config, props) => { const _props = props || { androidApiKey: '', iosApiKey: '' }; + // Default shouldUseNonNullableIntent to false if not explicitly provided + if (_props.shouldUseNonNullableIntent === undefined) { + _props.shouldUseNonNullableIntent = false; + } + let newConfig = withGoogleServicesFile(config); newConfig = withClassPath(newConfig); newConfig = withApplyPlugin(newConfig); newConfig = withReactNativeBatchManifest(newConfig, _props); newConfig = withReactNativeBatchAppBuildGradle(newConfig, _props); newConfig = withReactNativeBatchMainApplication(newConfig); - newConfig = withReactNativeBatchMainActivity(newConfig); + newConfig = withReactNativeBatchMainActivity(newConfig, _props); newConfig = withReactNativeBatchInfoPlist(newConfig, _props); newConfig = withReactNativeBatchEntitlements(newConfig); newConfig = withReactNativeBatchAppDelegate(newConfig); From 8e03eb6f6e9c7b3042674bf38c78ed131b3311c6 Mon Sep 17 00:00:00 2001 From: Thomas Mengelatte Date: Tue, 21 Oct 2025 14:06:25 +0200 Subject: [PATCH 3/3] all: bump version to 11.1.0 --- CHANGELOG.md | 7 +++++++ .../main/java/com/batch/batch_rn/RNBatchModuleImpl.java | 2 +- ios/RNBatch.h | 2 +- package.json | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b2b7e..32daf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +11.1.0 +___ + +**Expo** +- Added configuration field `shouldUseNonNullableIntent` to control whether the MainActivity's `onNewIntent` method uses a nullable or non-nullable Intent parameter on Android. This is required for compatibility with AndroidX Activity 1.9+ which uses non-nullable Intent types. By default, it is set to `false` (nullable Intent) for backwards compatibility. Set it to `true` if you're using AndroidX Activity 1.9 or higher. + + 11.0.0 ___ diff --git a/android/src/main/java/com/batch/batch_rn/RNBatchModuleImpl.java b/android/src/main/java/com/batch/batch_rn/RNBatchModuleImpl.java index 9160b61..d58ab5e 100644 --- a/android/src/main/java/com/batch/batch_rn/RNBatchModuleImpl.java +++ b/android/src/main/java/com/batch/batch_rn/RNBatchModuleImpl.java @@ -53,7 +53,7 @@ public class RNBatchModuleImpl { private static final String PLUGIN_VERSION_ENVIRONMENT_VARIABLE = "batch.plugin.version"; - public static final String PLUGIN_VERSION = "ReactNative/11.0.0"; + public static final String PLUGIN_VERSION = "ReactNative/11.1.0"; public static final String LOGGER_TAG = "RNBatchBridge"; diff --git a/ios/RNBatch.h b/ios/RNBatch.h index 9d6817c..5eb2acd 100644 --- a/ios/RNBatch.h +++ b/ios/RNBatch.h @@ -1,7 +1,7 @@ #import #import -#define PluginVersion "ReactNative/11.0.0" +#define PluginVersion "ReactNative/11.1.0" #ifdef RCT_NEW_ARCH_ENABLED #import diff --git a/package.json b/package.json index 5433ea2..c8fe372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@batch.com/react-native-plugin", - "version": "11.0.0", + "version": "11.1.0", "description": "Batch.com React-Native Plugin", "homepage": "https://github.com/BatchLabs/Batch-React-Native-Plugin", "main": "dist/Batch.js",