managarten/apps-archived/maerchenzauber/apps/mobile/docs/DEEPLINK_CRASH_FIX.md
Till-JS 61d181fbc2 chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace:
- bauntown (community website)
- maerchenzauber (AI story generation)
- memoro (voice memo app)
- news (news aggregation)
- nutriphi (nutrition tracking)
- reader (reading app)
- uload (URL shortener)
- wisekeep (AI wisdom extraction)

Update CLAUDE.md documentation:
- Add presi to active projects
- Document archived projects section
- Update workspace configuration

Archived apps can be re-activated by moving back to apps/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 07:03:59 +01:00

19 KiB

iOS Deeplink Crash Fix - Complete Debugging Journey

Date: 2025-11-02 Platform: iOS 26.0.1 (iPhone 13) App: Märchenzauber v1.1.0 Issue: App crashes when opening character sharing deeplinks

Executive Summary

The app was crashing when users opened deeplinks to shared characters (maerchenzauber://share/character/{id}). What appeared to be a single issue was actually 5 distinct crashes that occurred sequentially during navigation. This document details the complete debugging journey, dead ends, and solutions.


The Problem

Initial Symptoms

  • App crashes ~1 second after opening a character sharing deeplink
  • Only happens on physical iOS devices (not simulator)
  • User sees "Opening character..." loading screen, then crash
  • No obvious error message in logs before crash
1. User clicks: maerchenzauber://share/character/{id}
2. App opens: app/share/character/[id].tsx
3. Should redirect to: app/character/preview/[characterId].tsx
4. Should display: Character preview screen
5. Actually: ❌ CRASH

The Debugging Journey

Crash #1: LinearGradient Rendering (FALSE LEAD)

Symptoms:

Thread 0 (Main):
RNSScreen setViewToSnapshot + 1416
UIKit snapshotViewAfterScreenUpdates + 148

Initial Diagnosis: WRONG Misdiagnosed as LinearGradient rendering issue based on Stack Overflow comment.

Action Taken:

  • Updated expo-linear-gradient from 14.0.1 → 15.0.7
  • Added explicit layout bounds to all LinearGradient components
  • Added minHeight: 100 to login screen gradient

Result: Crash still occurred (but we fixed compatibility issues)

Learning: Don't trust Stack Overflow comments blindly - analyze the actual crash stack trace.


Crash #2: CoreGraphics Image Deallocation

Symptoms:

Thread 0 (Main):
CoreGraphics vm_allocator_deallocate + 44
CoreGraphics image_finalize + 60
QuartzCore CA::Layer::~Layer() + 328

Diagnosis: CORRECT Image loading race condition during rapid navigation - images being deallocated while CoreGraphics still rendering them.

Root Cause:

<Image
  source={{ uri: currentImage }}
  transition={300}  // ❌ Transition conflicts with navigation
/>

Solution:

<Image
  source={{ uri: currentImage }}
  transition={0}  // ✅ Disable transitions
  recyclingKey={characterId}  // ✅ Help expo-image reuse instances
  cachePolicy="memory-disk"  // ✅ Proper caching
/>

Files Modified:

  • app/character/preview/[characterId].tsx:360

Result: Fixed image crashes, but navigation still crashed.


Crash #3: RNSScreen Snapshot on Share Route

Symptoms:

Thread 0 (Main):
RNSScreen setViewToSnapshot + 1416
react-native-screens snapshot creation

Diagnosis: CORRECT The /share/ route wasn't inheriting the root Stack configuration that disables snapshots.

Root Cause:

app/
├── _layout.tsx          // Has freezeOnBlur: false
└── share/
    ├── _layout.tsx      // ❌ MISSING - needs its own config
    └── character/
        └── [id].tsx

Solution 1: Created dedicated Stack layout for share route

// app/share/_layout.tsx
export default function ShareLayout() {
  return (
    <Stack
      screenOptions={{
        headerShown: false,
        animation: Platform.OS === 'ios' ? 'none' : 'default',
        freezeOnBlur: false,  // ✅ Prevent snapshots
        presentation: 'transparentModal',  // ✅ Bypass native screens
      }}
    />
  );
}

Solution 2: Bypassed screen rendering entirely

// app/share/character/[id].tsx - BEFORE
export default function ShareCharacterDeeplink() {
  const [loading, setLoading] = useState(true);
  return loading ? <LoadingScreen /> : <Redirect />;  // ❌ Creates snapshot
}

// AFTER
export default function ShareCharacterDeeplink() {
  return <Redirect href={`/character/preview/${id}`} />;  // ✅ No snapshot
}

Files Created/Modified:

  • Created: app/share/_layout.tsx
  • Modified: app/share/character/[id].tsx

Result: Snapshot crashes resolved, but app still crashed!


Crash #4: THE ACTUAL LinearGradient Crash

Symptoms:

Thread 0 (Main):
5   CoreGraphics    CGContextDrawLinearGradient + 236
6   Mrchenzauber    LinearGradientLayer.draw(in:) + 932
7   Mrchenzauber    LinearGradientLayer.display() + 420

Diagnosis: CORRECT (Finally!) LinearGradient components using width: '100%' had undefined dimensions during navigation before parent layout was fully calculated.

Root Cause:

// During navigation, this sequence happens:
1. Component mounts
2. LinearGradient tries to render
3. Parent layout hasn't resolved yet
4. width: '100%' = undefined/0/NaN
5. CoreGraphics crashes trying to render gradient with invalid dimensions

Why This Happens During Deeplinks:

  • Deeplink navigation bypasses normal React Native navigation lifecycle
  • Components mount before layout calculations complete
  • Normal navigation has timing that masks this issue

Solution Pattern:

// ❌ BEFORE - Crash during navigation
const styles = StyleSheet.create({
  gradientStyle: {
    width: '100%', // Can be undefined during layout
    justifyContent: 'center',
  },
});

// ✅ AFTER - Safe during navigation
const styles = StyleSheet.create({
  gradientStyle: {
    alignSelf: 'stretch', // More reliable than width: '100%'
    minWidth: 200, // Ensures CoreGraphics has valid dimensions
    justifyContent: 'center',
  },
});

Files Fixed (3 components):

  1. app/login.tsx:1141 - manaInfoBanner

    manaInfoBanner: {
      alignSelf: 'stretch',  // Changed from width: '100%'
      minWidth: 200,
      minHeight: 100,
    }
    

    Also removed locations={[0, 0.7, 1]} prop for simpler gradient distribution.

  2. components/story/EndScreen.tsx:280 - buttonGradient

    buttonGradient: {
      alignSelf: 'stretch',  // Changed from width: '100%'
      minWidth: 200,
    }
    
  3. components/molecules/PremiumCuddlyToyCard.tsx:119 - gradientBackground

    gradientBackground: {
      alignSelf: 'stretch',  // Changed from width: '100%'
      minWidth: 200,
    }
    

Result: CoreGraphics crashes resolved!


Crash #5: MagicalLoadingScreen Missing Prop

Symptoms:

ERROR  [TypeError: Cannot convert undefined value to object]
Code: MagicalLoadingScreen.tsx:173
{phrases[currentPhrase]}

Diagnosis: CORRECT Simple prop missing - MagicalLoadingScreen requires context prop to determine which phrases to show.

Root Cause:

// app/character/preview/[characterId].tsx:315
return <MagicalLoadingScreen />;  // ❌ Missing context prop

// This caused:
const phrases = magicalPhrases[undefined];  // undefined
phrases[currentPhrase]  // ❌ Cannot convert undefined to object

Solution:

return <MagicalLoadingScreen context="character" />;  // ✅ Fixed

Files Modified:

  • app/character/preview/[characterId].tsx:315

Result: All crashes resolved!


Date: 2025-11-04 Context: Universal links implementation for character sharing

Symptoms:

Thread 0 (Main):
9   Mrchenzauber     LinearGradientLayer.display() + 64  ← CRASH
3   CoreGraphics     ripc_DrawShading + 6864

Diagnosis: CORRECT Another LinearGradient component with width: '100%' and height: '100%' causing the same crash pattern as Crash #4, but this time in a different component during app startup.

Root Cause:

// components/atoms/TimeOfDayBackground.tsx:66-74
gradient: {
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  width: '100%',  // ❌ Caused crash during navigation
  height: '100%', // ❌ Caused crash during navigation
}

Why This Crashed: When a component has position: 'absolute' with all four edges defined (top, left, right, bottom), the width and height properties are redundant. During rapid navigation or startup, these percentage-based dimensions can be undefined/0/NaN before parent layout is calculated, causing CoreGraphics to crash.

Solution:

// components/atoms/TimeOfDayBackground.tsx:66-74
gradient: {
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  // Removed width/height - absolute positioning with all edges defined
  // is more reliable and prevents CoreGraphics crashes during navigation
}

Files Modified:

  • components/atoms/TimeOfDayBackground.tsx:66-74

Result: App startup crashes resolved!

Learning: When absolutely positioning an element with all edges defined, NEVER add width or height properties. The layout system automatically calculates dimensions from the edge constraints.


Issue #7: Expo Router Duplicate Screen Names

Date: 2025-11-04 Context: Universal links route configuration

Symptoms:

ERROR: A navigator cannot contain multiple 'Screen' components
with the same name (found duplicate screen named 'character/[id]')

Diagnosis: CORRECT Expo-router was finding duplicate route definitions due to having both a file and a directory with the same dynamic segment name.

Root Cause:

app/
├── character/
│   ├── [id].tsx              ← Screen: character/[id]
│   └── [id]/                 ← CONFLICT! Also creates character/[id]
│       └── [shareCode].tsx

And similarly:

app/share/character/
├── [id].tsx                  ← Screen: share/character/[id]
└── [id]/                     ← CONFLICT! Also creates share/character/[id]
    └── [shareCode].tsx

Why This Happens: Expo-router generates screen names from file paths. When you have both:

  • A file: character/[id].tsx
  • A directory: character/[id]/

Both register as the same screen name character/[id], causing a conflict.

Solution: Remove the unnecessary file routes and keep only the nested routes:

app/
├── character/
│   └── [id].tsx              ← Character detail screen (kept)
└── share/character/
    └── [id]/                 ← Directory for deep link routes
        ├── _layout.tsx       ← Crash prevention layout
        └── [shareCode].tsx   ← Deep link handler (kept)

Files Removed:

  • Deleted: app/character/[id]/[shareCode].tsx (duplicate)
  • Deleted: app/share/character/[id].tsx (unnecessary)

Important: After removing route files, you MUST clear Metro cache:

rm -rf .expo node_modules/.cache
watchman watch-del-all
npx expo start --clear

Result: Route conflicts resolved!

Learning:

  1. Never have both a file and directory with the same dynamic segment name
  2. Metro aggressively caches route configuration - always clear cache after route changes
  3. Use watchman watch-del-all to clear file system watch caches

Summary of Solutions

Issue Root Cause Solution Files
Image crashes Transition animation conflicts with navigation Disable transitions, add recycling keys character/preview/[characterId].tsx
Snapshot crashes Share route missing Stack config Created share/_layout.tsx with snapshot prevention share/_layout.tsx (new)
Navigation crashes Loading screen triggers snapshot creation Use immediate <Redirect> instead share/character/[id].tsx
LinearGradient crashes width: '100%' undefined during layout Use alignSelf: 'stretch' + minWidth login.tsx, EndScreen.tsx, PremiumCuddlyToyCard.tsx
Loading screen crash Missing required prop Add context="character" prop character/preview/[characterId].tsx
TimeOfDayBackground crash width/height: '100%' on absolute positioned gradient Remove width/height, use edge constraints only TimeOfDayBackground.tsx
Route naming conflict Duplicate screen names in expo-router Remove conflicting file routes, clear Metro cache Route structure cleanup

Key Technical Learnings

1. React Native Navigation Lifecycle

  • Deeplinks bypass normal navigation timing
  • Components can mount before layout calculations complete
  • Always ensure components handle undefined dimensions gracefully

2. expo-router File-Based Routing

  • Each directory can have its own _layout.tsx
  • Child layouts don't automatically inherit parent Stack settings
  • Use freezeOnBlur: false to prevent snapshot crashes

3. react-native-screens Snapshots

  • iOS creates snapshots during screen transitions for animations
  • Snapshots can fail when:
    • Views have complex gradients/blurs during rapid navigation
    • Layout isn't fully resolved
    • Images are loading/unloading
  • Solutions:
    • freezeOnBlur: false - Disable freezing
    • animation: 'none' - Disable animations
    • presentation: 'transparentModal' - Bypass native screens
    • Use <Redirect> to avoid rendering intermediate screens

4. expo-linear-gradient Rendering

  • width: '100%' can be undefined during navigation
  • alignSelf: 'stretch' is more reliable
  • Always provide minimum dimensions for CoreGraphics:
    minWidth: 200,  // Ensures valid render bounds
    minHeight: 100
    
  • Avoid locations prop unless necessary - simpler is safer

5. expo-image Best Practices for Navigation

<Image
  source={{ uri }}
  transition={0}  // Disable for rapid navigation
  recyclingKey={uniqueId}  // Help expo-image reuse instances
  cachePolicy="memory-disk"  // Proper caching
/>

6. Debugging Complex Issues

  • One symptom can have multiple root causes
  • Fix each issue, test, document what changed
  • Don't assume first diagnosis is correct
  • Stack traces show where it crashes, not always why

Testing Checklist

After implementing fixes, test:

  • Cold start deeplink (app not running)
  • Warm start deeplink (app in background)
  • Multiple deeplinks in succession
  • Slow network conditions
  • Physical iOS device (not just simulator)
  • Different iOS versions if possible

Prevention for Future Development

Code Review Guidelines

  1. Never use width: '100%' with LinearGradient

    // ❌ AVOID
    <LinearGradient style={{ width: '100%' }} />
    
    // ✅ USE
    <LinearGradient style={{ alignSelf: 'stretch', minWidth: 200 }} />
    
  2. Never use width/height on absolute positioned elements with all edges defined

    // ❌ AVOID
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      width: '100%',  // Redundant and can cause crashes
      height: '100%', // Redundant and can cause crashes
    }}
    
    // ✅ USE
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      // Width/height automatically calculated from edges
    }}
    
  3. Always provide context prop to MagicalLoadingScreen

    // ❌ AVOID
    <MagicalLoadingScreen />
    
    // ✅ USE
    <MagicalLoadingScreen context="character" />
    
  4. Disable image transitions for navigation screens

    // ❌ RISKY
    <Image transition={300} />
    
    // ✅ SAFE
    <Image transition={0} recyclingKey={id} />
    
  5. Configure Stack settings in every _layout.tsx

    export default function Layout() {
      return (
        <Stack
          screenOptions={{
            freezeOnBlur: false,  // Prevent snapshots
            animation: Platform.OS === 'ios' ? 'default' : 'slide_from_right',
          }}
        />
      );
    }
    
  6. Avoid expo-router file/directory naming conflicts

    // ❌ AVOID - Creates duplicate screens
    app/
    ├── character/
       ├── [id].tsx         Screen: character/[id]
       └── [id]/            CONFLICT: Also character/[id]
           └── detail.tsx
    
    // ✅ USE - Clear separation
    app/
    ├── character/
       └── [id].tsx         Screen: character/[id]
    └── character-detail/
        └── [id]/            Screen: character-detail/[id]
            └── detail.tsx
    
  7. Always clear Metro cache after route changes

    # After adding/removing route files:
    rm -rf .expo node_modules/.cache
    watchman watch-del-all
    npx expo start --clear
    

Automated Testing

Add E2E test for deeplink navigation:

describe('Deeplink Navigation', () => {
  it('should open character preview without crashing', async () => {
    await device.launchApp({
      url: 'maerchenzauber://share/character/test-id',
    });
    await expect(element(by.id('character-preview'))).toBeVisible();
  });
});

Modified Files

  • app/_layout.tsx - Navigation timeout increased
  • app/login.tsx - LinearGradient dimensions fixed
  • app/character/preview/[characterId].tsx - Image transitions disabled, MagicalLoadingScreen prop added
  • components/story/EndScreen.tsx - LinearGradient dimensions fixed
  • components/molecules/PremiumCuddlyToyCard.tsx - LinearGradient dimensions fixed
  • package.json - Updated expo-linear-gradient to 15.0.7

Created Files

  • app/share/_layout.tsx - Stack configuration for share routes
  • docs/DEEPLINK_CRASH_FIX.md - This document

References


Conclusion

What seemed like a single crash was actually 7 distinct issues across two debugging sessions:

Session 1 (2025-11-02):

  1. Image transition race conditions
  2. Missing Stack layout configuration
  3. Snapshot creation during intermediate screens
  4. LinearGradient rendering with undefined dimensions
  5. Missing required component prop

Session 2 (2025-11-04 - Universal Links): 6. TimeOfDayBackground LinearGradient crash (same pattern as #4) 7. Expo-router duplicate screen names

The key to solving this was:

  • Methodical testing after each fix
  • Documenting each crash with stack traces
  • Not assuming the first diagnosis was correct
  • Understanding the React Native/iOS rendering pipeline
  • Aggressive cache clearing when changing routes

Total time: ~6 hours of debugging (across 2 sessions) Total issues fixed: 7 Final result: Stable deeplink navigation on iOS + Universal links working