OneApp Docs
PackagesUI

@repo/uni-app

Cross-platform navigation, storage, and utilities for web and React Native. One API for routing, localStorage, and device features. Works with Next.js and Expo Router. No platform-specific code needed.

Quick Start

Add universal utilities in 5 minutes:

pnpm add @repo/uni-app

Same navigation, storage, hooks on web and native. Skip to Quick Start →

Why @repo/uni-app?

Next.js uses next/router, Expo uses expo-router. Different APIs for navigation mean duplicated code. Web has localStorage, native has AsyncStorage. Platform detection requires manual checks. Sharing features (like "Share this page") need different implementations. Clipboard access varies by platform.

@repo/uni-app solves this with unified APIs that work the same on web and native.

Production-ready with Next.js + Expo Router support, secure storage, keyboard handling, and platform detection.

Use cases

  • Cross-platform apps — Same navigation code for web and mobile
  • Data persistence — Store user preferences with one API across platforms
  • Authentication — Secure token storage with Keychain/Keystore on native
  • Responsive design — Detect dimensions, keyboard, app state
  • Social features — Share content, copy links, open external URLs

How it works

@repo/uni-app provides unified APIs that adapt to each platform:

import { useRouter, storage } from "@repo/uni-app";

function ProfileScreen() {
  const router = useRouter();

  // Same API on web and native
  const handleSave = async () => {
    await storage.set("user_prefs", { theme: "dark" });
    router.push("/settings");
  };

  return <Button onPress={handleSave}>Save Settings</Button>;
}
// Web: Uses next/router and localStorage
// Native: Uses expo-router and AsyncStorage

Uses platform adapters that wrap Next.js Router, Expo Router, AsyncStorage, and native modules with a consistent interface.

Key features

Universal navigation — Same router API for Next.js and Expo Router

Cross-platform storage — Unified API for localStorage and AsyncStorage

Secure storage — Keychain (iOS) and Keystore (Android) for sensitive data

Device hooks — Dimensions, keyboard, app state, color scheme

Platform utilities — Share, clipboard, linking with one API

Type-safe — Full TypeScript support with platform-specific types

Quick Start

1. Install the package

pnpm add @repo/uni-app

2. Use cross-platform navigation

app/components/NavigationExample.tsx
import { useRouter, Link } from "@repo/uni-app/navigation";
import { Button } from "@repo/uni-ui";

export function NavigationExample() {
  const router = useRouter();

  return (
    <>
      <Link href="/about">About Us</Link>
      <Button onPress={() => router.push("/settings")}>Settings</Button>
    </>
  );
}

3. Store data with unified storage

app/utils/preferences.ts
import { storage, secureStorage } from "@repo/uni-app/storage";

// Regular data (uses localStorage on web, AsyncStorage on native)
export async function savePreferences(prefs: UserPreferences) {
  await storage.set("user_preferences", prefs);
}

export async function getPreferences() {
  return await storage.get<UserPreferences>("user_preferences");
}

// Sensitive data (uses Keychain on iOS, Keystore on Android)
export async function saveAuthToken(token: string) {
  await secureStorage.set("auth_token", token);
}

4. Detect platform and device state

app/components/ResponsiveLayout.tsx
import { Platform, useColorScheme, useDimensions } from "@repo/uni-app";
import { VStack } from "@repo/uni-ui";

export function ResponsiveLayout() {
  const colorScheme = useColorScheme();
  const { width } = useDimensions();
  const isTablet = width >= 768;

  return (
    <VStack
      className={`
        ${colorScheme === "dark" ? "bg-gray-900" : "bg-white"}
        ${isTablet ? "flex-row" : "flex-col"}
      `}
    >
      {Platform.isWeb && <div>Web-specific content}
      {/* Your content */}
    </VStack>
  );
}

That's it! You now have cross-platform navigation, storage, and utilities working on web and native.

Share and clipboard

Add social features with one API:

import { share, clipboard } from "@repo/uni-app";

// Share content
await share({
  title: "Check this out!",
  url: "https://example.com"
});

// Copy to clipboard
await clipboard.copy("Text to copy");

Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/uni-app
Dependenciesexpo-router, next/router
PlatformsWeb (Next.js), Native (Expo)

Export Paths

PathDescription
@repo/uni-appMain exports
@repo/uni-app/navigationUniversal navigation
@repo/uni-app/storageCross-platform storage
@repo/uni-app/hooksUniversal hooks
@repo/uni-app/native-bridgeWebView ↔ Native communication

Unified API

The navigation API works the same on both web (Next.js) and native (Expo Router). No platform-specific code needed.

useRouter Hook

import { useRouter } from "@repo/uni-app/navigation";

function MyComponent() {
  const router = useRouter();

  // highlight-start
  // Navigate
  router.push("/dashboard");

  // With params
  router.push("/user/123");

  // Replace (no back)
  router.replace("/login");

  // Go back
  router.back();
  // highlight-end

  return <Button onPress={() => router.push("/settings")}>Settings</Button>;
}
import { Link } from "@repo/uni-app/navigation";

// Text link
<Link href="/about">About Us</Link>

// Wrap any component
// highlight-start
<Link href="/profile" asChild>
  <Avatar src={user.image} />
</Link>
// highlight-end

// External link
<Link href="https://example.com" external>
  External Site
</Link>

useParams Hook

import { useParams } from "@repo/uni-app/navigation";

// In /user/[id] route
function UserProfile() {
  // highlight-next-line
  const { id } = useParams<{ id: string }>();

  return <Text>User ID: {id}</Text>;
}

Storage

Async Storage

Platform Storage

Uses AsyncStorage on native and localStorage on web. Data is automatically serialized/deserialized as JSON.

import { storage } from "@repo/uni-app/storage";

// Set value
// highlight-next-line
await storage.set("user_preferences", { theme: "dark", language: "en" });

// Get value
const prefs = await storage.get<UserPreferences>("user_preferences");

// Remove
await storage.remove("user_preferences");

// Clear all
await storage.clear();

Secure Storage

Sensitive Data

Use secure storage for tokens, credentials, and other sensitive data. Uses Keychain on iOS and Keystore on Android.

import { secureStorage } from "@repo/uni-app/storage";

// For sensitive data (uses Keychain/Keystore on native)
// highlight-start
await secureStorage.set("auth_token", token);
const token = await secureStorage.get("auth_token");
await secureStorage.remove("auth_token");
// highlight-end

Hooks

useColorScheme

import { useColorScheme } from "@repo/uni-app/hooks";

function ThemedComponent() {
  // highlight-next-line
  const colorScheme = useColorScheme(); // "light" | "dark"

  return (
    <View className={colorScheme === "dark" ? "bg-gray-900" : "bg-white"}>
      <Text>Current theme: {colorScheme}</Text>
    </View>
  );
}

useDimensions

import { useDimensions } from "@repo/uni-app/hooks";

function ResponsiveComponent() {
  // highlight-next-line
  const { width, height, scale } = useDimensions();

  const isTablet = width >= 768;

  return (
    <View className={isTablet ? "flex-row" : "flex-col"}>
      {/* Responsive layout */}
    </View>
  );
}

useKeyboard

import { useKeyboard } from "@repo/uni-app/hooks";

function InputScreen() {
  // highlight-next-line
  const { keyboardHeight, isKeyboardVisible } = useKeyboard();

  return (
    <View style={{ paddingBottom: keyboardHeight }}>
      <Input placeholder="Type here..." />
    </View>
  );
}

useAppState

import { useAppState } from "@repo/uni-app/hooks";

function DataSyncComponent() {
  // highlight-next-line
  const appState = useAppState(); // "active" | "background" | "inactive"

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

Platform Detection

import { Platform } from "@repo/uni-app";

// Check platform
// highlight-start
if (Platform.isWeb) {
  // Web-specific code
}

if (Platform.isNative) {
  // Native-specific code
}

if (Platform.isIOS) {
  // iOS-specific code
}

if (Platform.isAndroid) {
  // Android-specific code
}
// highlight-end

// Platform select
const borderRadius = Platform.select({
  ios: 20,
  android: 8,
  web: 12
});

Sharing

import { share } from "@repo/uni-app";

// Share content
// highlight-start
await share({
  title: "Check this out!",
  message: "Interesting content to share",
  url: "https://example.com/content"
});
// highlight-end

// Check if sharing is available
const canShare = await share.isAvailable();

Clipboard

import { clipboard } from "@repo/uni-app";

// Copy to clipboard
await clipboard.copy("Text to copy");

// Read from clipboard
const text = await clipboard.paste();

Linking

import { linking } from "@repo/uni-app";

// Open URL
await linking.openURL("https://example.com");

// Open settings
await linking.openSettings();

// Check if can open URL
const canOpen = await linking.canOpenURL("mailto:test@example.com");

// Open email
// highlight-start
await linking.openEmail("support@example.com", {
  subject: "Help Request",
  body: "I need help with..."
});
// highlight-end

Native Bridge

The Native Bridge enables web applications running inside a React Native WebView to call native features like haptics, clipboard, and share sheets.

When to Use

Use NativeBridge when embedding oneapp-onstage or other web apps inside the mobile-app WebView. The bridge automatically detects if running in native and gracefully falls back on web.

Check if Native

import { NativeBridge, isNative } from "@repo/uni-app/native-bridge";

// Check if running in native WebView
if (isNative()) {
  // Native-specific code
}

// Or use the object
if (NativeBridge.isNative()) {
  console.log("Platform:", NativeBridge.getPlatform()); // "ios" | "android"
}

Haptic Feedback

import { haptic } from "@repo/uni-app/native-bridge";

// Trigger haptic feedback
// highlight-start
haptic("light"); // Light tap
haptic("medium"); // Medium tap
haptic("heavy"); // Heavy tap
haptic("success"); // Success notification
haptic("warning"); // Warning notification
haptic("error"); // Error notification
// highlight-end

Clipboard

import { copyToClipboard, pasteFromClipboard } from "@repo/uni-app/native-bridge";

// Copy to native clipboard
// highlight-next-line
await copyToClipboard("Text to copy");

// Paste from clipboard
const text = await pasteFromClipboard();

Share

import { share } from "@repo/uni-app/native-bridge";

// Open native share sheet
// highlight-start
await share({
  title: "Check this out!",
  message: "Interesting content to share",
  url: "https://example.com/content"
});
// highlight-end

Authentication

import { getAuthToken } from "@repo/uni-app/native-bridge";

// Get auth token from native secure storage
// highlight-next-line
const token = await getAuthToken();

if (token) {
  // Use token for API calls
  fetch("/api/data", {
    headers: { Authorization: `Bearer ${token}` }
  });
}
import { navigateNative } from "@repo/uni-app/native-bridge";

// Navigate to a native screen from web
// highlight-start
navigateNative("/profile");
navigateNative("/settings", { section: "notifications" });
// highlight-end

Open External URLs

import { openURL } from "@repo/uni-app/native-bridge";

// Open URL in native browser
openURL("https://example.com");

React Hook

import { useNativeBridge } from "@repo/uni-app/native-bridge";

function MyComponent() {
  const {
    isNative,
    platform,
    haptic,
    copyToClipboard,
    share,
    getAuthToken,
  } = useNativeBridge();

  const handleShare = async () => {
    if (isNative) {
      haptic("success");
    }
    await share({ message: "Check this out!" });
  };

  return <Button onPress={handleShare}>Share</Button>;
}

Wait for Bridge Ready

import { onNativeBridgeReady } from "@repo/uni-app/native-bridge";

// Wait for native bridge to initialize
useEffect(() => {
  const cleanup = onNativeBridgeReady((platform) => {
    console.log("Native bridge ready on:", platform);
  });
  return cleanup;
}, []);

On this page