Skip to content

Refactor AmicusFab paywall routing to use callback instead of getParent().navigate() #1563

@CraigBuckmaster

Description

@CraigBuckmaster

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:

  1. A parent navigator exists above this one (coupling to tree shape)
  2. The target lives in a tab literally named 'AmicusTab' (coupling to string id)
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions