Problem
AmicusFab.handlePress currently reaches up through the navigator tree to open the paywall:
// app/src/components/amicus/AmicusFab.tsx
const parent = navigation?.getParent<NavigationProp<ParamListBase>>();
parent?.navigate('AmicusTab', { screen: 'Paywall' });
This bakes three brittle assumptions into a leaf component:
- A parent navigator exists above this one (coupling to tree shape)
- The target lives in a tab literally named
'AmicusTab' (coupling to string id)
- The target is a screen named
'Paywall' nested inside that tab (coupling to inner nav structure)
If we ever restructure navigators (flatten tabs, add a stack above, rename), getParent() silently returns a different node and the optional chaining swallows the failure — no TS error, no runtime error, just a button that doesn't work.
Proposed fix
Invert control: have AmicusFab emit an intent, let the parent (AppShell) handle the navigation.
// AmicusFab.tsx
interface Props {
onOpenPaywall?: () => void;
}
const handlePress = useCallback(() => {
if (access.reason === 'not_premium') {
onOpenPaywall?.();
return;
}
setPeekOpen(true);
}, [access.reason, onOpenPaywall]);
// App.tsx
<AmicusFab
onOpenPaywall={() => navigationRef.navigate('AmicusTab', { screen: 'Paywall' })}
/>
The FAB now just says "someone wants the paywall." AppShell owns how. If navigators get restructured, one call site in App.tsx changes instead of hunting down getParent() calls in leaf components.
Scope
In scope:
- Add
onOpenPaywall?: () => void prop to AmicusFab
- Replace the
getParent().navigate() call with onOpenPaywall?.()
- Wire the callback from
AppShell in app/App.tsx using navigationRef.navigate with proper TypeScript typing (no as unknown as casts — use TabParamList properly)
- Update
AmicusFab.test.tsx to pass onOpenPaywall in the non-premium test case
Out of scope (deliberately):
- Removing the
useNavigation() try/catch — keep the defensive guard until we have evidence from a rentamac session that it's not load-bearing (see separate card)
- Prop-drilling
navigationState through to AmicusPeekSheet — leave useNavigationState() hook in place
- Any MapLibre or MapScreen refactor
Why separate card
PR #1562 attempted this alongside several other changes (MapLibre patch rewrite with worse docs, try/catch removal, nav-state prop drilling, regression test deletion, MapScreen helper extraction). The entangled scope made cherry-picking messier than a clean rewrite. This card is the one genuinely-good idea from that PR, isolated.
Acceptance criteria
AmicusFab.tsx contains no reference to getParent or navigate for paywall routing
App.tsx provides onOpenPaywall callback using navigationRef directly, properly typed against TabParamList
- Existing non-premium paywall-opens test passes with the callback approach
- No regression in existing
renders null when mounted outside NavigationContainer test (keep master's try/catch for now)
- Lint + type-check clean
Files touched
app/src/components/amicus/AmicusFab.tsx — add prop, swap call site
app/App.tsx — wire callback
app/src/components/amicus/__tests__/AmicusFab.test.tsx — update non-premium test
References
Problem
AmicusFab.handlePresscurrently reaches up through the navigator tree to open the paywall:This bakes three brittle assumptions into a leaf component:
'AmicusTab'(coupling to string id)'Paywall'nested inside that tab (coupling to inner nav structure)If we ever restructure navigators (flatten tabs, add a stack above, rename),
getParent()silently returns a different node and the optional chaining swallows the failure — no TS error, no runtime error, just a button that doesn't work.Proposed fix
Invert control: have AmicusFab emit an intent, let the parent (AppShell) handle the navigation.
The FAB now just says "someone wants the paywall." AppShell owns how. If navigators get restructured, one call site in App.tsx changes instead of hunting down
getParent()calls in leaf components.Scope
In scope:
onOpenPaywall?: () => voidprop toAmicusFabgetParent().navigate()call withonOpenPaywall?.()AppShellinapp/App.tsxusingnavigationRef.navigatewith proper TypeScript typing (noas unknown ascasts — useTabParamListproperly)AmicusFab.test.tsxto passonOpenPaywallin the non-premium test caseOut of scope (deliberately):
useNavigation()try/catch — keep the defensive guard until we have evidence from a rentamac session that it's not load-bearing (see separate card)navigationStatethrough toAmicusPeekSheet— leaveuseNavigationState()hook in placeWhy separate card
PR #1562 attempted this alongside several other changes (MapLibre patch rewrite with worse docs, try/catch removal, nav-state prop drilling, regression test deletion, MapScreen helper extraction). The entangled scope made cherry-picking messier than a clean rewrite. This card is the one genuinely-good idea from that PR, isolated.
Acceptance criteria
AmicusFab.tsxcontains no reference togetParentornavigatefor paywall routingApp.tsxprovidesonOpenPaywallcallback usingnavigationRefdirectly, properly typed againstTabParamListrenders null when mounted outside NavigationContainertest (keep master's try/catch for now)Files touched
app/src/components/amicus/AmicusFab.tsx— add prop, swap call siteapp/App.tsx— wire callbackapp/src/components/amicus/__tests__/AmicusFab.test.tsx— update non-premium testReferences