Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .buildkite/basic/react-native-android-full-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ steps:
reactnavigation: "false"

- label: ':android: Build react-native-navigation {{matrix}} test fixture APK (Old Arch)'
skip: true # Skipped pending PLAT-15027
# skip: true # Skipped pending PLAT-15027
key: "build-react-native-navigation-android-fixture-old-arch"
timeout_in_minutes: 30
agents:
Expand All @@ -115,7 +115,7 @@ steps:
limit: 1

- label: ':android: Build react-native-navigation {{matrix}} test fixture APK (New Arch)'
skip: true # Skipped pending PLAT-15027
#skip: true # Skipped pending PLAT-15027
key: "build-react-native-navigation-android-fixture-new-arch"
timeout_in_minutes: 30
agents:
Expand Down Expand Up @@ -245,7 +245,7 @@ steps:
reactnavigation: "false"

- label: ":bitbar: :android: react-native-navigation {{matrix}} Android 12 (Old Arch) end-to-end tests"
skip: true # Skipped pending PLAT-15027
#skip: true # Skipped pending PLAT-15027
depends_on: "build-react-native-navigation-android-fixture-old-arch"
timeout_in_minutes: 10
plugins:
Expand Down Expand Up @@ -287,7 +287,7 @@ steps:
- "0.72"

- label: ":bitbar: :android: react-native-navigation {{matrix}} Android 12 (New Arch) end-to-end tests"
skip: true # Skipped pending PLAT-15027
# skip: true # Skipped pending PLAT-15027
depends_on: "build-react-native-navigation-android-fixture-new-arch"
timeout_in_minutes: 10
plugins:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

### Changed

- (plugin-react-native-navigation) Update react-native-navigation peer dependency to support v8 [#2727](https://github.com/bugsnag/bugsnag-js/pull/2727)

## [8.9.0] - 2026-04-08

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/plugin-react-native-navigation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"peerDependencies": {
"@bugsnag/core": "^8.0.0",
"react-native-navigation": "2 - 7"
"react-native-navigation": "2 - 8"
},
"peerDependenciesMeta": {
"@bugsnag/core": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,175 @@ describe('plugin-react-native-navigation', () => {

expect(breadcrumbs.length).toBe(0)
})

describe('navigation context tracking', () => {
it('tracks navigation between multiple screens', () => {
let listener = (event: Event) => {
throw new Error(`This function was not supposed to be called! ${event.componentName}`)
}

const Navigation = {
events () {
return {
registerComponentDidAppearListener (callback: (event: Event) => never) {
listener = callback
}
}
}
}

const plugin = new Plugin(Navigation)
const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] })

expect(client.getContext()).toBe(undefined)

// Navigate to Home
listener({ componentId: 1, componentName: 'Home', passProps: {} })
expect(client.getContext()).toBe('Home')

// Navigate to Profile
listener({ componentId: 2, componentName: 'Profile', passProps: {} })
expect(client.getContext()).toBe('Profile')

// Navigate to Settings
listener({ componentId: 3, componentName: 'Settings', passProps: {} })
expect(client.getContext()).toBe('Settings')

// Back to Home
listener({ componentId: 1, componentName: 'Home', passProps: {} })
expect(client.getContext()).toBe('Home')
})

it('handles navigation with complex component names', () => {
let listener = (event: Event) => {
throw new Error(`This function was not supposed to be called! ${event.componentName}`)
}

const Navigation = {
events () {
return {
registerComponentDidAppearListener (callback: (event: Event) => never) {
listener = callback
}
}
}
}

const plugin = new Plugin(Navigation)
const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] })

// Test with namespaced component names
listener({ componentId: 1, componentName: 'com.example.screens.HomeScreen', passProps: {} })
expect(client.getContext()).toBe('com.example.screens.HomeScreen')

// Test with screen names containing special characters
listener({ componentId: 2, componentName: 'User-Details-Screen', passProps: {} })
expect(client.getContext()).toBe('User-Details-Screen')
})
})

describe('breadcrumb metadata', () => {
it('includes previous screen context in breadcrumb', () => {
let listener = (event: Event) => {
throw new Error(`This function was not supposed to be called! ${event.componentName}`)
}

const Navigation = {
events () {
return {
registerComponentDidAppearListener (callback: (event: Event) => never) {
listener = callback
}
}
}
}

const breadcrumbs: Breadcrumb[] = []

const plugin = new Plugin(Navigation)
const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] })
client.addOnBreadcrumb(breadcrumb => { breadcrumbs.push(breadcrumb) })

listener({ componentId: 1, componentName: 'Home', passProps: {} })
listener({ componentId: 2, componentName: 'Details', passProps: {} })

expect(breadcrumbs.length).toBe(2)
expect(breadcrumbs[1].metadata).toStrictEqual({ to: 'Details', from: 'Home' })
})

it('sets from as undefined for initial navigation', () => {
let listener = (event: Event) => {
throw new Error(`This function was not supposed to be called! ${event.componentName}`)
}

const Navigation = {
events () {
return {
registerComponentDidAppearListener (callback: (event: Event) => never) {
listener = callback
}
}
}
}

const breadcrumbs: Breadcrumb[] = []

const plugin = new Plugin(Navigation)
const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] })
client.addOnBreadcrumb(breadcrumb => { breadcrumbs.push(breadcrumb) })

listener({ componentId: 1, componentName: 'Splash', passProps: {} })

expect(breadcrumbs.length).toBe(1)
expect(breadcrumbs[0].metadata).toStrictEqual({ to: 'Splash', from: undefined })
})
})

describe('plugin error handling', () => {
it('handles events without Navigation API gracefully', () => {
const message = '@bugsnag/plugin-react-native-navigation reference to `Navigation` was undefined'

expect(() => {
// eslint-disable-next-line no-new
new Plugin(undefined)
}).toThrow(new Error(message))
})

it('does not crash when Navigation.events is not available', () => {
// Create a Navigation object without the events method
const Navigation = {
events: () => ({
registerComponentDidAppearListener: jest.fn()
})
}

const plugin = new Plugin(Navigation)
const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] })

// Should not throw and client should be properly initialized
expect(client).toBeDefined()
})

it('handles missing Navigation object in plugin constructor', () => {
const message = '@bugsnag/plugin-react-native-navigation reference to `Navigation` was undefined'

expect(() => {
// eslint-disable-next-line no-new
new Plugin(null)
}).toThrow(new Error(message))
})

it('initializes plugin with valid Navigation API', () => {
const Navigation = {
events: () => ({
registerComponentDidAppearListener: jest.fn()
})
}

const plugin = new Plugin(Navigation)

// Plugin should be created successfully
expect(plugin).toBeDefined()
})
})
})
96 changes: 96 additions & 0 deletions scripts/react-native/android-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,102 @@ module.exports = {
fs.writeFileSync(mainActivityPath, mainActivityContents)
},
buildAPK: function buildAPK (fixtureDir, newArchEnabled) {
// Update Kotlin version to 1.9.22 to be compatible with bugsnag-android-core 6.25.0
// (compiled with Kotlin metadata version 1.9.0) and align JVM targets to 11 across
// all subprojects to prevent compilation mismatches (PLAT-15027)
const rootBuildGradlePath = `${fixtureDir}/android/build.gradle`
let rootBuildGradle = fs.readFileSync(rootBuildGradlePath, 'utf8')

// Determine if this is a newer RN version (0.84+) that uses Kotlin 2.x + AGP 9
// which handles JVM target alignment automatically
const rnVersion = parseFloat(process.env.RN_VERSION || '0')
const isKotlin2 = rnVersion >= 0.84

// Remove any existing subprojects block with afterEvaluate or pluginManager
rootBuildGradle = rootBuildGradle.replace(
/\nsubprojects\s*\{[\s\S]*?(afterEvaluate|pluginManager)[\s\S]*?\n\}\s*$/,
''
)

if (!isKotlin2) {
// For RN <=0.83 (Kotlin 1.x): Update Kotlin version to 1.9.22 and add JVM target alignment

// Update RNNKotlinVersion if present (react-native-navigation fixtures)
rootBuildGradle = rootBuildGradle.replace(
/RNNKotlinVersion\s*=\s*"[^"]+"/,
'RNNKotlinVersion = "1.9.22"'
)

// Update any direct kotlin-gradle-plugin version references in buildscript
rootBuildGradle = rootBuildGradle.replace(
/classpath\s*\(?["']org\.jetbrains\.kotlin:kotlin-gradle-plugin:[^"']+["']\)?/,
'classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")'
)

// Add subprojects block for JVM target alignment (Gradle <9 compatible)
// Use afterEvaluate to ensure sourceCompatibility is set before reading it,
// since pluginManager.withPlugin fires at plugin-apply time (before the android block)
rootBuildGradle += `
subprojects {
pluginManager.withPlugin('org.jetbrains.kotlin.android') {
afterEvaluate {
def javaVersion = android.compileOptions.sourceCompatibility.toString()
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = javaVersion
}
}
}
}
}
`
}
// For RN 0.84+ (Kotlin 2.x + AGP 9): No subprojects block needed.
// Kotlin 2.x automatically aligns JVM targets with the Android plugin.
// However, third-party libraries that apply kotlin-android plugin directly
// will fail with "sourceCompatibility is not yet finalized". Patch them
// to wrap the kotlin plugin in a try/catch and use compileSdkVersion method.
if (isKotlin2) {
const nodeModulesDir = `${fixtureDir}/node_modules`
if (fs.existsSync(nodeModulesDir)) {
const patchLibraryBuildGradle = (buildGradlePath) => {
if (!fs.existsSync(buildGradlePath)) return
let content = fs.readFileSync(buildGradlePath, 'utf8')

// Remove buildscript block — AGP 9 uses plugins from the root project
// and library buildscript blocks with old AGP versions cause conflicts
content = content.replace(
/buildscript\s*\{[\s\S]*?\n\}\s*\n/,
''
)

// Wrap kotlin-android plugin application in try/catch (handles both
// single and double quotes, and both 'kotlin-android' and full plugin id)
content = content.replace(
/apply plugin:\s*["'](?:org\.jetbrains\.kotlin\.android|kotlin-android)["']/g,
"try { apply plugin: 'org.jetbrains.kotlin.android' } catch (e) { /* Kotlin 2.x + AGP 9 compatibility */ }"
)

// Update Java compatibility to VERSION_17 for Kotlin 2.x
content = content.replace(
/JavaVersion\.VERSION_1_8/g,
'JavaVersion.VERSION_17'
)

fs.writeFileSync(buildGradlePath, content)
console.log(`Patched library build.gradle: ${buildGradlePath}`)
}

// Patch known third-party libraries that apply kotlin-android plugin directly
const libsToPatch = ['react-native-file-access', 'react-native-safe-area-context']
libsToPatch.forEach(lib => {
patchLibraryBuildGradle(`${nodeModulesDir}/${lib}/android/build.gradle`)
})
}
}

fs.writeFileSync(rootBuildGradlePath, rootBuildGradle)

if (newArchEnabled) {
execFileSync('./gradlew', ['generateCodegenArtifactsFromSchema'], { cwd: `${fixtureDir}/android`, stdio: 'inherit' })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}
}

apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
try {
apply plugin: 'org.jetbrains.kotlin.android'
} catch (e) {
// Kotlin 2.x + AGP 9 may fail here; the root project handles Kotlin config
}
if (isNewArchitectureEnabled()) {
apply plugin: 'com.facebook.react'
}
Expand All @@ -26,16 +20,18 @@ if (isNewArchitectureEnabled()) {
android {
buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
compileSdkVersion safeExtGet('compileSdkVersion', 28)

if (android.hasProperty('namespace')) {
namespace 'com.reactnative.scenarios'
}
namespace 'com.reactnative.scenarios'

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 28)
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

sourceSets {
main {
if (isNewArchitectureEnabled()) {
Expand Down
Loading
Loading