@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-appSame 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 AsyncStorageUses 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-app2. Use cross-platform navigation
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
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
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
| Property | Value |
|---|---|
| Location | packages/uni-app |
| Dependencies | expo-router, next/router |
| Platforms | Web (Next.js), Native (Expo) |
Export Paths
| Path | Description |
|---|---|
@repo/uni-app | Main exports |
@repo/uni-app/navigation | Universal navigation |
@repo/uni-app/storage | Cross-platform storage |
@repo/uni-app/hooks | Universal hooks |
@repo/uni-app/native-bridge | WebView ↔ Native communication |
Navigation
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>;
}Link Component
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-endHooks
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-endNative 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-endClipboard
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-endAuthentication
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}` }
});
}Navigate to Native
import { navigateNative } from "@repo/uni-app/native-bridge";
// Navigate to a native screen from web
// highlight-start
navigateNative("/profile");
navigateNative("/settings", { section: "notifications" });
// highlight-endOpen 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;
}, []);Related Packages
- @repo/uni-ui - Universal UI components
- @repo/auth-mobile - Mobile authentication
- mobile-app - Hybrid mobile app using NativeBridge