npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ridwan-retainer/app-shell

v0.1.1

Published

Foundational runtime shell for app lifecycle, providers, and navigation

Downloads

188

Readme

@ridwan-retainer/app-shell

A comprehensive, production-ready application runtime framework for React Native apps. Provides lifecycle management, state management, navigation, error handling, remote configuration, and more out of the box.

npm version License: MIT

📋 Table of Contents

✨ Features

Core Features

  • 🔄 Lifecycle Management: Comprehensive app lifecycle hooks and state management
  • 🏪 State Management: Built-in Zustand stores for theme, settings, feature flags, and telemetry
  • 🧭 Navigation: Type-safe React Navigation integration with deep linking
  • ❌ Error Handling: Global error boundaries with Sentry integration
  • ⏳ Loading States: Sophisticated loading and splash screen management
  • 🔧 Remote Config: Firebase Remote Config integration for feature flags
  • 🔌 Adapter System: Plugin architecture for extending functionality
  • 📦 Provider Composition: Clean provider nesting and composition
  • 📝 TypeScript: Full type safety throughout
  • ⚡ Performance: Optimized rendering and state updates
  • ✅ Well-tested: Extensive test coverage

State Stores

  • Theme: Dark/light mode with custom theme support
  • Settings: App-wide settings persistence
  • Feature Flags: Runtime feature toggles
  • Telemetry: Usage tracking and analytics

📦 Installation

npm install @ridwan-retainer/app-shell

Required Peer Dependencies

npm install @react-navigation/native \
            react-native-screens \
            react-native-safe-area-context \
            react \
            react-native

Optional Peer Dependencies

For full functionality:

# For AsyncStorage persistence
npm install @react-native-async-storage/async-storage

# For Remote Config
npm install @react-native-firebase/app @react-native-firebase/remote-config

# For Error Tracking
npm install @sentry/react-native

# For Splash Screens
npm install react-native-bootsplash

🚀 Quick Start

Basic Setup

import { LifecycleProvider } from '@ridwan-retainer/app-shell';
import { NavigationContainer } from '@react-navigation/native';

function App() {
  return (
    <LifecycleProvider
      onInitialize={async () => {
        // Initialize your app (API clients, analytics, etc.)
        console.log('App initializing...');
      }}
      onReady={() => {
        // App is ready to use
        console.log('App ready!');
      }}
    >
      <NavigationContainer>
        <YourAppContent />
      </NavigationContainer>
    </LifecycleProvider>
  );
}

With Error Boundary

import {
  LifecycleProvider,
  ErrorBoundary,
  ErrorFallback,
} from '@ridwan-retainer/app-shell';

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, stackTrace) => {
        console.error('App error:', error, stackTrace);
        // Report to error tracking service
      }}
    >
      <LifecycleProvider>
        <YourApp />
      </LifecycleProvider>
    </ErrorBoundary>
  );
}

With All Features

import {
  LifecycleProvider,
  ErrorBoundary,
  LoadingProvider,
  RemoteConfigProvider,
  initializeSentry,
} from '@ridwan-retainer/app-shell';

// Initialize Sentry
initializeSentry({
  dsn: 'your-sentry-dsn',
  environment: __DEV__ ? 'development' : 'production',
});

function App() {
  return (
    <ErrorBoundary>
      <LifecycleProvider
        onInitialize={async () => {
          // Load critical data
          await loadAuthState();
          await initializeAnalytics();
        }}
      >
        <RemoteConfigProvider>
          <LoadingProvider>
            <NavigationContainer>
              <YourApp />
            </NavigationContainer>
          </LoadingProvider>
        </RemoteConfigProvider>
      </LifecycleProvider>
    </ErrorBoundary>
  );
}

🏗 Architecture

Provider Hierarchy

ErrorBoundary (Top-level error catching)
  └─ LifecycleProvider (App lifecycle management)
      └─ RemoteConfigProvider (Feature flags & config)
          └─ LoadingProvider (Loading states & splash)
              └─ NavigationContainer (React Navigation)
                  └─ Your App Content

Data Flow

User Action → State Update → Zustand Store → Component Re-render
               ↓
         Side Effects (Persistence, Analytics, etc.)

📚 API Reference

Lifecycle Management

<LifecycleProvider />

Manages app lifecycle and initialization.

import { LifecycleProvider } from '@ridwan-retainer/app-shell';

<LifecycleProvider
  onInitialize={async () => {
    // Run once on app start
    await loadUserData();
    await connectWebSocket();
  }}
  onReady={() => {
    // Called when initialization completes
    console.log('App is ready!');
  }}
  onBackground={() => {
    // App moved to background
    saveCurrentState();
  }}
  onForeground={() => {
    // App returned to foreground
    refreshData();
  }}
>
  {children}
</LifecycleProvider>

Props:

{
  onInitialize?: () => Promise<void> | void;
  onReady?: () => void;
  onBackground?: () => void;
  onForeground?: () => void;
  onError?: (error: Error) => void;
  children: ReactNode;
}

useAppState()

Monitor app foreground/background state.

import { useAppState } from '@ridwan-retainer/app-shell';

function DataSync() {
  const appState = useAppState();

  useEffect(() => {
    if (appState === 'active') {
      syncData();
    }
  }, [appState]);

  return null;
}

Returns: 'active' | 'background' | 'inactive'

useStartupType()

Determine if app is cold start or warm start.

import { useStartupType } from '@ridwan-retainer/app-shell';

function Analytics() {
  const startupType = useStartupType();

  useEffect(() => {
    logEvent('app_start', { type: startupType });
  }, []);
}

Returns: 'cold' | 'warm'

LifecycleManager

Programmatic lifecycle control.

import { LifecycleManager } from '@ridwan-retainer/app-shell';

// Get lifecycle instance
const lifecycle = LifecycleManager.getInstance();

// Listen to events
lifecycle.on('initialize', async () => {
  await setupApp();
});

lifecycle.on('ready', () => {
  console.log('App ready');
});

// Trigger lifecycle phase
await lifecycle.initialize();

State Management

All stores are built with Zustand and include:

  • Type-safe actions and selectors
  • Automatic persistence (where applicable)
  • Optimized re-renders

Theme Store

import { useTheme, useThemeActions } from '@ridwan-retainer/app-shell';

function ThemeToggle() {
  const theme = useTheme();
  const { toggleTheme, setTheme } = useThemeActions();

  return (
    <View>
      <Text>Current theme: {theme.mode}</Text>
      <Button title="Toggle Theme" onPress={toggleTheme} />
      <Button title="Dark Mode" onPress={() => setTheme('dark')} />
      <Button title="Light Mode" onPress={() => setTheme('light')} />
    </View>
  );
}

Theme Type:

{
  mode: 'light' | 'dark';
  colors: {
    primary: string;
    background: string;
    text: string;
    border: string;
    // ... more colors
  };
}

Custom Theme:

import { useThemeActions } from '@ridwan-retainer/app-shell';

const customTheme = {
  mode: 'dark',
  colors: {
    primary: '#FF6B6B',
    background: '#1A1A1A',
    text: '#FFFFFF',
    // ...
  },
};

const { setCustomTheme } = useThemeActions();
setCustomTheme(customTheme);

Settings Store

import {
  useSettings,
  useSettingsActions,
} from '@ridwan-retainer/app-shell';

function SettingsScreen() {
  const settings = useSettings();
  const { updateSetting, resetSettings } = useSettingsActions();

  return (
    <View>
      <Switch
        value={settings.notifications}
        onValueChange={(value) =>
          updateSetting('notifications', value)
        }
      />
      <Switch
        value={settings.analytics}
        onValueChange={(value) =>
          updateSetting('analytics', value)
        }
      />
      <Button title="Reset" onPress={resetSettings} />
    </View>
  );
}

Built-in Settings:

{
  notifications: boolean;
  analytics: boolean;
  [key: string]: any; // Custom settings
}

Feature Flags Store

import {
  useFeatureFlag,
  useFeatureFlagActions,
} from '@ridwan-retainer/app-shell';

function NewFeature() {
  const isEnabled = useFeatureFlag('new_feature');
  const { setFlag } = useFeatureFlagActions();

  // Enable for testing
  useEffect(() => {
    if (__DEV__) {
      setFlag('new_feature', true);
    }
  }, []);

  if (!isEnabled) return null;

  return <NewFeatureComponent />;
}

Actions:

{
  setFlag: (key: string, value: boolean) => void;
  setFlags: (flags: Record<string, boolean>) => void;
  resetFlags: () => void;
}

Telemetry Store

import {
  useTelemetry,
  useTelemetryActions,
} from '@ridwan-retainer/app-shell';

function ScreenView({ screenName }: { screenName: string }) {
  const { trackEvent } = useTelemetryActions();

  useEffect(() => {
    trackEvent('screen_view', { screen: screenName });
  }, [screenName]);

  return null;
}

// Usage
function ProfileScreen() {
  return (
    <>
      <ScreenView screenName="profile" />
      <ProfileContent />
    </>
  );
}

Telemetry Actions:

{
  trackEvent: (name: string, properties?: Record<string, any>) => void;
  trackScreen: (screenName: string) => void;
  setUserId: (userId: string) => void;
  clearUserId: () => void;
}

Navigation

<NavigationContainer />

Type-safe navigation container wrapper.

import { NavigationContainer } from '@ridwan-retainer/app-shell';

type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Settings: undefined;
};

function App() {
  return (
    <NavigationContainer<RootStackParamList>
      linking={{
        prefixes: ['myapp://'],
        config: {
          screens: {
            Home: 'home',
            Profile: 'profile/:userId',
            Settings: 'settings',
          },
        },
      }}
      onReady={() => {
        console.log('Navigation ready');
      }}
    >
      <RootNavigator />
    </NavigationContainer>
  );
}

useTypedNavigation()

Type-safe navigation hook.

import { useTypedNavigation } from '@ridwan-retainer/app-shell';

type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
};

function HomeScreen() {
  const navigation = useTypedNavigation<RootStackParamList>();

  const goToProfile = (userId: string) => {
    // Type-safe: TypeScript knows userId is required
    navigation.navigate('Profile', { userId });
  };

  return <Button title="View Profile" onPress={() => goToProfile('123')} />;
}

useTypedRoute()

Type-safe route params hook.

import { useTypedRoute } from '@ridwan-retainer/app-shell';

type ProfileScreenParams = {
  userId: string;
  tab?: 'posts' | 'photos';
};

function ProfileScreen() {
  const route = useTypedRoute<ProfileScreenParams>();

  const { userId, tab = 'posts' } = route.params;

  return (
    <View>
      <Text>User ID: {userId}</Text>
      <Text>Active Tab: {tab}</Text>
    </View>
  );
}

navigationRef

Programmatic navigation without hooks.

import { navigationRef } from '@ridwan-retainer/app-shell';

// Navigate from anywhere (e.g., push notification handler)
function handleNotificationPress(notificationData: any) {
  if (navigationRef.isReady()) {
    navigationRef.navigate('Notification', {
      id: notificationData.id,
    });
  }
}

Error Handling

<ErrorBoundary />

Catch and handle React errors.

import { ErrorBoundary, ErrorFallback } from '@ridwan-retainer/app-shell';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, errorInfo) => {
    // Log to error tracking service
    Sentry.captureException(error, { extra: errorInfo });
  }}
  onReset={() => {
    // Reset app state if needed
    resetAppState();
  }}
>
  <App />
</ErrorBoundary>

Props:

{
  FallbackComponent: React.ComponentType<FallbackProps>;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  onReset?: () => void;
  resetKeys?: string[]; // Keys that trigger reset when changed
  children: ReactNode;
}

<ErrorFallback />

Default error UI component.

import { ErrorFallback } from '@ridwan-retainer/app-shell';

// Use as-is
<ErrorBoundary FallbackComponent={ErrorFallback}>

// Or customize
function CustomErrorFallback({ error, resetError }: FallbackProps) {
  return (
    <View>
      <Text>Something went wrong</Text>
      <Text>{error.message}</Text>
      <Button title="Try Again" onPress={resetError} />
    </View>
  );
}

useErrorHandler()

Programmatically handle errors.

import { useErrorHandler } from '@ridwan-retainer/app-shell';

function DataLoader() {
  const handleError = useErrorHandler();

  const loadData = async () => {
    try {
      const data = await fetchData();
      return data;
    } catch (error) {
      // This will trigger the nearest ErrorBoundary
      handleError(error);
    }
  };

  return <Button onPress={loadData} />;
}

initializeSentry()

Initialize Sentry error tracking.

import { initializeSentry } from '@ridwan-retainer/app-shell';

initializeSentry({
  dsn: 'your-sentry-dsn',
  environment: __DEV__ ? 'development' : 'production',
  enableInExpoDevelopment: false,
  debug: __DEV__,
  tracesSampleRate: 1.0,
});

Loading & Splash

<LoadingProvider />

Manage loading states and splash screen.

import { LoadingProvider } from '@ridwan-retainer/app-shell';

<LoadingProvider
  onReady={() => {
    console.log('App ready, hiding splash');
  }}
>
  {children}
</LoadingProvider>

useLoadingState()

Control loading states.

import { useLoadingState } from '@ridwan-retainer/app-shell';

function DataFetcher() {
  const { setLoading, clearLoading } = useLoadingState();

  const fetchData = async () => {
    setLoading('data_fetch', 'Loading data...');
    try {
      const data = await api.getData();
      return data;
    } finally {
      clearLoading('data_fetch');
    }
  };

  return <Button onPress={fetchData} />;
}

useAppReady()

Check if app is ready.

import { useAppReady } from '@ridwan-retainer/app-shell';

function App() {
  const isReady = useAppReady();

  if (!isReady) {
    return <SplashScreen />;
  }

  return <MainApp />;
}

useSplashControl()

Manually control splash screen.

import { useSplashControl } from '@ridwan-retainer/app-shell';

function Initializer() {
  const { hideSplash } = useSplashControl();

  useEffect(() => {
    const init = async () => {
      await loadCriticalData();
      await setupServices();
      hideSplash(); // Hide splash when ready
    };
    init();
  }, []);

  return null;
}

Remote Config

<RemoteConfigProvider />

Integrate Firebase Remote Config.

import { RemoteConfigProvider } from '@ridwan-retainer/app-shell';

<RemoteConfigProvider
  defaults={{
    enable_new_feature: false,
    api_timeout: 5000,
    theme_color: '#007AFF',
  }}
  minimumFetchInterval={3600} // 1 hour
>
  {children}
</RemoteConfigProvider>

useFeatureFlag()

Check feature flags from Remote Config.

import { useFeatureFlag } from '@ridwan-retainer/app-shell';

function ExperimentalFeature() {
  const isEnabled = useFeatureFlag('enable_new_ui');

  if (!isEnabled) return null;

  return <NewUIComponent />;
}

useConfigValue()

Get any config value.

import { useConfigValue } from '@ridwan-retainer/app-shell';

function ApiClient() {
  const timeout = useConfigValue<number>('api_timeout', 5000);
  const baseUrl = useConfigValue<string>('api_base_url', 'https://api.example.com');

  // Use in fetch config
  return { timeout, baseUrl };
}

useRemoteConfig()

Access full Remote Config API.

import { useRemoteConfig } from '@ridwan-retainer/app-shell';

function ConfigDebug() {
  const { config, isLoading, refresh } = useRemoteConfig();

  return (
    <View>
      <Text>Config: {JSON.stringify(config, null, 2)}</Text>
      <Button title="Refresh" onPress={refresh} disabled={isLoading} />
    </View>
  );
}

Adapters

The adapter system allows you to extend app-shell functionality.

Creating an Adapter

import { AdapterDefinition } from '@ridwan-retainer/app-shell';

interface AnalyticsAdapter {
  trackEvent: (event: string, properties?: any) => void;
  trackScreen: (screenName: string) => void;
  setUserId: (userId: string) => void;
}

const analyticsAdapter: AdapterDefinition<AnalyticsAdapter> = {
  name: 'analytics',
  version: '1.0.0',
  
  initialize: async (config) => {
    // Initialize analytics SDK
    await AnalyticsSDK.init(config.apiKey);
    
    return {
      trackEvent: (event, properties) => {
        AnalyticsSDK.track(event, properties);
      },
      trackScreen: (screenName) => {
        AnalyticsSDK.screen(screenName);
      },
      setUserId: (userId) => {
        AnalyticsSDK.identify(userId);
      },
    };
  },
  
  cleanup: async () => {
    // Cleanup if needed
    await AnalyticsSDK.flush();
  },
};

Using an Adapter

import { AdapterRegistry, useAdapter } from '@ridwan-retainer/app-shell';

// Register adapter
AdapterRegistry.register(analyticsAdapter);

// Use in component
function TrackingComponent() {
  const analytics = useAdapter<AnalyticsAdapter>('analytics');

  useEffect(() => {
    analytics?.trackScreen('home');
  }, []);

  const trackClick = () => {
    analytics?.trackEvent('button_click', { button: 'subscribe' });
  };

  return <Button onPress={trackClick} />;
}

💡 Complete Examples

Example 1: Production App Setup

import {
  ErrorBoundary,
  LifecycleProvider,
  LoadingProvider,
  RemoteConfigProvider,
  NavigationContainer,
  initializeSentry,
} from '@ridwan-retainer/app-shell';

// Initialize error tracking
initializeSentry({ dsn: SENTRY_DSN });

function App() {
  return (
    <ErrorBoundary>
      <LifecycleProvider
        onInitialize={async () => {
          // Initialize critical services
          await initAuth();
          await loadUserPreferences();
          await connectAnalytics();
        }}
        onReady={() => {
          console.log('App initialization complete');
        }}
        onBackground={() => {
          // Save state when backgrounded
          saveAppState();
        }}
        onForeground={() => {
          // Refresh data when foregrounded
          refreshData();
        }}
      >
        <RemoteConfigProvider
          defaults={{
            enable_new_ui: false,
            api_timeout: 5000,
          }}
        >
          <LoadingProvider>
            <NavigationContainer
              linking={{
                prefixes: ['myapp://'],
                config: { screens: { /* ... */ } },
              }}
            >
              <MainNavigator />
            </NavigationContainer>
          </LoadingProvider>
        </RemoteConfigProvider>
      </LifecycleProvider>
    </ErrorBoundary>
  );
}

Example 2: Theme Switcher

import {
  useTheme,
  useThemeActions,
} from '@ridwan-retainer/app-shell';

function ThemeSwitcher() {
  const theme = useTheme();
  const { toggleTheme, setCustomTheme } = useThemeActions();

  const applyCustomTheme = () => {
    setCustomTheme({
      mode: 'dark',
      colors: {
        primary: '#FF6B6B',
        background: '#1A1A1A',
        text: '#FFFFFF',
        border: '#333333',
        error: '#FF4444',
        success: '#44FF44',
      },
    });
  };

  return (
    <View style={{ backgroundColor: theme.colors.background }}>
      <Text style={{ color: theme.colors.text }}>
        Current Mode: {theme.mode}
      </Text>
      <Button title="Toggle Theme" onPress={toggleTheme} />
      <Button title="Custom Theme" onPress={applyCustomTheme} />
    </View>
  );
}

Example 3: Feature Flag System

import {
  useFeatureFlag,
  useFeatureFlagActions,
} from '@ridwan-retainer/app-shell';

// Admin panel to toggle features
function FeatureFlagsPanel() {
  const { setFlag } = useFeatureFlagActions();

  return (
    <View>
      <FlagToggle
        name="new_ui"
        label="New UI Design"
        onToggle={(value) => setFlag('new_ui', value)}
      />
      <FlagToggle
        name="beta_features"
        label="Beta Features"
        onToggle={(value) => setFlag('beta_features', value)}
      />
    </View>
  );
}

// Feature component that respects flag
function NewUIFeature() {
  const isEnabled = useFeatureFlag('new_ui');

  if (!isEnabled) {
    return <LegacyUI />;
  }

  return <NewUI />;
}

Example 4: Navigation with Deep Linking

import {
  NavigationContainer,
  useTypedNavigation,
  useTypedRoute,
} from '@ridwan-retainer/app-shell';

type RootParamList = {
  Home: undefined;
  Article: { id: string; source?: string };
  Profile: { userId: string };
};

function App() {
  return (
    <NavigationContainer<RootParamList>
      linking={{
        prefixes: ['myapp://', 'https://myapp.com'],
        config: {
          screens: {
            Home: '',
            Article: 'article/:id',
            Profile: 'user/:userId',
          },
        },
      }}
    >
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Article" component={ArticleScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

function HomeScreen() {
  const navigation = useTypedNavigation<RootParamList>();

  const openArticle = (id: string) => {
    navigation.navigate('Article', { id, source: 'home_feed' });
  };

  return <ArticleList onArticlePress={openArticle} />;
}

function ArticleScreen() {
  const route = useTypedRoute<{ id: string; source?: string }>();
  const { id, source } = route.params;

  // Track where user came from
  useEffect(() => {
    trackEvent('article_view', { id, source });
  }, [id, source]);

  return <ArticleContent id={id} />;
}

✅ Best Practices

1. Initialize Early

Place lifecycle initialization as high as possible in component tree:

// App.tsx - root file
<LifecycleProvider onInitialize={initializeApp}>
  <RestOfApp />
</LifecycleProvider>

2. Use Error Boundaries

Wrap different app sections with error boundaries:

<ErrorBoundary> {/* Top-level */}
  <Navigation>
    <ErrorBoundary> {/* Per-screen */}
      <ComplexScreen />
    </ErrorBoundary>
  </Navigation>
</ErrorBoundary>

3. Centralize State

Use provided stores for app-wide state:

// Don't create separate state management for theme, settings, etc.
// Use built-in stores
const theme = useTheme();
const settings = useSettings();

4. Type Your Navigation

Always type your navigation stack:

type RootStackParamList = {
  Home: undefined;
  Detail: { id: string };
};

const navigation = useTypedNavigation<RootStackParamList>();

5. Handle Loading States

Use loading provider for async operations:

const { setLoading, clearLoading } = useLoadingState();

const loadData = async () => {
  setLoading('data', 'Loading...');
  try {
    await fetchData();
  } finally {
    clearLoading('data');
  }
};

6. Remote Config Defaults

Always provide defaults for remote config:

<RemoteConfigProvider
  defaults={{
    feature_enabled: false, // Safe default
    timeout: 5000,
  }}
>

🐛 Troubleshooting

Issue: "Provider not found"

Solution: Ensure providers are in correct order:

<ErrorBoundary>
  <LifecycleProvider>
    <LoadingProvider>
      {/* Your app */}
    </LoadingProvider>
  </LifecycleProvider>
</ErrorBoundary>

Issue: Navigation types not working

Solution: Define param list type:

type RootParamList = {
  Home: undefined;
  // ...
};

const navigation = useTypedNavigation<RootParamList>();

Issue: Remote Config not updating

Solution: Check fetch interval and force refresh:

const { refresh } = useRemoteConfig();
await refresh();

Issue: Theme changes not persisting

Solution: Ensure AsyncStorage is linked:

npm install @react-native-async-storage/async-storage
cd ios && pod install

📖 Migration Guide

From 0.0.x to 0.1.0

No breaking changes. Initial release.

🤝 Contributing

Contributions welcome! Submit PRs to GitHub repository.

📄 License

MIT © Ridwan Hamid

🔗 Links

📞 Support