Skip to content

Commit 2abeaa3

Browse files
authored
fix(Android, Tabs): propagate actionOrigin into navigation state progression (#3996)
## Description `TabsContainer` (Android, gamma) was deciding several things off `isInExternalOperationContext` even though the real intent was to gate on the action's `actionOrigin`. The two were aligned only by accident — `isInExternalOperationContext == true` ⇔ `actionOrigin ∈ { PROGRAMMATIC_JS, PROGRAMMATIC_NATIVE }` — which made `PROGRAMMATIC_NATIVE` updates behave incorrectly on Android compared to iOS: 1. `progressNavigationState` did not have `actionOrigin` available, so consumers observing the progression could not distinguish user-driven from external/programmatic transitions. 2. `lastUINavState` was not updated for `PROGRAMMATIC_NATIVE`, even though that origin represents a native-authoritative state change (downstream library calling `submitSelectionOfTabsScreenWithKey`). This caused subsequent JS requests with stale `baseProvenance` to slip through `isNavigationStateStale` checks instead of being rejected. 3. The "prevent native selection" gate fired on `!isInExternalOperationContext`, which incidentally also rejected `PROGRAMMATIC_NATIVE` updates. Per intent (and matching iOS), this gate should reject only `USER` actions. This PR threads the resolved `actionOrigin` through the relevant code paths and switches each gate to test the origin directly. iOS and Android now agree on semantics. Side note: `isPreventNativeSelectionEnabled` is misnamed — it really means "prevent user selection". Renaming it deferred until a future major release. ## Changes - Compute `actionOrigin` once in `onMenuItemSelected`, before any gate that depends on it. - Thread `actionOrigin` through `updateSelectedFragment` and `progressNavigationState`. - `lastUINavState` now updates whenever `actionOrigin != PROGRAMMATIC_JS` (was: whenever `!isInExternalOperationContext`). - Prevent-native-selection gate now checks `actionOrigin == USER` (was: `!isInExternalOperationContext`). ## Test plan Manual reproduction on Android via `FabricExample`: 1. **`PROGRAMMATIC_NATIVE` no longer rejected by prevent-selection.** From a downstream-library code path (or a temporary call site) invoke `tabsContainer.submitSelectionOfTabsScreenWithKey(key)` on a `TabsScreen` whose `isPreventNativeSelectionEnabled = true`. Before: selection blocked, `onNavigationStateUpdatePrevented` emitted. After: selection applied, no prevention emitted. 2. **`lastUINavState` advances for `PROGRAMMATIC_NATIVE`.** After the above selection, dispatch a JS `navStateRequest` with the pre-native-update `baseProvenance`. Before: request applied (stale not detected). After: request rejected as stale via `isNavigationStateStale`. 3. **`actionOrigin` reaches observers during progression.** Register a `TabsNavigationStateObserver` and verify `actionOrigin` on `dispatchOnNativeStateChange` for each of: native tab tap (`USER`), `navStateRequest` from JS (`PROGRAMMATIC_JS`), `submitSelectionOfTabsScreenWithKey` (`PROGRAMMATIC_NATIVE`). 4. **Regression check.** Standard tab tap on a tab with `isPreventNativeSelectionEnabled = true` is still prevented and emits `onNavigationStateUpdatePrevented`. No automated tests exist for this path yet; recommend adding one matrix test in a follow-up. ## Checklist - [ ] Included code example that can be used to test this change. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes
1 parent 46036b3 commit 2abeaa3

1 file changed

Lines changed: 21 additions & 13 deletions

File tree

  • android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,10 @@ class TabsContainer internal constructor(
474474
}
475475
}
476476

477-
private fun updateSelectedFragment(nextSelectedFragment: TabsScreenFragment): Boolean {
477+
private fun updateSelectedFragment(
478+
nextSelectedFragment: TabsScreenFragment,
479+
actionOrigin: TabsActionOrigin,
480+
): Boolean {
478481
if (navState.isEmpty()) {
479482
check(isInExternalOperationContext && pendingStateUpdateRequest != null)
480483
navState = TabsNavigationState(nextSelectedFragment.requireScreenKey, 0)
@@ -488,11 +491,11 @@ class TabsContainer internal constructor(
488491
val currentSelectedFragment = selectedTab
489492

490493
if (nextSelectedFragment === currentSelectedFragment) {
491-
progressNavigationState(navState.selectedScreenKey)
494+
progressNavigationState(navState.selectedScreenKey, actionOrigin)
492495
return true
493496
}
494497

495-
progressNavigationState(nextSelectedFragment.requireScreenKey)
498+
progressNavigationState(nextSelectedFragment.requireScreenKey, actionOrigin)
496499
requireFragmentManager
497500
.createTransactionWithReordering()
498501
.let {
@@ -503,9 +506,12 @@ class TabsContainer internal constructor(
503506
return true
504507
}
505508

506-
private fun progressNavigationState(selectedScreenKey: String) {
509+
private fun progressNavigationState(
510+
selectedScreenKey: String,
511+
actionOrigin: TabsActionOrigin,
512+
) {
507513
navState = TabsNavigationState(selectedScreenKey, navState.provenance + 1)
508-
if (!isInExternalOperationContext) {
514+
if (actionOrigin != TabsActionOrigin.PROGRAMMATIC_JS) {
509515
lastUINavState = navState
510516
}
511517
}
@@ -521,13 +527,20 @@ class TabsContainer internal constructor(
521527

522528
val isRepeated = nextSelectedFragment === currSelectedFragment
523529

530+
val actionOrigin =
531+
if (isInExternalOperationContext) {
532+
requirePendingStateUpdateRequest().actionOrigin
533+
} else {
534+
TabsActionOrigin.USER
535+
}
536+
524537
// If this is user action we test whether it should be prevented before we progress the state.
525-
if (!isRepeated && !isInExternalOperationContext && nextSelectedFragment.isPreventNativeSelectionEnabled) {
538+
if (!isRepeated && actionOrigin == TabsActionOrigin.USER && nextSelectedFragment.isPreventNativeSelectionEnabled) {
526539
observerRegistry.emitOnNavigationStateUpdatePrevented(navState, nextSelectedFragment.requireScreenKey)
527540
return false
528541
}
529542

530-
val stateChanged = updateSelectedFragment(nextSelectedFragment)
543+
val stateChanged = updateSelectedFragment(nextSelectedFragment, actionOrigin)
531544

532545
val hasTriggeredSpecialEffect =
533546
if (isRepeated) specialEffectsHandler.handleRepeatedTabSelection() else false
@@ -537,12 +550,7 @@ class TabsContainer internal constructor(
537550
navState,
538551
isRepeated = isRepeated,
539552
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
540-
actionOrigin =
541-
if (isInExternalOperationContext) {
542-
requirePendingStateUpdateRequest().actionOrigin
543-
} else {
544-
TabsActionOrigin.USER
545-
},
553+
actionOrigin = actionOrigin,
546554
)
547555
}
548556

0 commit comments

Comments
 (0)