Skip to content

Commit 6f192fc

Browse files
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
1 parent 1ac9231 commit 6f192fc

File tree

5 files changed

+160
-16
lines changed

5 files changed

+160
-16
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Props } from '../withReactNativeBatch';
2+
3+
describe('withReactNativeBatch', () => {
4+
describe('Props default values', () => {
5+
it('should default shouldUseNonNullableIntent to false when props is undefined', () => {
6+
const props = undefined;
7+
const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false };
8+
9+
expect(_props.shouldUseNonNullableIntent).toBe(false);
10+
});
11+
12+
it('should default shouldUseNonNullableIntent to false when props is provided without the property', () => {
13+
const props: Props = {
14+
androidApiKey: 'FAKE_ANDROID_API_KEY',
15+
iosApiKey: 'FAKE_IOS_API_KEY',
16+
};
17+
const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false };
18+
19+
expect(_props.shouldUseNonNullableIntent).toBeUndefined();
20+
});
21+
22+
it('should use provided shouldUseNonNullableIntent value when explicitly set to true', () => {
23+
const props: Props = {
24+
androidApiKey: 'FAKE_ANDROID_API_KEY',
25+
iosApiKey: 'FAKE_IOS_API_KEY',
26+
shouldUseNonNullableIntent: true,
27+
};
28+
const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false };
29+
30+
expect(_props.shouldUseNonNullableIntent).toBe(true);
31+
});
32+
33+
it('should use provided shouldUseNonNullableIntent value when explicitly set to false', () => {
34+
const props: Props = {
35+
androidApiKey: 'FAKE_ANDROID_API_KEY',
36+
iosApiKey: 'FAKE_IOS_API_KEY',
37+
shouldUseNonNullableIntent: false,
38+
};
39+
const _props = props || { androidApiKey: '', iosApiKey: '', shouldUseNonNullableIntent: false };
40+
41+
expect(_props.shouldUseNonNullableIntent).toBe(false);
42+
});
43+
});
44+
});
Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import { modifyMainActivity } from '../android/withReactNativeBatchMainActivity';
1+
import { modifyMainJavaActivity, modifyMainKotlinActivity } from '../android/withReactNativeBatchMainActivity';
22
import {
33
mainJavaActivityExpectedFixture,
44
mainJavaActivityFixture,
55
mainKotlinActivityExpectedFixture,
6+
mainKotlinActivityExpectedFixtureNullable,
67
mainKotlinActivityFixture,
78
} from '../fixtures/mainActivity';
89

9-
describe(modifyMainActivity, () => {
10-
it('should push on new intent in java main activity', () => {
11-
const result = modifyMainActivity(mainJavaActivityFixture);
12-
expect(result).toEqual(mainJavaActivityExpectedFixture);
10+
describe('withReactNativeBatchMainActivity', () => {
11+
describe('modifyMainJavaActivity', () => {
12+
it('should push on new intent in java main activity', () => {
13+
const result = modifyMainJavaActivity(mainJavaActivityFixture);
14+
expect(result).toEqual(mainJavaActivityExpectedFixture);
15+
});
1316
});
1417

15-
it('should push on new intent in kotlin main activity', () => {
16-
const result = modifyMainActivity(mainKotlinActivityFixture);
18+
describe('modifyMainKotlinActivity', () => {
19+
it('should push on new intent in kotlin main activity with non-nullable Intent (SDK 54+)', () => {
20+
const result = modifyMainKotlinActivity(mainKotlinActivityFixture, true);
21+
expect(result).toEqual(mainKotlinActivityExpectedFixture);
22+
});
1723

18-
expect(result).toEqual(mainKotlinActivityExpectedFixture);
24+
it('should push on new intent in kotlin main activity with nullable Intent (SDK 53-)', () => {
25+
const result = modifyMainKotlinActivity(mainKotlinActivityFixture, false);
26+
expect(result).toEqual(mainKotlinActivityExpectedFixtureNullable);
27+
});
1928
});
2029
});

plugin/src/android/withReactNativeBatchMainActivity.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ConfigPlugin, withMainActivity } from '@expo/config-plugins';
1+
import { ConfigPlugin, ExportedConfigWithProps, withMainActivity } from '@expo/config-plugins';
2+
import { ApplicationProjectFile } from '@expo/config-plugins/build/android/Paths';
3+
4+
export type MainActivityProps = {
5+
shouldUseNonNullableIntent?: boolean;
6+
};
27

38
export const modifyMainJavaActivity = (content: string): string => {
49
let newContent = content;
@@ -44,7 +49,7 @@ import com.batch.android.Batch;`
4449
return newContent;
4550
};
4651

47-
export const modifyMainKotlinActivity = (content: string): string => {
52+
export const modifyMainKotlinActivity = (content: string, useNonNullableIntent: boolean): string => {
4853
let newContent = content;
4954

5055
if (!newContent.includes('import android.content.Intent')) {
@@ -68,9 +73,12 @@ import com.batch.android.Batch`
6873
const start = newContent.substring(0, lastBracketIndex);
6974
const end = newContent.substring(lastBracketIndex);
7075

76+
// Use non-nullable Intent for SDK 54+, nullable for SDK 53 and below
77+
const intentType = useNonNullableIntent ? 'Intent' : 'Intent?';
78+
7179
newContent =
7280
start +
73-
`\n override fun onNewIntent(intent: Intent?) {
81+
`\n override fun onNewIntent(intent: ${intentType}) {
7482
super.onNewIntent(intent)
7583
Batch.onNewIntent(this, intent)
7684
}\n` +
@@ -86,21 +94,28 @@ import com.batch.android.Batch`
8694
return newContent;
8795
};
8896

89-
export const modifyMainActivity = (content: string): string => {
90-
return isKotlinMainActivity(content) ? modifyMainKotlinActivity(content) : modifyMainJavaActivity(content);
97+
export const modifyMainActivity = (
98+
config: ExportedConfigWithProps<ApplicationProjectFile>,
99+
shouldUseNonNullableIntent: boolean = false
100+
): string => {
101+
return isKotlinMainActivity(config.modResults.contents)
102+
? modifyMainKotlinActivity(config.modResults.contents, shouldUseNonNullableIntent)
103+
: modifyMainJavaActivity(config.modResults.contents);
91104
};
92105

93106
const isKotlinMainActivity = (content: string): boolean => {
94107
return content.includes('class MainActivity : ReactActivity()');
95108
};
96109

97-
export const withReactNativeBatchMainActivity: ConfigPlugin<object | void> = config => {
110+
export const withReactNativeBatchMainActivity: ConfigPlugin<MainActivityProps | void> = (config, props) => {
111+
const shouldUseNonNullableIntent = props?.shouldUseNonNullableIntent ?? false;
112+
98113
return withMainActivity(config, config => {
99114
return {
100115
...config,
101116
modResults: {
102117
...config.modResults,
103-
contents: modifyMainActivity(config.modResults.contents),
118+
contents: modifyMainActivity(config, shouldUseNonNullableIntent),
104119
},
105120
};
106121
});

plugin/src/fixtures/mainActivity.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class MainActivity : ReactActivity() {
103103
}
104104
}`;
105105

106+
// Expected fixture for Expo SDK 54+ (non-nullable Intent)
106107
export const mainKotlinActivityExpectedFixture = `package com.arnaudr.expobeta50
107108
108109
import android.os.Build
@@ -117,6 +118,75 @@ import com.batch.android.Batch
117118
118119
import expo.modules.ReactActivityDelegateWrapper
119120
121+
class MainActivity : ReactActivity() {
122+
override fun onCreate(savedInstanceState: Bundle?) {
123+
// Set the theme to AppTheme BEFORE onCreate to support
124+
// coloring the background, status bar, and navigation bar.
125+
// This is required for expo-splash-screen.
126+
setTheme(R.style.AppTheme);
127+
super.onCreate(null)
128+
}
129+
130+
/**
131+
* Returns the name of the main component registered from JavaScript. This is used to schedule
132+
* rendering of the component.
133+
*/
134+
override fun getMainComponentName(): String = "main"
135+
136+
/**
137+
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
138+
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
139+
*/
140+
override fun createReactActivityDelegate(): ReactActivityDelegate {
141+
return ReactActivityDelegateWrapper(
142+
this,
143+
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
144+
object : DefaultReactActivityDelegate(
145+
this,
146+
mainComponentName,
147+
fabricEnabled
148+
){})
149+
}
150+
151+
/**
152+
* Align the back button behavior with Android S
153+
* where moving root activities to background instead of finishing activities.
154+
*/
155+
override fun invokeDefaultOnBackPressed() {
156+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
157+
if (!moveTaskToBack(false)) {
158+
// For non-root activities, use the default implementation to finish them.
159+
super.invokeDefaultOnBackPressed()
160+
}
161+
return
162+
}
163+
164+
// Use the default back button implementation on Android S
165+
// because it's doing more than [Activity.moveTaskToBack] in fact.
166+
super.invokeDefaultOnBackPressed()
167+
}
168+
169+
override fun onNewIntent(intent: Intent) {
170+
super.onNewIntent(intent)
171+
Batch.onNewIntent(this, intent)
172+
}
173+
}`;
174+
175+
// Expected fixture for Expo SDK 53 and below (nullable Intent)
176+
export const mainKotlinActivityExpectedFixtureNullable = `package com.arnaudr.expobeta50
177+
178+
import android.os.Build
179+
import android.os.Bundle
180+
181+
import com.facebook.react.ReactActivity
182+
import com.facebook.react.ReactActivityDelegate
183+
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
184+
import com.facebook.react.defaults.DefaultReactActivityDelegate
185+
import android.content.Intent
186+
import com.batch.android.Batch
187+
188+
import expo.modules.ReactActivityDelegateWrapper
189+
120190
class MainActivity : ReactActivity() {
121191
override fun onCreate(savedInstanceState: Bundle?) {
122192
// Set the theme to AppTheme BEFORE onCreate to support

plugin/src/withReactNativeBatch.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,26 @@ export type Props = {
1616
enableDefaultOptOut?: boolean;
1717
enableProfileCustomIDMigration?: boolean;
1818
enableProfileCustomDataMigration?: boolean;
19+
shouldUseNonNullableIntent?: boolean;
1920
};
2021
/**
2122
* Apply react-native-batch configuration for Expo SDK 42 projects.
2223
*/
2324
const withReactNativeBatch: ConfigPlugin<Props | void> = (config, props) => {
2425
const _props = props || { androidApiKey: '', iosApiKey: '' };
2526

27+
// Default shouldUseNonNullableIntent to false if not explicitly provided
28+
if (_props.shouldUseNonNullableIntent === undefined) {
29+
_props.shouldUseNonNullableIntent = false;
30+
}
31+
2632
let newConfig = withGoogleServicesFile(config);
2733
newConfig = withClassPath(newConfig);
2834
newConfig = withApplyPlugin(newConfig);
2935
newConfig = withReactNativeBatchManifest(newConfig, _props);
3036
newConfig = withReactNativeBatchAppBuildGradle(newConfig, _props);
3137
newConfig = withReactNativeBatchMainApplication(newConfig);
32-
newConfig = withReactNativeBatchMainActivity(newConfig);
38+
newConfig = withReactNativeBatchMainActivity(newConfig, _props);
3339
newConfig = withReactNativeBatchInfoPlist(newConfig, _props);
3440
newConfig = withReactNativeBatchEntitlements(newConfig);
3541
newConfig = withReactNativeBatchAppDelegate(newConfig);

0 commit comments

Comments
 (0)