# Analytics & Error Tracking Source: https://docs.shipnative.app/core-features/analytics Product analytics with PostHog and error tracking with Sentry Shipnative includes comprehensive monitoring with [PostHog](https://posthog.com/) for analytics and [Sentry](https://sentry.io/) for error tracking. All utilities are pre-built and ready to use. ## Setup ```bash theme={null} yarn setup ``` The wizard prompts for your PostHog API key and host URL. Or manually add to `apps/app/.env`: ```bash theme={null} EXPO_PUBLIC_POSTHOG_API_KEY=your_api_key EXPO_PUBLIC_POSTHOG_HOST=https://app.posthog.com ``` **Without keys:** Mock mode logs events to console. Connect real PostHog early to capture usage data. *** ## Usage ### useAnalytics Hook ```typescript theme={null} import { useAnalytics } from '@/hooks/useAnalytics' function MyComponent() { const { trackEvent, trackScreen, isFeatureEnabled } = useAnalytics() // Track events trackEvent('button_clicked', { button: 'submit' }) // Track screens trackScreen('HomeScreen') // Feature flags if (isFeatureEnabled('new-feature')) { // Show feature } } ``` ### Utility Functions For use outside React components: ```typescript theme={null} import { trackEvent, trackScreen, identifyUser, clearUser, } from '@/utils/analytics' trackEvent('purchase_completed', { amount: 9.99 }) trackScreen('ProductScreen', { product_id: '123' }) identifyUser('user-123', { email: 'user@example.com' }) clearUser() // on logout ``` ### Direct Client Access ```typescript theme={null} import { posthog } from '@/services/posthog' posthog.track('event_name', { property: 'value' }) posthog.identify('user-id', { email: 'user@example.com' }) ``` *** ## Feature Flags ```typescript theme={null} const { isFeatureEnabled, getFeatureFlag } = useAnalytics() // Boolean flag if (isFeatureEnabled('dark-mode-v2')) { // Use new dark mode } // Multivariate flag const variant = getFeatureFlag('pricing-test') // 'control' | 'variant-a' ``` Create flags in PostHog Dashboard → Feature Flags. See [PostHog Feature Flags](https://posthog.com/docs/feature-flags). *** ## Pre-built Event Helpers ```typescript theme={null} import { trackAuth, trackSubscription } from '@/utils/analytics' // Auth events trackAuth.signupCompleted('user-123', { method: 'email' }) trackAuth.loginCompleted('user-123', { method: 'google' }) trackAuth.logout() // Subscription events trackSubscription.started({ plan: 'pro_monthly' }) trackSubscription.completed({ plan: 'pro_monthly', price: 9.99 }) ``` *** ## Testing **Mock mode:** Events logged to console as `[MockPostHog] Event: ...` **Real PostHog:** 1. Add API key and host to `.env` 2. Trigger events in app 3. View in PostHog Dashboard → Events / Persons *** ## Troubleshooting **Events not showing in PostHog?** * Verify API key and host are correct * Allow a few minutes for events to process * Check console for PostHog errors **Still in mock mode?** * Verify `apps/app/.env` has correct keys * Restart Metro: `yarn start --clear` For more, see [PostHog React Native SDK](https://posthog.com/docs/libraries/react-native). *** ## Error Tracking with Sentry Sentry automatically captures JavaScript errors, native crashes, and performance issues. It's configured to work seamlessly with your app. ### Setup Run the setup wizard: ```bash theme={null} yarn setup ``` The wizard will prompt for your Sentry DSN. Or manually add to `apps/app/.env`: ```bash theme={null} SENTRY_DSN=your_sentry_dsn ``` **Without DSN:** Mock mode logs errors to console. Connect real Sentry early to catch production issues. ### Automatic Error Capture Sentry automatically captures: * **JavaScript errors** - Unhandled exceptions and promise rejections * **Native crashes** - iOS and Android native errors * **React errors** - Component render errors via Error Boundaries * **Performance issues** - Slow screens and network requests No additional code needed - errors are automatically reported. ### Manual Error Reporting Capture errors manually with context: ```typescript theme={null} import * as Sentry from '@sentry/react-native' try { processPayment(data) } catch (error) { Sentry.captureException(error, { level: 'error', tags: { section: 'payments' }, extra: { userId: user.id, amount: 9.99 }, }) } ``` ### Breadcrumbs Add breadcrumbs for debugging context: ```typescript theme={null} import * as Sentry from '@sentry/react-native' Sentry.addBreadcrumb({ category: 'navigation', message: 'User navigated to Profile', level: 'info', data: { from: 'Home', to: 'Profile', }, }) ``` ### User Context Associate errors with users: ```typescript theme={null} import * as Sentry from '@sentry/react-native' // Set user (on login) Sentry.setUser({ id: user.id, email: user.email, username: user.name, }) // Clear user (on logout) Sentry.setUser(null) ``` ### Performance Monitoring Track custom operations: ```typescript theme={null} import * as Sentry from '@sentry/react-native' // Track async operation await Sentry.startSpan( { name: 'fetch-user-data', op: 'http.client', }, async (span) => { const response = await fetch('https://api.example.com/users') const data = await response.json() span?.setAttribute('http.status_code', response.status) span?.setAttribute('user.count', data.length) return data } ) ``` ### Utility Functions Pre-built helpers in `utils/crashReporting.ts`: ```typescript theme={null} import { logError, logWarning, logInfo } from '@/utils/crashReporting' logError('Payment failed', { userId: '123', amount: 9.99 }) logWarning('Slow network detected', { latency: 5000 }) logInfo('Feature flag evaluated', { flag: 'new-ui', value: true }) ``` ### Testing Sentry Trigger a test error: ```typescript theme={null} import * as Sentry from '@sentry/react-native' // Capture test error Sentry.captureMessage('Test error from app', { level: 'error', tags: { test: true }, }) // Or throw error throw new Error('Test native crash') ``` View errors in Sentry Dashboard → Issues. ### Filtering Errors Configure what gets reported in `services/sentry.ts`: ```typescript theme={null} Sentry.init({ beforeSend: (event, hint) => { // Filter out specific errors if (event.exception?.values?.[0]?.value?.includes('Network request failed')) { return null // Don't send } // Remove sensitive data if (event.user) { delete event.user.email } return event }, }) ``` ### Production Best Practices 1. **Environment tagging** - Automatically set via `EXPO_PUBLIC_ENV` 2. **Release tracking** - Set release version for tracking issues across versions 3. **Source maps** - Uploaded automatically for readable stack traces 4. **User privacy** - Avoid sending PII (personally identifiable information) For more, see [Sentry React Native SDK](https://docs.sentry.io/platforms/react-native/). # Authentication Source: https://docs.shipnative.app/core-features/authentication User login, signup, and account management with Supabase or Convex Shipnative supports two authentication backends: **Supabase** and **Convex**. Both provide email/password auth, social login, and secure session management. ## Setup ```bash theme={null} yarn setup ``` The wizard prompts for your backend choice and credentials, then configures `apps/app/.env` automatically. **Without credentials:** The app runs in mock mode with simulated auth. Connect a real backend before production to test email confirmation, password reset, and OAuth flows. *** ## Using Authentication ### The useAuth() Hook Use `useAuth()` for **all** screens and components. It's the ONLY auth hook you need - it works with both Supabase and Convex backends: ```typescript theme={null} import { useAuth } from '@/hooks' function ProfileScreen() { const { // State user, // Unified AppUser object userId, // User ID string isAuthenticated, // Boolean: is user logged in? isLoading, // Boolean: is auth loading? isEmailVerified, // Boolean: email confirmed? hasCompletedOnboarding, // Boolean: onboarding done? provider, // "supabase" | "convex" // Auth Actions signIn, // Email/password sign in signUp, // Email/password sign up signOut, // Sign out resetPassword, // Send password reset email signInWithGoogle, // Google OAuth signInWithApple, // Apple OAuth signInWithMagicLink, // Magic link / OTP verifyOtp, // Verify OTP code // Profile Actions updateProfile, // Update user profile data completeOnboarding, // Mark onboarding as complete } = useAuth() if (isLoading) return if (!isAuthenticated) return return ( Welcome, {user?.displayName}! Email: {user?.email} ) } const styles = StyleSheet.create((theme) => ({ container: { gap: theme.spacing.md, }, })) ``` **Key Points:** * Zod schema for type-safe validation * React Hook Form for performance * Error messages display automatically * Loading state during submission *** ## Data Lists & Feeds ### Fetch and Display a List ```typescript theme={null} import { FlatList, View, RefreshControl } from 'react-native' import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { useQuery } from '@tanstack/react-query' import { supabase } from '@/services/supabase' import { Text } from '@/components/Text' import { Card } from '@/components/Card' import { Spinner } from '@/components/Spinner' interface Post { id: string title: string content: string created_at: string } export function PostsFeed() { const { theme } = useUnistyles() // Fetch posts with React Query const { data: posts, isLoading, error, refetch, isRefetching } = useQuery({ queryKey: ['posts'], queryFn: async () => { const { data, error } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }) if (error) throw error return data as Post[] }, }) // Loading state if (isLoading) { return ( ) } // Error state if (error) { return ( Failed to load posts {error.message} ) } // Empty state if (!posts || posts.length === 0) { return ( No posts yet Be the first to create one! ) } // Render list return ( item.id} renderItem={({ item }) => ( {item.title} {item.content} {new Date(item.created_at).toLocaleDateString()} )} refreshControl={ } contentContainerStyle={styles.listContent} /> ) } const styles = StyleSheet.create((theme) => ({ centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: theme.spacing.lg, gap: theme.spacing.sm, }, listContent: { padding: theme.spacing.lg, gap: theme.spacing.md, }, card: { gap: theme.spacing.sm, }, })) ``` **Key Points:** * React Query for automatic caching and refetching * Pull-to-refresh with `RefreshControl` * Loading, error, and empty states * Optimistic UI updates *** ## Pull-to-Refresh ### Add to ScrollView ```typescript theme={null} import { ScrollView, RefreshControl } from 'react-native' import { useUnistyles } from 'react-native-unistyles' import { useState } from 'react' export function MyScreen() { const { theme } = useUnistyles() const [refreshing, setRefreshing] = useState(false) const onRefresh = async () => { setRefreshing(true) try { // Fetch fresh data await fetchData() } finally { setRefreshing(false) } } return ( } > {/* Your content */} ) } ``` *** ## Infinite Scroll / Pagination ```typescript theme={null} import { FlatList } from 'react-native' import { useInfiniteQuery } from '@tanstack/react-query' import { supabase } from '@/services/supabase' const PAGE_SIZE = 20 export function InfinitePostsFeed() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['posts', 'infinite'], queryFn: async ({ pageParam = 0 }) => { const { data, error } = await supabase .from('posts') .select('*') .range(pageParam, pageParam + PAGE_SIZE - 1) .order('created_at', { ascending: false }) if (error) throw error return data }, getNextPageParam: (lastPage, allPages) => { if (lastPage.length < PAGE_SIZE) return undefined return allPages.length * PAGE_SIZE }, initialPageParam: 0, }) const posts = data?.pages.flat() ?? [] return ( item.id} renderItem={({ item }) => } onEndReached={() => { if (hasNextPage && !isFetchingNextPage) { fetchNextPage() } }} onEndReachedThreshold={0.5} ListFooterComponent={ isFetchingNextPage ? : null } /> ) } ``` **Key Points:** * Automatic pagination with React Query * Load more on scroll * Loading indicator at bottom *** ## Create, Update, Delete (CRUD) ### Create a Post ```typescript theme={null} import { useMutation, useQueryClient } from '@tanstack/react-query' import { supabase } from '@/services/supabase' export function useCreatePost() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (post: { title: string; content: string }) => { const { data, error } = await supabase .from('posts') .insert(post) .select() .single() if (error) throw error return data }, onSuccess: () => { // Refresh posts list queryClient.invalidateQueries({ queryKey: ['posts'] }) }, }) } // Usage in component function CreatePostScreen() { const createPost = useCreatePost() const handleSubmit = async (data: FormData) => { await createPost.mutateAsync({ title: data.title, content: data.content, }) // Navigate back or show success } return (
{/* Form fields */}
) } ``` ### Update a Post ```typescript theme={null} export function useUpdatePost() { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ id, updates }: { id: string; updates: Partial }) => { const { data, error } = await supabase .from('posts') .update(updates) .eq('id', id) .select() .single() if (error) throw error return data }, onSuccess: (data) => { // Update the specific post in cache queryClient.setQueryData(['posts', data.id], data) // Refresh list queryClient.invalidateQueries({ queryKey: ['posts'] }) }, }) } ``` ### Delete a Post ```typescript theme={null} export function useDeletePost() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (postId: string) => { const { error } = await supabase .from('posts') .delete() .eq('id', postId) if (error) throw error }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }) }, }) } // Usage with confirmation function PostCard({ post }) { const deletePost = useDeletePost() const handleDelete = () => { Alert.alert( 'Delete Post', 'Are you sure you want to delete this post?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => deletePost.mutate(post.id), }, ] ) } return ( {/* Post content */} ) } ``` *** ## ️ Image Upload ```typescript theme={null} import * as ImagePicker from 'expo-image-picker' import { supabase } from '@/services/supabase' import { decode } from 'base64-arraybuffer' import { useState } from 'react' export function useImageUpload() { const [uploading, setUploading] = useState(false) const uploadImage = async (bucket: string = 'avatars') => { try { // Request permissions const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync() if (status !== 'granted') { alert('Sorry, we need camera roll permissions!') return null } // Pick image const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, }) if (result.canceled) return null setUploading(true) // Get the file const image = result.assets[0] const fileExt = image.uri.split('.').pop() const fileName = `${Date.now()}.${fileExt}` const filePath = `${fileName}` // Convert to base64 const response = await fetch(image.uri) const blob = await response.blob() const arrayBuffer = await new Response(blob).arrayBuffer() // Upload to Supabase Storage const { data, error } = await supabase.storage .from(bucket) .upload(filePath, decode(arrayBuffer), { contentType: image.type || 'image/jpeg', }) if (error) throw error // Get public URL const { data: { publicUrl } } = supabase.storage .from(bucket) .getPublicUrl(filePath) return publicUrl } catch (error) { console.error('Upload error:', error) alert('Failed to upload image') return null } finally { setUploading(false) } } return { uploadImage, uploading } } // Usage function AvatarPicker() { const { uploadImage, uploading } = useImageUpload() const [avatarUrl, setAvatarUrl] = useState(null) const handlePickImage = async () => { const url = await uploadImage('avatars') if (url) { setAvatarUrl(url) // Update user profile with new avatar URL } } return ( ) } ``` *** ## Error Handling ### Global Error Boundary ```typescript theme={null} import React, { Component, ErrorInfo, ReactNode } from 'react' import { View, Text } from 'react-native' import { Button } from '@/components/Button' import { trackError } from '@/utils/analytics' interface Props { children: ReactNode } interface State { hasError: boolean error: Error | null } export class ErrorBoundary extends Component { constructor(props: Props) { super(props) this.state = { hasError: false, error: null } } static getDerivedStateFromError(error: Error): State { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: ErrorInfo) { // Log to Sentry trackError(error, { tags: { type: 'error-boundary' }, extra: { componentStack: errorInfo.componentStack }, }) } render() { if (this.state.hasError) { return ( Oops! Something went wrong {this.state.error?.message || 'Unknown error'} ) } return this.props.children } } // Wrap your app function App() { return ( ) } ``` ### Async Error Handling ```typescript theme={null} async function fetchData() { try { const response = await fetch('https://api.example.com/data') if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() return { data, error: null } } catch (error) { console.error('Fetch error:', error) trackError(error as Error, { tags: { type: 'api-error' }, }) return { data: null, error: error as Error } } } ``` *** ## Modal Dialogs ```typescript theme={null} import { Modal, View, Pressable } from 'react-native' import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { Text } from '@/components/Text' import { Button } from '@/components/Button' import { useState } from 'react' export function ConfirmDialog({ visible, title, message, onConfirm, onCancel, }: { visible: boolean title: string message: string onConfirm: () => void onCancel: () => void }) { const { theme } = useUnistyles() return ( {/* Backdrop */} {/* Dialog */} {title} {message} ) } const styles = StyleSheet.create((theme) => ({ backdrop: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', padding: theme.spacing.lg, }, dialog: { backgroundColor: theme.colors.card, borderRadius: theme.radius.lg, padding: theme.spacing.lg, maxWidth: 400, width: '100%', gap: theme.spacing.md, }, actions: { flexDirection: 'row', gap: theme.spacing.sm, justifyContent: 'flex-end', }, })) // Usage function MyScreen() { const [showDialog, setShowDialog] = useState(false) const handleDelete = () => { // Perform delete setShowDialog(false) } return ( <> setShowDialog(false)} /> ) } ``` *** ## Bottom Sheet ```typescript theme={null} import { useRef } from 'react' import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet' import { View } from 'react-native' import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { Text } from '@/components/Text' import { Button } from '@/components/Button' export function MyScreen() { const { theme } = useUnistyles() const bottomSheetRef = useRef(null) const openSheet = () => bottomSheetRef.current?.expand() const closeSheet = () => bottomSheetRef.current?.close() return ( Bottom Sheet Content goes here ) } const styles = StyleSheet.create((theme) => ({ sheetContent: { padding: theme.spacing.lg, gap: theme.spacing.md, }, })) ``` *** ## Next Steps Explore all pre-built components Learn Zustand and React Query patterns Advanced form patterns and validation Connect to backend services **Remember:** These patterns are starting points. Customize them for your specific needs. The components and services are designed to be flexible and composable! # Extending Services Source: https://docs.shipnative.app/development/extending-services Add third-party services like OneSignal, Firebase, or Convex Shipnative's service architecture is designed to be modular. Adding a new service follows a consistent pattern across the codebase. ## Architecture Overview Services implement platform-agnostic interfaces, making it easy to swap providers or add new ones: ``` apps/app/app/ ├── types/ # Service interfaces ├── services/ # Service implementations ├── config/ │ ├── env.ts # Environment variables (Zod validated) │ └── features.ts # Feature flags └── app.tsx # Service initialization ``` **Key principles:** * Each service implements a TypeScript interface * Platform detection handles iOS/Android/Web differences automatically * Services fail gracefully when credentials are missing * Feature flags control service availability *** ## Adding a New Service Let's walk through adding a hypothetical service. The same pattern applies whether you're adding OneSignal, Firebase, Convex, Amplitude, or any other provider. ### Step 1: Define the Interface Create a type definition in `apps/app/app/types/`: ```typescript theme={null} // apps/app/app/types/newservice.ts export type NewServicePlatform = "newservice" export interface NewServiceConfig { apiKey: string // Add platform-specific options as needed } export interface NewServiceInterface { platform: NewServicePlatform initialize(config: NewServiceConfig): Promise // Define your service methods doSomething(data: unknown): Promise // Optional cleanup destroy?(): Promise } ``` Export from the types index: ```typescript theme={null} // apps/app/app/types/index.ts export * from "./newservice" ``` ### Step 2: Add Environment Variables Update the Zod schema in `apps/app/app/config/env.ts`: ```typescript theme={null} const EnvSchema = z.object({ // ... existing variables // New service newserviceApiKey: z.string().optional(), }) ``` Add to the `isServiceConfigured` function: ```typescript theme={null} export function isServiceConfigured( service: "supabase" | "revenuecat" | "posthog" | "sentry" | "newservice" ): boolean { switch (service) { // ... existing cases case "newservice": return !!env.newserviceApiKey } } ``` ### Step 3: Add Feature Flag Update `apps/app/app/config/features.ts`: ```typescript theme={null} export interface FeatureFlags { // ... existing flags enableNewService: boolean } function getFeatureFlags(): FeatureFlags { return { // ... existing flags enableNewService: isServiceConfigured("newservice"), } } ``` ### Step 4: Create the Service Create the service implementation in `apps/app/app/services/`: ```typescript theme={null} // apps/app/app/services/newservice.ts import { Platform } from "react-native" import { env } from "../config/env" import type { NewServiceInterface, NewServiceConfig } from "../types/newservice" import { logger } from "../utils/Logger" // Platform-specific SDK loading let SDK: typeof import("newservice-sdk") | null = null if (Platform.OS !== "web") { try { SDK = require("newservice-react-native") } catch { if (__DEV__) logger.warn("NewService SDK not available") } } else { // Web SDK loaded differently if needed } class NewService implements NewServiceInterface { platform = "newservice" as const private initialized = false async initialize(config: NewServiceConfig): Promise { if (this.initialized) return try { SDK?.configure(config.apiKey) this.initialized = true logger.info("NewService initialized") } catch (error) { logger.error("NewService init failed", {}, error as Error) } } async doSomething(data: unknown): Promise { if (!this.initialized) { logger.warn("NewService not initialized") return } SDK?.doSomething(data) } async destroy(): Promise { // Cleanup if needed } } // Export singleton export const newservice: NewServiceInterface = new NewService() // Export init function export function initNewService(): void { if (!env.newserviceApiKey) { if (__DEV__) { logger.info("NewService not configured - skipping") } return } newservice.initialize({ apiKey: env.newserviceApiKey, }) } ``` ### Step 5: Initialize in App Add initialization to `apps/app/app/app.tsx`: ```typescript theme={null} import { initNewService } from "./services/newservice" // Inside useEffect with other service inits useEffect(() => { InteractionManager.runAfterInteractions(() => { // ... existing inits initNewService() }) }, []) ``` ### Step 6: Use the Service ```typescript theme={null} import { newservice } from "@/services/newservice" import { features } from "@/config/features" function MyComponent() { useEffect(() => { if (features.enableNewService) { newservice.doSomething({ key: "value" }) } }, []) } ``` *** ## Quick Reference: Files to Update | File | What to Add | | ------------------------ | ---------------------------------- | | `types/newservice.ts` | Interface definition | | `types/index.ts` | Export the new types | | `config/env.ts` | Zod schema + `isServiceConfigured` | | `config/features.ts` | Feature flag | | `services/newservice.ts` | Service implementation | | `app.tsx` | Initialization call | *** ## Common Integrations ### Push Notifications (OneSignal) Replace or extend the existing Expo notifications: ```typescript theme={null} // types/pushNotifications.ts export interface PushService { initialize(): Promise requestPermission(): Promise subscribe(userId: string): Promise unsubscribe(): Promise } ``` ### Realtime Database (Convex) Add alongside or instead of Supabase realtime: ```typescript theme={null} // types/realtime.ts export interface RealtimeService { connect(): Promise subscribe(query: string, callback: (data: T) => void): () => void mutate(mutation: string, args: unknown): Promise } ``` ### Analytics (Amplitude, Mixpanel) The existing `AnalyticsService` interface works for most providers: ```typescript theme={null} // Just implement the existing interface import type { AnalyticsService } from "../types/analytics" class AmplitudeService implements AnalyticsService { platform = "amplitude" as const // ... implement methods } ``` *** ## Replacing Existing Services To swap out a service (e.g., PostHog → Amplitude): 1. Create new implementation using the **same interface** 2. Update the export in the service file 3. No other code changes needed ```typescript theme={null} // services/analytics.ts import { AmplitudeService } from "./amplitude" import { PostHogService } from "./posthog" // Swap by changing this line export const analytics: AnalyticsService = new AmplitudeService() // export const analytics: AnalyticsService = new PostHogService() ``` *** ## Platform-Specific SDKs Many services have different SDKs for mobile and web: ```typescript theme={null} let MobileSDK: typeof import("service-react-native") | null = null let WebSDK: typeof import("service-js") | null = null if (Platform.OS === "web") { import("service-js").then((module) => { WebSDK = module }) } else { try { MobileSDK = require("service-react-native") } catch { // SDK not installed } } // Then use the appropriate one const sdk = Platform.OS === "web" ? WebSDK : MobileSDK ``` *** ## Testing Your Integration 1. **Without credentials:** Service should skip initialization gracefully 2. **With credentials:** Verify initialization logs appear 3. **Cross-platform:** Test on iOS, Android, and Web Check the console for initialization logs: ``` [NewService] initialized // or [NewService] not configured - skipping ``` *** ## Troubleshooting **Service not initializing?** * Verify environment variable is set in `apps/app/.env` * Check variable is prefixed with `EXPO_PUBLIC_` * Restart Metro: `yarn app:start --clear` **TypeScript errors?** * Ensure interface is exported from `types/index.ts` * Check import paths use `@/` alias **Platform-specific issues?** * Verify SDK is installed: `yarn add service-react-native` * Check SDK supports your React Native version * Review SDK documentation for Expo compatibility # Forms and Validation Source: https://docs.shipnative.app/development/forms Build forms with React Hook Form and Zod validation Shipnative uses **React Hook Form** for form state and **Zod** for validation. This combo gives you type-safe, performant forms with minimal code. **For vibecoders:** Just tell your AI "create a form with email and password fields" and it'll generate this pattern. ## React Hook Form: Performant Form Management [React Hook Form](https://react-hook-form.com/) is a popular library that helps you build forms with less code and better performance. It focuses on uncontrolled components, reducing re-renders and improving overall responsiveness. ### Key Features * **Performance:** Minimizes re-renders by isolating component updates. * **Developer Experience:** Simple API with intuitive hooks. * **Validation Integration:** Seamlessly integrates with schema validation libraries like Zod. * **Uncontrolled Components:** Leverages native HTML form validation and reduces component re-renders. ### Basic Usage 1. **Import `useForm`:** ```typescript theme={null} import { useForm, Controller } from 'react-hook-form' import { TextField } from '@/../components/TextField' // Assuming a custom TextField component import { Button, View } from 'react-native' ``` 2. **Initialize `useForm`:** ```typescript theme={null} interface FormData { email: string; password: string; } export const LoginForm = () => { const { control, handleSubmit, formState: { errors }, } = useForm({ defaultValues: { email: '', password: '', }, }) const onSubmit = (data: FormData) => console.log(data) return ( ( )} /> ( )} /> ``` *** ## Variants Unistyles lets you define style variants for different states: ```typescript theme={null} import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { Pressable, Text } from 'react-native' type ButtonVariant = 'filled' | 'outlined' | 'ghost' type ButtonSize = 'sm' | 'md' | 'lg' interface ButtonProps { variant?: ButtonVariant size?: ButtonSize onPress: () => void children: React.ReactNode } function Button({ variant = 'filled', size = 'md', onPress, children }: ButtonProps) { return ( {children} ) } const styles = StyleSheet.create((theme) => ({ base: { borderRadius: theme.radius.md, justifyContent: 'center', alignItems: 'center', }, variants: { variants: { variant: { filled: { backgroundColor: theme.colors.primary, }, outlined: { borderWidth: 1, borderColor: theme.colors.border, backgroundColor: 'transparent', }, ghost: { backgroundColor: 'transparent', }, }, size: { sm: { height: 36, paddingHorizontal: theme.spacing.md }, md: { height: 44, paddingHorizontal: theme.spacing.lg }, lg: { height: 56, paddingHorizontal: theme.spacing.xl }, }, }, }, text: { variants: { variant: { filled: { color: theme.colors.primaryForeground }, outlined: { color: theme.colors.foreground }, ghost: { color: theme.colors.primary }, }, }, }, })) ``` *** ## Responsive Styles ```typescript theme={null} const styles = StyleSheet.create((theme, runtime) => ({ container: { flexDirection: 'column', padding: theme.spacing.md, // Tablet and up variants: { breakpoint: { md: { flexDirection: 'row', maxWidth: 800, alignSelf: 'center', }, }, }, }, })) ``` Breakpoints are defined in `apps/app/app/theme/unistyles.ts`. *** ## Common Mistakes | Mistake | Fix | | ------------------------------- | --------------------------------------------- | | Hardcoded colors like `#000000` | Use `theme.colors.foreground` | | Hardcoded spacing like `16` | Use `theme.spacing.md` | | Inline styles everywhere | Use `StyleSheet.create` | | Missing theme function | Use `StyleSheet.create((theme) => ({ ... }))` | | Using NativeWind/Tailwind | NativeWind was removed, use Unistyles | *** ## Customizing the Theme Edit `apps/app/app/theme/unistyles.ts` to change: ```typescript theme={null} export const lightTheme = { colors: { primary: '#6366F1', // Your brand color primaryForeground: '#fff', background: '#ffffff', foreground: '#000000', // ... more colors }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, // ... typography, radius, shadows } export const darkTheme = { colors: { primary: '#818CF8', // Slightly lighter for dark mode primaryForeground: '#fff', background: '#0a0a0a', foreground: '#ffffff', // ... matching structure }, // Same spacing, typography, etc. } ``` *** ## Resources * [Unistyles Documentation](https://unistyl.es/) * Theme file: `apps/app/app/theme/unistyles.ts` * Components: `apps/app/app/components/` * Component Showcase: Dev Menu in running app # Getting Started Source: https://docs.shipnative.app/getting-started/index Two paths to get started - choose based on your style Pick your path: Start building immediately with AI (Quickstart), or configure all services first (Full Setup). **Start building in 2 minutes** For developers who want to start vibecoding immediately with AI assistance. Runs in mock mode by default, but we recommend running `yarn setup` early to add real API keys for accurate behavior. Perfect for: Non-technical users, rapid prototyping, AI-assisted development **Configure everything from the start** For developers who want to understand the full stack and configure all backend services (Supabase, RevenueCat, PostHog, etc.) from day one. Perfect for: Technical users, production planning, full control ## What's the Difference? | Aspect | Quickstart (Vibecoders) | Full Setup (Engineers) | | ----------------------------- | --------------------------- | -------------------------------- | | **Time to start** | 2 minutes | 15-30 minutes | | **API keys needed** | None | Required for production features | | **Backend setup** | Not required (uses mocks) | Full configuration available | | **Best for** | Building features with AI | Understanding the full stack | | **When to add real services** | Later, when ready to deploy | Immediately or later | **Both paths lead to the same place!** You can start with the quickstart and add real API keys later, or configure everything upfront. Choose based on your preference and timeline. ## What You'll Learn This section covers: * Clone and install in 2 minutes * Run the app (mock fallback) and add real API keys early with `yarn setup` * Start building features with AI assistance * Use the "golden prompt" to leverage AI effectively * Test different user states with the dev menu [Go to Quickstart →](/getting-started/quickstart) * Clone the repository * Run the interactive setup wizard * Configure backend services (Supabase, RevenueCat, PostHog, Sentry) * Set up OAuth providers (Google, Apple) * Verify your configuration * Understand what each service does [Go to Full Setup →](/getting-started/installation) * Launch on iOS simulator/device * Launch on Android emulator/device * Use the dev menu and debugging tools * Understand mock mode vs. real services * Toggle between different user states [Go to Running Your App →](/getting-started/running-the-app) * Understand the monorepo layout * Navigate the codebase effectively * Know where to add screens, components, and state * Find key configuration files * Explore pre-built components and utilities [Go to Project Structure →](/getting-started/project-structure) ## Recommended Path Pick **Quickstart** if you want to start vibecoding now, or **Full Setup** if you want to configure services first. Follow the guide to clone, install, and launch your app on iOS or Android. Read the [Project Structure](/getting-started/project-structure) guide to know where things are. Use AI assistance to add features, or manually explore the codebase. It's up to you! **New to vibecoding?** Start with the [Quickstart guide](/getting-started/quickstart) - it explains vibecoding and gets you building features in minutes! Let's get your project off the ground! # Full Setup Source: https://docs.shipnative.app/getting-started/installation Configure backend services for production For quick start, see [Quickstart](/getting-started/quickstart). This guide covers full production configuration. **You can skip all service configuration initially.** The app runs in mock mode by default with simulated auth, database, and payments. Configure real services when you're ready for production. ## Requirements **For development:** Nothing extra. Mock services work out of the box. **For production:** * Backend (Supabase or Convex) - **required** * Android release keystore - **required** * Bundle identifiers in `app.json` - **required** * RevenueCat, PostHog, Sentry - optional but recommended ## Prerequisites Install these tools before proceeding: | Tool | How to Install | Verify | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------- | | **Node.js 20** | [Volta](https://volta.sh) (recommended): `curl https://get.volta.sh \| bash && volta install node@20.19.0 yarn@4.9.1` | `node -v` → v20.x.x | | **Yarn** | Included with Volta, or: `corepack enable` | `yarn -v` → 4.x.x | | **Git** | macOS: `xcode-select --install` / Windows: [git-scm.com](https://git-scm.com) | `git --version` | | **Xcode** (iOS) | [Mac App Store](https://apps.apple.com/app/xcode/id497799835) - open once to accept license | N/A | | **Android Studio** (Android) | [developer.android.com/studio](https://developer.android.com/studio) - install SDK during setup | N/A | **New to JavaScript/Node?** We recommend [Volta](https://volta.sh) - it automatically manages Node versions so you don't have to think about it. Just run the install command above and you're set. **Node version matters.** The project requires Node 20 specifically. Node 22+ causes build failures with some native dependencies. Run `node -v` to check your version before proceeding. ## Step 1: Clone and Install ```bash theme={null} git clone https://github.com/shipnativeapp/shipnative.git cd shipnative yarn install ``` ## Step 2: Run Setup Wizard ```bash theme={null} yarn setup ``` The wizard configures: * App name and bundle identifier * Backend provider (Supabase or Convex) * Backend credentials * OAuth providers (Google, Apple) * RevenueCat API keys * PostHog analytics * Sentry error tracking **Options:** ```bash theme={null} yarn setup --help # Show all options yarn setup --dry-run # Preview changes yarn setup --non-interactive # For CI/CD ``` ### Backend Selection The wizard prompts you to choose a backend: | Backend | Best For | | ------------ | ------------------------------------------------------------ | | **Supabase** | SQL apps, PostgreSQL experience, RLS-based security | | **Convex** | TypeScript-first apps, reactive queries, function-level auth | **After you choose**, the setup wizard automatically: * Removes the unused backend's code (folders, hooks, services) * Updates `.env.example` to only show your chosen backend's variables * Removes the unused backend's packages from `package.json` * Runs `yarn install` to update the lockfile This gives you a clean codebase with only the backend you selected. See [Backend & Database](/core-features/backend) for detailed comparison. ### Service Configuration Each service is optional. Skip any to use mock mode. | Service | Where to Find Credentials | | ------------- | ----------------------------------------------------------------------------------------- | | Supabase | Dashboard → Settings → API → Publishable key | | Google OAuth | [Google Cloud Console](https://console.cloud.google.com/) → APIs & Services → Credentials | | Apple Sign In | [Apple Developer Portal](https://developer.apple.com/) → Certificates, IDs & Profiles | | RevenueCat | Dashboard → Your App → API Keys | | PostHog | Dashboard → Project Settings → API Keys | | Sentry | Dashboard → Settings → Client Keys (DSN) | | Service | Where to Find Credentials | | ------------- | --------------------------------------------------- | | Convex | Run `npx convex dev` from project root | | Google OAuth | Convex Dashboard → Settings → Environment Variables | | Apple Sign In | Convex Dashboard → Settings → Environment Variables | | RevenueCat | Dashboard → Your App → API Keys | | PostHog | Dashboard → Project Settings → API Keys | | Sentry | Dashboard → Settings → Client Keys (DSN) | **Convex Quick Setup:** ```bash theme={null} # From project root npx convex dev # Creates project, saves URL to .env.local # Required: Set up auth keys (JWT_PRIVATE_KEY + JWKS) npx @convex-dev/auth npx convex run seed:run # Add demo data ``` **Both `JWT_PRIVATE_KEY` and `JWKS` are required.** Without them, sign-in will fail. See [Convex Setup](/core-features/convex#2-set-up-authentication-keys) for manual setup if needed. OAuth secrets go in Convex Dashboard → Settings → Environment Variables: * `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` * `AUTH_APPLE_ID`, `AUTH_APPLE_SECRET` * `RESEND_API_KEY` (for magic link emails) See individual feature guides for detailed setup: * [Authentication](/core-features/authentication) * [Social Login](/core-features/social-login) * [Payments](/core-features/payments) * [Analytics](/core-features/analytics) * [Error Tracking](/core-features/error-tracking) ### Security Note `EXPO_PUBLIC_*` variables are bundled into your app. Only use: * Supabase publishable key (protected by RLS) * Convex deployment URL (protected by function-level auth) * RevenueCat public keys * PostHog project API key Never expose `service_role` keys or secrets in your app. ## Step 3: Run Your App ```bash theme={null} cd apps/app yarn ios # or: yarn android, yarn web ``` *** ## Manual Configuration If you prefer manual configuration: ```bash theme={null} cp apps/app/.env.example apps/app/.env ``` Edit `apps/app/.env`: ```bash theme={null} EXPO_PUBLIC_BACKEND_PROVIDER=supabase EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-key EXPO_PUBLIC_POSTHOG_API_KEY=your-key EXPO_PUBLIC_POSTHOG_HOST=https://app.posthog.com EXPO_PUBLIC_REVENUECAT_IOS_KEY=your-key EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=your-key EXPO_PUBLIC_SENTRY_DSN=your-dsn ``` ```bash theme={null} EXPO_PUBLIC_BACKEND_PROVIDER=convex EXPO_PUBLIC_CONVEX_URL=https://your-project.convex.cloud EXPO_PUBLIC_POSTHOG_API_KEY=your-key EXPO_PUBLIC_POSTHOG_HOST=https://app.posthog.com EXPO_PUBLIC_REVENUECAT_IOS_KEY=your-key EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=your-key EXPO_PUBLIC_SENTRY_DSN=your-dsn ``` Edit `apps/app/app.json`: ```json theme={null} { "expo": { "name": "Your App Name", "slug": "your-app-slug", "ios": { "bundleIdentifier": "com.yourcompany.yourapp" }, "android": { "package": "com.yourcompany.yourapp" }, "scheme": "yourapp" } } ``` ## Mock Mode Without credentials, services run in mock mode: | Service | Mock Behavior | | --------------- | ---------------------------------------------- | | Supabase/Convex | In-memory auth/database, login always succeeds | | RevenueCat | Purchases succeed instantly, dev menu toggle | | PostHog | Events logged to console | | Sentry | Errors logged to console | See [Mock Services](/development/mock-services) for details. *** ## Troubleshooting ### Setup wizard fails? * Ensure Node.js 20+ and Yarn are installed * Delete partial `.env`: `rm apps/app/.env` * Try again: `yarn setup` ### "Package not found in project" error? If you see an error like: ``` Internal Error: Package for myapp@workspace:apps/app not found in the project ``` This happens when `yarn.lock` is out of sync after renaming your app. Fix it: ```bash theme={null} yarn install # Regenerates yarn.lock with new package name ``` ### iOS build fails with Swift errors? If you see errors like `missing argument for parameter 'logHandlers'` in expo-widgets: ```bash theme={null} # Ensure patches are applied yarn install cd apps/app yarn prebuild:clean yarn ios ``` The `postinstall` script applies patches automatically. If issues persist, manually verify patches exist in `/patches/` directory. ### App won't start? ```bash theme={null} cd apps/app yarn start --clear ``` ### Builds are slow? `yarn ios` / `yarn android` compile native code (3-5 minutes). For daily development: ```bash theme={null} # Build once yarn ios # Then use the dev server (instant refresh) yarn start ``` Only rebuild when native dependencies change. ### API keys not working? * Keys must be in `apps/app/.env` * All keys need `EXPO_PUBLIC_` prefix * Restart Metro after adding keys See [Troubleshooting](/troubleshooting) for more. *** ## Next Steps Configure your chosen backend Set up user auth Set up RevenueCat Ship to app stores # Marketing Landing Page Source: https://docs.shipnative.app/getting-started/marketing-page Ready-to-use marketing page with waitlist and launch modes Shipnative includes a marketing landing page built with Vite + React + Tailwind CSS. Use it to collect emails before launch (waitlist mode) or showcase your app after it's live (launch mode). **For vibecoders:** `yarn setup` configures this for you. Just run `yarn web` to see your marketing page at localhost:5173. **Which landing page is this?** * `apps/web/` - Your app's marketing page (this guide) * `landing_page/` - Shipnative's own marketing site (not yours) ## Two Modes The marketing page has two modes you can switch between: ### Waitlist Mode (Pre-Launch) Perfect for collecting early user emails before your app launches. **Features:** * Email capture form with validation * Success confirmation message * App screenshot showcase * Feature bento grid with 6 tiles: * AI-first architecture * Mock mode * Premium design system * Monetization * Analytics & reliability * Optimized dev loop * AI tools integration section (Cursor, Claude, Copilot, etc.) * Fully responsive design * SEO optimized **When to use:** Building in public, collecting beta testers, pre-launch hype ### Launch Mode (Post-Launch) Perfect for when your app is live on the App Store and Google Play. **Features:** * App Store and Google Play badge links * Hero section with app name and tagline * Feature highlights (3 key benefits) * Clean, minimal design focused on downloads * SEO optimized **When to use:** App is live and you want to drive downloads ## ️ Quick Setup `yarn setup` now also asks for the marketing page mode. It writes `apps/web/.env` for you, so you can skip to Step 3 if you just ran it. **Important:** The Resend API key is now stored securely in Supabase Edge Function secrets, not in the frontend `.env` file. See the "Adding a Waitlist API" section below for setup instructions. ### Step 1: Copy Environment Variables (if you skipped `yarn setup`) ```bash theme={null} cp apps/web/.env.example apps/web/.env ``` ### Step 2: Configure Your Settings Edit `apps/web/.env` (or let `yarn setup` fill it): ```bash theme={null} # Choose mode: 'waitlist' or 'launch' VITE_MODE=waitlist # Your app details VITE_APP_NAME=YourApp VITE_APP_DESCRIPTION=Your app description goes here VITE_APP_URL=https://yourapp.com # SEO & Social VITE_OG_IMAGE=https://yourapp.com/og-image.jpg # Waitlist Mode Settings VITE_APP_SCREENSHOT_URL=/app-screenshot.png VITE_WAITLIST_API_ENDPOINT=https://your-project-ref.supabase.co/functions/v1/waitlist # Supabase Edge Function endpoint # Launch Mode Settings (required only in launch mode) VITE_IOS_APP_URL=https://apps.apple.com/app/your-app VITE_ANDROID_APP_URL=https://play.google.com/store/apps/details?id=com.yourapp ``` ### Step 3: Switch Modes Quickly ```bash theme={null} yarn marketing:waitlist # sets VITE_MODE=waitlist in apps/web/.env yarn marketing:launch # sets VITE_MODE=launch in apps/web/.env # or: yarn marketing:mode ``` ### Step 4: Run the Development Server ```bash theme={null} yarn web # alias for `yarn marketing:dev` # or: yarn marketing:dev ``` Visit `http://localhost:5173` to see your marketing page! ## Customization ### Switching Between Modes Use the helper command (it updates `apps/web/.env` for you): ```bash theme={null} yarn marketing:mode waitlist # Pre-launch waitlist yarn marketing:mode launch # Post-launch store links # Aliases: yarn marketing:waitlist / yarn marketing:launch ``` Prefer to edit manually? Change `VITE_MODE` in `apps/web/.env`. No code changes needed - the app automatically renders the correct mode! ### Customizing Waitlist Mode Edit `/apps/web/src/components/WaitlistMode.tsx`: **Change feature tiles** (lines 92-164): ```typescript theme={null} const bentoTiles: BentoTile[] = [ { title: 'Your Feature Title', description: 'Feature description here', icon: '', pill: 'Optional badge', accent: 'from-[#E0F2FE] via-white to-[#C7E9FF]', // Gradient colors span: 'lg:col-span-2', // Optional: make tile wider points: [ 'Key benefit 1', 'Key benefit 2', ], metric: { // Optional: add a metric label: 'to launch', value: '< 1 hr', }, }, // Add more tiles... ] ``` **Change AI tools** (lines 33-90): ```typescript theme={null} const aiTools: AiTool[] = [ { name: 'Tool Name', url: 'https://tool.com', domain: 'tool.com', logoText: 'TN', // 1-2 letter abbreviation logoGradient: 'from-[#0C163D] via-[#0F4C75] to-[#2DD4BF]', tagline: 'Short description of integration', }, // Add more tools... ] ``` **Add your app screenshot:** 1. Place your screenshot in `/apps/web/public/app-screenshot.png` 2. Or update `VITE_APP_SCREENSHOT_URL` to use a URL ### Customizing Launch Mode Edit `/apps/web/src/components/LaunchMode.tsx`: **Change feature highlights** (lines 92-108): ```typescript theme={null}
️

Your Feature

Feature description

``` **Customize colors and styling:** The landing page uses Tailwind CSS. Edit component files to change: * Colors: `bg-blue-600`, `text-gray-900`, etc. * Spacing: `p-4`, `m-8`, `gap-6`, etc. * Layout: Grid, flex, responsive breakpoints ### Adding a Waitlist API with Supabase Edge Function The waitlist form is already configured to use a Supabase Edge Function endpoint. This keeps your Resend API key secure on the server side. **Step 1: Create the Waitlist Table** The waitlist table is included in the default Supabase schema. You have three options: **Option A: Use Migrations (Recommended)** The initial migration already includes the waitlist table: ```bash theme={null} # Link your project (one-time) supabase link --project-ref your-project-ref # Apply all migrations supabase db push ``` **Option B: Run the full schema** If you prefer not to use migrations, run the entire `supabase/schema.sql` file in your Supabase SQL Editor. It includes the waitlist table along with all other default tables. **Option C: Run just the waitlist table SQL** If you've already set up your database and just need the waitlist table, run this SQL in your Supabase SQL Editor: ```sql theme={null} -- Waitlist table for storing email addresses CREATE TABLE IF NOT EXISTS public.waitlist ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT NOT NULL UNIQUE, source TEXT DEFAULT 'marketing_page', user_agent TEXT, ip_address TEXT, email_sent BOOLEAN DEFAULT false, email_sent_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Enable Row Level Security ALTER TABLE public.waitlist ENABLE ROW LEVEL SECURITY; -- Allow anonymous inserts (for waitlist form submissions) CREATE POLICY "Anyone can add to waitlist" ON public.waitlist FOR INSERT WITH CHECK (true); -- Allow authenticated users to view waitlist CREATE POLICY "Authenticated users can view waitlist" ON public.waitlist FOR SELECT USING (auth.role() = 'authenticated'); -- Create indexes CREATE INDEX IF NOT EXISTS waitlist_email_idx ON public.waitlist(email); CREATE INDEX IF NOT EXISTS waitlist_created_at_idx ON public.waitlist(created_at DESC); ``` **Step 2: Deploy the Edge Function** 1. Install Supabase CLI (if not already installed): ```bash theme={null} yarn global add supabase # or: npm install -g supabase ``` 2. Login to Supabase: ```bash theme={null} supabase login ``` 3. Link your project: ```bash theme={null} supabase link --project-ref your-project-ref ``` 4. Initialize Supabase in your project (if not already done): ```bash theme={null} supabase init ``` 5. Create the edge function: ```bash theme={null} supabase functions new waitlist ``` This creates `supabase/functions/waitlist/index.ts`. Replace its contents with this code: ```typescript theme={null} import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } try { const supabaseUrl = Deno.env.get('SUPABASE_URL')! const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! const supabase = createClient(supabaseUrl, supabaseServiceKey) const resendApiKey = Deno.env.get('RESEND_API_KEY') const { email } = await req.json() if (!email || typeof email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return new Response( JSON.stringify({ error: 'Invalid email address' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const userAgent = req.headers.get('user-agent') || '' const ipAddress = req.headers.get('x-forwarded-for')?.split(',')[0] || '' const { data: waitlistEntry, error: dbError } = await supabase .from('waitlist') .upsert( { email: email.toLowerCase().trim(), source: 'marketing_page', user_agent: userAgent, ip_address: ipAddress, }, { onConflict: 'email', ignoreDuplicates: false } ) .select() .single() if (dbError) { return new Response( JSON.stringify({ error: 'Failed to save email' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Send confirmation email via Resend (if configured) let emailSent = false if (resendApiKey && waitlistEntry) { try { const resendResponse = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${resendApiKey}`, }, body: JSON.stringify({ from: Deno.env.get('RESEND_FROM_EMAIL') || 'onboarding@resend.dev', to: email, subject: Deno.env.get('RESEND_WAITLIST_SUBJECT') || 'Thanks for joining our waitlist!', html: Deno.env.get('RESEND_WAITLIST_HTML') || `

Thanks for joining our waitlist!

We'll notify you when we launch.

`, }), }) if (resendResponse.ok) { emailSent = true await supabase .from('waitlist') .update({ email_sent: true, email_sent_at: new Date().toISOString() }) .eq('id', waitlistEntry.id) } } catch (emailError) { console.error('Email sending error:', emailError) } } return new Response( JSON.stringify({ success: true, message: 'Email added to waitlist', email_sent: emailSent }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } catch (error) { return new Response( JSON.stringify({ error: error.message || 'Internal server error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } }) ``` 6. Deploy the edge function: ```bash theme={null} supabase functions deploy waitlist ``` 7. Set environment secrets in Supabase Dashboard: * Go to **Project Settings** → **Edge Functions** → **Secrets** * Add `RESEND_API_KEY` (your Resend API key) * Add `RESEND_FROM_EMAIL` (optional, e.g., `noreply@yourdomain.com`) * Add `RESEND_WAITLIST_SUBJECT` (optional, custom subject line) * Add `RESEND_WAITLIST_HTML` (optional, custom HTML email template) **Step 3: Configure the Frontend** Set the edge function endpoint in `apps/web/.env`: ```bash theme={null} VITE_WAITLIST_API_ENDPOINT=https://your-project-ref.supabase.co/functions/v1/waitlist ``` Replace `your-project-ref` with your actual Supabase project reference (found in your Supabase dashboard URL). **That's it!** The waitlist form will now: * Save emails to your Supabase database * Send confirmation emails via Resend (if configured) * Keep your Resend API key secure on the server ## Deployment ### Deploy to Vercel (Recommended) 1. Push your code to GitHub 2. Go to [vercel.com](https://vercel.com) 3. Import your repository 4. Set **Root Directory** to `apps/web` 5. Add environment variables from your `.env` file 6. Deploy! The landing page includes a `vercel.json` configuration file for easy deployment. ### Deploy to Netlify 1. Go to [netlify.com](https://netlify.com) 2. Import your repository 3. Set **Base directory** to `apps/web` 4. Set **Build command** to `yarn build` 5. Set **Publish directory** to `apps/web/dist` 6. Add environment variables 7. Deploy! ### Other Platforms The marketing page is a standard Vite app, so it works with: * Cloudflare Pages * GitHub Pages * AWS Amplify * Any static host Build command: `yarn build` (from `apps/web/`) Output directory: `dist/` ## SEO Configuration The landing page includes SEO components that set: * Meta title and description * Open Graph tags (for social sharing) * Twitter cards * Canonical URL Configure these in your `.env`: ```bash theme={null} VITE_APP_NAME=YourApp # Used as page title VITE_APP_DESCRIPTION=Your description # Used in meta description VITE_OG_IMAGE=https://yourapp.com/og-image.jpg # Social share image VITE_APP_URL=https://yourapp.com # Canonical URL ``` **Pro Tip:** Create an Open Graph image (1200x630px) for better social sharing. Place it in `/apps/web/public/og-image.jpg`. ## vs. Main Landing Page Repo You might be wondering: "What's the difference between `apps/web` and the `landing_page` repo?" | Feature | `apps/web` | `landing_page` | | ----------------- | ---------------------------------- | -------------------------------- | | **Purpose** | Your app's marketing page | Shipnative's own marketing | | **Included** | Yes, in the monorepo | Separate repository | | **Customization** | You own it - edit freely | Don't edit (it's for Shipnative) | | **Use case** | Pre-launch waitlist or launch page | Shipnative product marketing | | **Deploy** | Your domain | shipnative.com | **TL;DR:** Use `apps/web` for YOUR app. Ignore `landing_page` (unless you're contributing to Shipnative itself). ## Tips & Best Practices ### Pre-Launch Strategy 1. Start in **waitlist mode** while building 2. Share the link on Twitter, Product Hunt, Reddit 3. Collect 100-1000+ emails before launch 4. Send launch announcement when app goes live ### Launch Day 1. Switch to **launch mode** (`VITE_MODE=launch`) 2. Add your App Store / Play Store URLs 3. Redeploy 4. Email your waitlist! ### A/B Testing Want to test different copy? Create multiple deployments: * `waitlist-v1.yourapp.com` * `waitlist-v2.yourapp.com` Deploy the same code with different env vars and see which converts better. ### Custom Domain 1. Buy a domain (Namecheap, Google Domains, etc.) 2. Point it to your Vercel/Netlify deployment 3. Configure in your hosting dashboard 4. Update `VITE_APP_URL` to match ## Troubleshooting * Make sure your `.env` file is in `apps/web/`, not the monorepo root * Restart the dev server after changing `.env` * Vite requires `VITE_` prefix for all public env vars * Check for typos in variable names * Verify the image exists in `/apps/web/public/` * Check that `VITE_APP_SCREENSHOT_URL` matches your filename * Try using an absolute URL instead of a relative path * Clear browser cache * Check browser console for errors * Verify `VITE_WAITLIST_API_ENDPOINT` is correctly set * Test your API endpoint with Postman/Insomnia first * Check CORS settings on your API * Run `yarn install` in `apps/web/` * Verify `tailwind.config.cjs` exists * Check that `src/index.css` is imported in `src/main.tsx` * Try `yarn build` to see if it's a dev-only issue ## Next Steps Get your landing page live on Vercel in 5 minutes Start building your actual mobile app with vibecoding Track waitlist signups and page views with PostHog Point your domain to your landing page # Project Structure Source: https://docs.shipnative.app/getting-started/project-structure Understand the Shipnative monorepo layout Shipnative is a **monorepo** - one repository with multiple apps and shared packages. Here's how it's organized. ## What You Cloned When you run `git clone https://github.com/shipnativeapp/shipnative.git`, you get this structure: ``` shipnative/ # ← This is the monorepo (what you cloned) │ ├── apps/ │ ├── app/ # React Native mobile app (Expo) │ └── web/ # Marketing landing page (Vite) │ ├── convex/ # Convex backend (if using Convex) │ ├── schema.ts # Database schema │ ├── auth.ts # Auth configuration │ └── ... # Your custom functions │ ├── supabase/ # Supabase config (if using Supabase) │ ├── vibe/ # AI context files for vibecoding │ ├── SERVICES.md # Service architecture overview │ ├── MOCK_SERVICES.md # How mock mode works │ └── CONVEX.md # Convex-specific patterns │ ├── AGENTS.md # AI context (tech stack, directory map) ├── package.json # Monorepo root ├── turbo.json # Turborepo configuration └── yarn.lock # Dependency lock file ``` **This is what matters to you as a Shipnative customer.** Everything else you might see in external documentation or development setups is for Shipnative's internal development and maintenance. ## The Two Main Apps ### Mobile App (`apps/app/`) Your primary React Native application. This is where you'll spend most of your time building features. ``` apps/app/ │ ├── app/ # Source code │ ├── screens/ # Screen components │ ├── components/ # Reusable UI components │ ├── navigators/ # React Navigation setup │ ├── stores/ # Global state (Zustand) │ ├── services/ # External services (Supabase, RevenueCat, etc.) │ │ └── mocks/ # Mock implementations │ ├── hooks/ # Custom React hooks │ │ ├── supabase.ts # useSupabaseAuth hook │ │ ├── convex.ts # useConvexAuth hook │ │ └── useAppAuth.ts # Backend-agnostic auth hook │ ├── providers/ # React providers │ │ └── BackendProvider.tsx # Supabase or Convex provider │ ├── utils/ # Helper functions │ ├── theme/ # Design system │ │ └── unistyles.ts # Theme configuration │ ├── config/ # App configuration │ │ └── env.ts # Environment & backend detection │ └── app.tsx # App entry point │ ├── assets/ # Images, fonts, etc. ├── .env # API keys (git-ignored) ├── app.json # Expo configuration ├── index.tsx # Entry point └── package.json # App dependencies ``` **Key folders to know:** Pre-built components like `Button`, `Card`, `TextField`, etc. Use these instead of building from scratch. ```typescript theme={null} import { Button, Card, Text } from '@/components' ``` Native auth hooks for each backend, plus a unified hook. ```typescript theme={null} // Backend-specific (recommended for full control) import { useSupabaseAuth } from '@/hooks/supabase' import { useConvexAuth } from '@/hooks/convex' // Backend-agnostic (for shared components) import { useAppAuth } from '@/hooks/useAppAuth' ``` Zustand stores for app-wide state like subscriptions and preferences. ```typescript theme={null} import { useSubscriptionStore } from '@/stores/subscriptionStore' const { isPro, checkEntitlement } = useSubscriptionStore() ``` Service clients for Supabase, RevenueCat, PostHog, etc. Automatically uses mocks when API keys are missing. ```typescript theme={null} import { supabase } from '@/services/supabase' ``` Unistyles theme with colors, spacing, typography. Always use theme values instead of hardcoding. ```typescript theme={null} import { useUnistyles } from 'react-native-unistyles' const { theme } = useUnistyles() theme.colors.primary theme.spacing.md ``` ### Convex Backend (`convex/`) If using Convex, your backend functions live at the root level: ``` convex/ │ ├── _generated/ # Auto-generated types (don't edit) ├── schema.ts # Database schema ├── auth.ts # Auth configuration ├── users.ts # User queries and mutations ├── realtime.ts # Realtime features (chat, presence) └── ... # Your custom functions ``` ### Marketing Page (`apps/web/`) A Vite + React application for your app's marketing/landing page. ``` apps/web/ │ ├── src/ │ ├── app/ # Next.js App Router pages │ ├── components/ # React components │ └── lib/ # Utilities │ ├── public/ # Static assets └── package.json # Web app dependencies ``` ## Documentation Files The root directory contains markdown documentation for developers: | File | Purpose | | -------------------- | ------------------------------- | | `README.md` | Quick start and overview | | `SUPABASE.md` | Authentication & database setup | | `MONETIZATION.md` | RevenueCat payments setup | | `ANALYTICS.md` | PostHog & Sentry setup | | `NOTIFICATIONS.md` | Push notifications setup | | `DEPLOYMENT.md` | App store deployment guide | | `TROUBLESHOOTING.md` | Common issues and solutions | | `DESIGN_SYSTEM.md` | Design tokens and patterns | | `BACKEND.md` | Database schema | ## The AI Documentation System (Layered Context) Shipnative uses a **Layered Context** architecture. This ensures that AI agents get the right amount of information at the right time, minimizing "token waste." ### 1. Discovery Layer (`AGENTS.md`) Follows the [AGENTS.md standard](https://agents-md.com). These are "Front Desk" files that agents read first to understand the directory structure and constraints. * **Root**: `shipnative/AGENTS.md` * **Mobile**: `apps/app/AGENTS.md` * **Web**: `apps/web/AGENTS.md` ### 2. Specification Layer (`vibe/` folders) These are the **"Brain"** of the project, containing deep-dive technical specs. * **App Logic**: `apps/app/vibe/` (Styling, Architecture, Screen Templates) * **Infrastructure**: `vibe/` (Service architecture, Mock mode, Convex patterns) ### 3. Tech Stack Rules (in `AGENTS.md`) The root `AGENTS.md` contains the tech stack rules (e.g., "Always use Unistyles") that agents must follow. *** ## Configuration Files ### Root Level * **`package.json`** - Monorepo dependencies and scripts * **`turbo.json`** - Turborepo build configuration * **`tsconfig.json`** - TypeScript configuration * **`AGENTS.md`** - AI context (tech stack, directory map) * **`.cursorrules`** - Points to AGENTS.md ### App Level (`apps/app/`) * **`app.json`** - Expo configuration (app name, bundle ID, etc.) * **`.env`** - Environment variables (API keys, backend provider) * **`package.json`** - App-specific dependencies * **`tsconfig.json`** - TypeScript configuration ### Convex Level (`convex/`) * **`schema.ts`** - Convex database schema * **`auth.ts`** - Auth configuration ## How the Monorepo Works **Turborepo** manages the monorepo. When you run commands from the root: ```bash theme={null} yarn dev # Runs the mobile app (Expo) in dev mode yarn app:ios # Runs mobile app on iOS yarn web # Runs the marketing landing page (apps/web) yarn app:web # Runs the Expo web build of the app yarn build # Builds all apps ``` Each app can also be run independently: ```bash theme={null} cd apps/app yarn ios # Run mobile app cd apps/web yarn dev # Run landing page # Convex dev server (from root) npx convex dev # Run Convex dev server (if using Convex) ``` ## Where to Start Start in `apps/app/app/` - build screens and components. Let AI help you! Browse `apps/app/app/components/` to see what's available, then check `apps/app/app/screens/` for screen examples. ## Finding What You Need Add it to `apps/app/app/screens/`. Then register it in `apps/app/app/navigators/AppNavigator.tsx` or `MainTabNavigator.tsx`: * For main flow screens: Add to `AppNavigator.tsx` * For tab screens: Add to `MainTabNavigator.tsx` * Example: `apps/app/app/screens/ProfileScreen.tsx` Add it to `apps/app/app/components/`. Export it from `apps/app/app/components/index.ts` so others can import it easily. ```typescript theme={null} // In ProfileCard.tsx export const ProfileCard = () => { ... } // In index.ts export * from "./ProfileCard" ``` Create a Zustand store in `apps/app/app/stores/`. Follow the pattern in existing stores like `subscriptionStore.ts`. Edit `apps/app/app/theme/unistyles.ts`. This file defines colors, spacing, typography, shadows, and all design tokens. **Supabase:** Use the Supabase client in `apps/app/app/services/supabase.ts` or create Edge Functions. **Convex:** Add functions to `convex/`. Create queries, mutations, and actions as needed. **Supabase:** Check `apps/app/app/hooks/supabase.ts` for the `useSupabaseAuth` hook. **Convex:** Check `apps/app/app/hooks/convex.ts` for the `useConvexAuth` hook. **Backend-agnostic:** Use `apps/app/app/hooks/useAppAuth.ts` for components that work with either backend. ## Next Steps Use AI to build features See what's available Learn about mock mode Set up Supabase or Convex # Quickstart Source: https://docs.shipnative.app/getting-started/quickstart Start building in 2 minutes ## Prerequisites Before you start, install these tools: **Recommended: Use Volta** (automatically manages Node versions) ```bash theme={null} curl https://get.volta.sh | bash # macOS/Linux volta install node@20.19.0 yarn@4.9.1 ``` **Alternative: Use nvm** ```bash theme={null} curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install 20 nvm use 20 ``` **Verify installation:** ```bash theme={null} node -v # Should show v20.x.x ``` If you used Volta above, Yarn is already installed. Otherwise: ```bash theme={null} corepack enable # Enables Yarn (included with Node) ``` **Verify installation:** ```bash theme={null} yarn -v # Should show 4.x.x ``` **macOS:** Install Xcode Command Line Tools ```bash theme={null} xcode-select --install ``` **Windows:** Download from [git-scm.com](https://git-scm.com/download/win) **Linux:** ```bash theme={null} sudo apt install git # Ubuntu/Debian ``` 1. Install [Xcode from the Mac App Store](https://apps.apple.com/app/xcode/id497799835) 2. Open Xcode once and accept the license agreement 3. Install iOS Simulator (Xcode → Settings → Platforms → iOS) Xcode is \~12GB and only available on macOS. 1. Download [Android Studio](https://developer.android.com/studio) 2. During setup, install the Android SDK 3. Create an emulator: Tools → Device Manager → Create Device Works on macOS, Windows, and Linux. *** ## Setup ```bash theme={null} git clone https://github.com/shipnativeapp/shipnative.git cd shipnative yarn install yarn setup # Optional: configure app name and services cd apps/app yarn ios # or: yarn android, yarn web ``` **Mock mode is enabled by default.** You can skip `yarn setup` entirely and start building immediately. All services (auth, database, payments, analytics) work with simulated data. Configure real services when you're ready for production. *** ## Development Workflow **Important:** `yarn ios` / `yarn android` compiles native code, which takes several minutes. You only need to run this once (or when native dependencies change). **For daily development, use this workflow:** ```bash theme={null} # First time only (builds native app) cd apps/app yarn ios # Takes 3-5 minutes # Daily development (fast refresh) yarn start # or: yarn dev ``` The dev server provides instant hot reload for JavaScript/TypeScript changes. You only need to rebuild with `yarn ios` when: * Adding/removing native dependencies * Changing `app.json` configuration * Modifying native code (Swift/Kotlin) * The app gets uninstalled from the simulator *** ## Backend Choice During `yarn setup`, you'll choose between: | Backend | Best For | | ------------ | ------------------------------------------------------------ | | **Supabase** | SQL apps, PostgreSQL experience, RLS-based security | | **Convex** | TypeScript-first apps, reactive queries, function-level auth | Both work identically in mock mode. Choose based on your preference. After you choose, setup automatically removes the other backend's code and packages. You'll have a clean codebase with only your chosen backend. *** ## What's Included The app ships with working screens: * **Welcome/Login/Signup** - Full auth flow (Supabase or Convex) * **Home** - Main app screen * **Profile** - User settings and account deletion * **Paywall** - Subscription UI with mock purchases * **Dev Menu** - `Cmd+D` (iOS) / `Cmd+M` (Android) for debugging *** ## AI-Assisted Development Open your project in [Cursor](https://cursor.sh/) or Claude Code. To build features: ``` Read AGENTS.md. I want to build [describe your feature]. ``` The AI reads the `vibe/` context files and follows project patterns automatically. *** ## Key Directories | Location | Contents | | -------------------------- | ------------------------------------------------------- | | `apps/app/app/screens/` | Screen components | | `apps/app/app/components/` | Reusable UI components | | `apps/app/app/stores/` | Zustand stores (auth, subscriptions) | | `apps/app/app/services/` | Service clients (Supabase, Convex, RevenueCat, PostHog) | | `apps/app/app/hooks/` | Auth hooks (backend-specific after setup) | | `apps/app/app/theme/` | Unistyles theme tokens | | `convex/` | Convex backend functions (only if using Convex) | | `vibe/` | AI context files | *** ## Dev Menu Access via `Cmd+D` (iOS) / `Cmd+M` (Android): * Toggle free/pro subscription * Switch light/dark mode * View component showcase * Check which services are mocked *** ## Before Production 1. Configure backend credentials (Supabase or Convex) 2. Generate Android release keystore 3. Update bundle identifiers in `app.json` See [Full Setup Guide](/getting-started/installation). *** ## Next Steps How mock services work Choose and configure your backend Configure all services Ship to app stores # Running Your App Source: https://docs.shipnative.app/getting-started/running-the-app Launch your Shipnative app on iOS, Android, or Web Once you've cloned the repo and run `yarn install`, you're ready to launch! This guide covers iOS, Android, and Web. **Fast path:** You can launch without API keys (mock mode is automatic), but we recommend running `yarn setup` early so you test against your real services. ## Quick Commands From the `apps/app` directory: ```bash theme={null} cd apps/app yarn ios # iOS Simulator yarn android # Android Emulator yarn web # Web Browser (localhost:19006) ``` Or from the monorepo root: ```bash theme={null} yarn app:ios # iOS yarn app:android # Android yarn app:web # Web ``` Your app will use real services if `.env` is configured; otherwise it falls back to mock mode. Add real API keys early for more accurate behavior. *** ## Platform Details **Requirements:** * Xcode installed (latest version recommended) * iOS Simulator available ```bash theme={null} cd apps/app yarn ios ``` **First run takes longer** - Expo needs to build the native iOS project. Subsequent runs are much faster. **Using a specific simulator:** ```bash theme={null} # List available simulators xcrun simctl list devices available # Run on specific device expo run:ios --simulator "iPhone 15 Pro" ``` **Requirements:** * Android Studio installed * Android Emulator configured (or physical device) * `ANDROID_HOME` environment variable set ```bash theme={null} cd apps/app yarn android ``` **First run takes longer** - Expo needs to build the native Android project. **Troubleshooting:** * Make sure an emulator is running or a device is connected * Run `adb devices` to verify your device is recognized **Fully supported!** Shipnative runs on web via Expo Web and react-native-web. ```bash theme={null} cd apps/app yarn web ``` Opens at `http://localhost:19006` **What works on web:** * All UI components (Unistyles 3.0 supports web) * Authentication (Supabase works everywhere) * Payments (RevenueCat Web Billing) * Analytics (PostHog) * Dark mode * Responsive design **Production build:** ```bash theme={null} yarn bundle:web ``` *** ## Mock Mode vs Real Services When you run the app without API keys configured, **mock mode activates automatically**: | Service | Mock Behavior | | ------------- | ---------------------------------------------------------- | | **Auth** | Login/signup works with fake accounts | | **Payments** | Purchases succeed instantly, toggle Pro status in dev menu | | **Analytics** | Events logged to console | | **Errors** | Captured locally, not sent to Sentry | **We recommend connecting real services early!** While mocks are convenient for initial exploration, real services behave differently: * Email confirmation and OAuth require real Supabase * Payment flows need RevenueCat products configured * Analytics only capture real data with PostHog connected **Run `yarn setup` to configure services interactively.** **How to check service status:** Open the Developer Menu (`Cmd+D` on iOS, `Cmd+M` on Android) and check "Service Status". [Learn more about mock services →](/development/mock-services) *** ## Developer Menu Access the dev menu to debug and test: | Platform | How to Open | | -------------------- | ----------------------------- | | **iOS Simulator** | `Cmd + D` | | **Android Emulator** | `Cmd + M` (Mac) or `Ctrl + M` | | **Physical Device** | Shake the device | | **Web** | Browser DevTools | **Useful options:** * Component Showcase - see all UI components * Toggle dark/light mode * Switch between Free/Pro user * View service status (mock vs real) * Reload the app *** ## Troubleshooting Clear the cache and restart: ```bash theme={null} yarn app:start --clear ``` Or kill the process on port 8081: ```bash theme={null} lsof -ti:8081 | xargs kill -9 ``` Clean and reinstall CocoaPods: ```bash theme={null} cd apps/app/ios rm -rf Pods Podfile.lock pod install cd .. yarn ios ``` Set your `ANDROID_HOME` environment variable: ```bash theme={null} export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/platform-tools ``` Add this to your `~/.zshrc` or `~/.bashrc`. 1. Open the dev menu 2. Ensure "Fast Refresh" is enabled 3. If still not working, restart Metro: `yarn app:start --clear` *** ## Next Steps Understand where things live in the codebase Build features with AI assistance # Upgrading Source: https://docs.shipnative.app/getting-started/upgrading How to update your app with the latest Shipnative changes When we release updates to Shipnative, you may want to incorporate them into your project. Since you own your codebase and likely have custom modifications, updates require a manual merge process. **Your app is yours.** Unlike a dependency you update via npm, Shipnative is a starting point you've customized. Updates are applied by merging our changes with yours, not by replacing your code. ## Choose Your Workflow There are two main workflows for staying updated. **We recommend the Upstream Remote workflow** for most users. **Best for most users.** Keep your own repo structure, pull updates when needed. Simpler setup, works immediately. **Best if you want to contribute back.** Fork the repo, sync periodically. Requires accepting org invitation first. | Aspect | Upstream Remote | Fork-Based | | ---------------------- | -------------------------- | ------------------------------ | | **Setup complexity** | Simple - just add a remote | Requires org invitation + fork | | **Can contribute PRs** | No | Yes | | **Repo ownership** | Your own repo from scratch | Fork of Shipnative | | **Recommended for** | Most users | Contributors | *** ## Before Any Upgrade 1. **Commit all your current work** - Ensure your working directory is clean 2. **Check the [CHANGELOG](https://github.com/shipnativeapp/shipnative/blob/main/CHANGELOG.md)** - Review what changed and whether you need/want the updates 3. **Back up your branch** - Create a backup branch before merging ```bash theme={null} # Ensure everything is committed git status # Create a backup branch git checkout -b backup/pre-upgrade-$(date +%Y%m%d) git checkout main # or your primary branch ``` *** ## Workflow 1: Upstream Remote (Recommended) This is the simplest approach. You keep your own repo and pull updates from Shipnative when needed. ### Initial Setup (One-Time) After cloning Shipnative, set up your remotes: ```bash theme={null} # Rename the original remote to 'upstream' git remote rename origin upstream # Add your own repo as 'origin' git remote add origin https://github.com/YOUR_USERNAME/your-app.git # Verify setup git remote -v # origin https://github.com/YOUR_USERNAME/your-app.git (fetch) # origin https://github.com/YOUR_USERNAME/your-app.git (push) # upstream https://github.com/shipnativeapp/shipnative.git (fetch) # upstream https://github.com/shipnativeapp/shipnative.git (push) # Push to your repo git push -u origin main ``` **Already disconnected the remote?** No problem - just add it back: ```bash theme={null} git remote add upstream https://github.com/shipnativeapp/shipnative.git ``` ### Pulling Updates When you want to incorporate Shipnative updates: ```bash theme={null} # Fetch latest from Shipnative git fetch upstream # Create an upgrade branch (don't merge directly to main) git checkout -b upgrade/shipnative-$(date +%Y%m%d) # Merge upstream changes git merge upstream/main --no-commit ``` The `--no-commit` flag lets you review changes before committing. ### Resolve Conflicts If there are conflicts: ```bash theme={null} git status # Shows conflicted files ``` For each conflicted file: 1. Open the file and look for conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) 2. Decide which changes to keep (yours, theirs, or a combination) 3. Remove the conflict markers 4. Stage the resolved file: `git add ` ### Complete the Upgrade ```bash theme={null} # After resolving conflicts and testing git commit -m "Upgrade to Shipnative $(date +%Y%m%d)" # Merge to main git checkout main git merge upgrade/shipnative-$(date +%Y%m%d) # Push to your repo git push origin main ``` *** ## Workflow 2: Fork-Based Use this if you want to contribute changes back to Shipnative via pull requests. **Prerequisite:** You must accept the organization invitation first. Check your GitHub notifications or email for an invite to join `shipnativeapp`. Once accepted, you can fork the private repo. ### Initial Setup (One-Time) 1. **Accept org invitation** - Check GitHub notifications 2. **Fork the repo** - Go to [https://github.com/shipnativeapp/shipnative](https://github.com/shipnativeapp/shipnative) and click "Fork" 3. **Clone your fork:** ```bash theme={null} git clone https://github.com/YOUR_USERNAME/shipnative.git my-app cd my-app # Add upstream remote pointing to original git remote add upstream https://github.com/shipnativeapp/shipnative.git # Verify setup git remote -v # origin https://github.com/YOUR_USERNAME/shipnative.git (fetch) # origin https://github.com/YOUR_USERNAME/shipnative.git (push) # upstream https://github.com/shipnativeapp/shipnative.git (fetch) # upstream https://github.com/shipnativeapp/shipnative.git (push) ``` ### Syncing Your Fork When you want to incorporate Shipnative updates: ```bash theme={null} # Fetch latest from original repo git fetch upstream # Create upgrade branch git checkout -b upgrade/sync-$(date +%Y%m%d) # Merge upstream changes git merge upstream/main --no-commit # Resolve conflicts, test, then commit git commit -m "Sync with upstream $(date +%Y%m%d)" # Merge to main and push to your fork git checkout main git merge upgrade/sync-$(date +%Y%m%d) git push origin main ``` ### Contributing Back If you fix a bug or add a feature others might benefit from: ```bash theme={null} # Create a feature branch from latest upstream git fetch upstream git checkout -b fix/your-bugfix upstream/main # Make your changes, commit git add . git commit -m "Fix: description of fix" # Push to your fork git push origin fix/your-bugfix # Create PR on GitHub from your fork to shipnativeapp/shipnative ``` *** ## Additional Methods ### Cherry-Pick Specific Changes When you only want specific commits (e.g., a bug fix or new feature): ```bash theme={null} git fetch upstream git log upstream/main --oneline # Find commit hashes git checkout -b feature/specific-update git cherry-pick abc1234 # Cherry-pick by commit hash git cherry-pick def5678 ``` If conflicts occur: ```bash theme={null} # Resolve conflicts, then: git cherry-pick --continue ``` ### Manual Copy For small updates or when git conflicts are overwhelming: 1. Check the [CHANGELOG](https://github.com/shipnativeapp/shipnative/blob/main/CHANGELOG.md) for what changed 2. Browse the [commit history](https://github.com/shipnativeapp/shipnative/commits/main) to see file changes 3. Manually copy the relevant code changes into your project 4. Test thoroughly This method is error-prone for large updates. Prefer git merge when possible. *** ## Post-Upgrade Checklist After any upgrade: ```bash theme={null} yarn install ``` New packages may have been added. Compare your `.env` with `.env.example`: ```bash theme={null} diff apps/app/.env apps/app/.env.example ``` Add any new variables. For Supabase: ```bash theme={null} supabase db push ``` For Convex: ```bash theme={null} npx convex dev ``` If native code changed (new packages, config changes): ```bash theme={null} cd apps/app yarn prebuild:clean yarn ios # or yarn android ``` * Sign up / Sign in * Profile updates * Payments (in sandbox) * Push notifications *** ## Handling Common Conflicts ### app.json Conflicts Keep your app name, bundle ID, and scheme. Accept new Expo SDK versions or plugin configurations: ```json theme={null} { "expo": { "name": "YOUR APP NAME", // Keep yours "slug": "your-app-slug", // Keep yours "version": "1.0.0", // Keep yours "sdkVersion": "52.0.0", // Accept theirs (usually) "ios": { "bundleIdentifier": "com.you.yourapp" // Keep yours } } } ``` ### package.json Conflicts Generally accept the upstream dependency versions unless you have a specific reason to stay on an older version: ```bash theme={null} # After resolving package.json yarn install ``` ### Screen/Component Conflicts If you've heavily modified a screen: 1. Review what the upstream change does 2. Decide if you need it 3. If yes, manually integrate the logic into your version 4. If no, keep your version *** ## Staying Informed All releases documented with migration notes Announcements and upgrade help from the community *** ## FAQ **Use Upstream Remote** (recommended) unless you specifically want to contribute PRs back to Shipnative. It's simpler to set up and works the same way for pulling updates. There's no fixed schedule. Check the CHANGELOG periodically (monthly is reasonable) and upgrade when you see changes relevant to your app - security fixes, features you want, or bug fixes you've encountered. Yes. Each upgrade merges the latest changes regardless of how many versions you've skipped. However, larger gaps mean more potential conflicts. Ask in Discord! Share the conflicted file and your use case, and we'll help you work through it. Yes! Just add the upstream remote again: ```bash theme={null} git remote add upstream https://github.com/shipnativeapp/shipnative.git git fetch upstream ``` You need to accept your organization invitation first. Check your GitHub notifications or email for an invite to join `shipnativeapp`. Once you're an org member, you can fork private repos. # Shipnative Source: https://docs.shipnative.app/index Production-ready React Native starter kit

Ship your app in days, not months

Production-ready React Native boilerplate with auth, payments, analytics, and more. Build for iOS, Android, and Web from a single codebase.

Get Shipnative →
## What's Included Everything you need to launch a professional mobile app: Supabase (PostgreSQL) or Convex (TypeScript-first) Email, Google, Apple sign-in with either backend RevenueCat subscriptions across all platforms PostHog analytics and feature flags Sentry crash reporting and monitoring Unistyles 3.0 theming built-in ## Get Started Running in 2 minutes with mock services Configure real services step-by-step ```bash theme={null} git clone https://github.com/shipnativeapp/shipnative.git cd shipnative && yarn install && yarn setup cd apps/app && yarn ios ``` ## Why Shipnative? Start building immediately without configuring any services. Switch to real APIs when ready. AGENTS.md + vibe docs let AI tools understand and extend your codebase. Single React Native codebase deploys to iOS, Android, and Web. Battle-tested patterns and EAS deployment guides included. ***

Ready to ship your app?

Get Shipnative →
# Troubleshooting Source: https://docs.shipnative.app/troubleshooting Common issues and solutions ## Setup Issues ### `yarn install` Fails ```bash theme={null} rm -rf node_modules apps/*/node_modules yarn.lock yarn install ``` ### Environment Variables Not Loading 1. `.env` must be in `apps/app/` directory 2. Variables must start with `EXPO_PUBLIC_` 3. Restart Metro with cache clear: ```bash theme={null} yarn app:start --clear ``` *** ## Metro Bundler ### Metro Won't Start ```bash theme={null} rm -rf apps/app/.expo apps/app/node_modules/.cache lsof -ti:8081 | xargs kill -9 yarn app:start --clear ``` ### "Unable to resolve module" Check `apps/app/tsconfig.json` has correct path mappings: ```json theme={null} { "compilerOptions": { "paths": { "@/*": ["./app/*"] } } } ``` Then clear Metro cache: `yarn app:start --clear` *** ## iOS Issues ### Simulator Won't Launch ```bash theme={null} xcrun simctl list devices xcrun simctl boot "iPhone 15 Pro" yarn app:ios ``` ### "Command PhaseScriptExecution failed" ```bash theme={null} cd apps/app/ios rm -rf Pods Podfile.lock pod install --repo-update cd ../../.. yarn app:ios ``` ### White Screen Check console for JavaScript errors. Add `console.log` in `App.tsx` to debug. *** ## Android Issues ### Java Version Error If you see an error like: ``` Error resolving plugin [id: 'com.facebook.react.settings'] > 25.0.1 ``` **Cause**: Java 25 is too new. The `25.0.1` is your Java version, not a React Native version. **Solution**: Use Java 17 (LTS): ```bash theme={null} # Check available versions /usr/libexec/java_home -V # Build with Java 17 JAVA_HOME=$(/usr/libexec/java_home -v 17) yarn android # Or set permanently in ~/.zshrc export JAVA_HOME=$(/usr/libexec/java_home -v 17) ``` Install Java 17 via Homebrew: `brew install --cask temurin@17` ### SDK Location Not Found ```bash theme={null} export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/platform-tools ``` Or create `apps/app/android/local.properties`: ``` sdk.dir=/Users/YOUR_USERNAME/Library/Android/sdk ``` ### Gradle Build Fails ```bash theme={null} cd apps/app/android ./gradlew clean cd ../../.. yarn app:android ``` *** ## EAS Build Issues ### Yarn 4.x / Corepack Errors If EAS Build fails with errors like: * `EEXIST: file already exists` * `ETARGET No matching version found for yarn@4.9.1` * `Usage Error: This project is configured to use yarn` version mismatch **This is already fixed in the boilerplate.** Shipnative bundles Yarn 4.x directly in `.yarn/releases/`, bypassing the corepack/npm conflict. If you're on an older version, upgrade by pulling the latest changes: ```bash theme={null} # Download and bundle yarn mkdir -p .yarn/releases curl -L -o .yarn/releases/yarn-4.9.1.cjs \ https://repo.yarnpkg.com/4.9.1/packages/yarnpkg-cli/bin/yarn.js # Update .yarnrc.yml to use bundled yarn echo "yarnPath: .yarn/releases/yarn-4.9.1.cjs" >> .yarnrc.yml # Commit it (the yarn binary is ~3MB) git add .yarn/releases/ .yarnrc.yml git commit -m "Bundle yarn 4.9.1 for EAS compatibility" ``` Do NOT use `corepack: true` or `yarn: "4.x.x"` in eas.json. The bundled yarn approach handles everything automatically. *** ## Runtime Errors ### Navigation Errors * Screen must be registered in `app/navigators/` * Screen names are case-sensitive * Check navigator nesting ### "Maximum update depth exceeded" Infinite re-render loop. Don't call `setState` in render: ```typescript theme={null} // BAD const MyComponent = () => { const [count, setCount] = useState(0) setCount(count + 1) // Infinite loop! return } // GOOD const MyComponent = () => { const [count, setCount] = useState(0) useEffect(() => { setCount(c => c + 1) }, []) return } ``` ### Authentication Loop * Check auth state persistence in `useAuthStore` * Don't navigate in render functions - use `useEffect` *** ## Social Login & Magic Link Issues | Issue | Solution | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | No redirect after OAuth login | Add `[scheme]://auth/callback` to Supabase URL Configuration | | Magic link redirects to wrong URL | Add `http://localhost:3000/auth/callback` (Vite) or `http://localhost:19006/auth/callback` (Expo web) to Supabase URL Configuration | | Nonce error with Google | Disable "Skip Nonce Check" in Supabase Auth → Google | | iOS Google not working | Set `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID` and rebuild | *** ## Deep Linking ### Testing Deep Links on iOS Simulator **Dev vs Production URL schemes are different!** Expo development builds register `exp+` as the URL scheme, not just ``. ```bash theme={null} # For development builds (what you get with `yarn ios`) xcrun simctl openurl booted "exp+yourscheme://profile" # For production/release builds (EAS builds, TestFlight, App Store) xcrun simctl openurl booted "yourscheme://profile" ``` To verify which scheme your app is using, check the built app's `Info.plist` - look for `CFBundleURLSchemes`. ### Deep Link Not Working 1. **Verify the scheme in `app.json`:** ```json theme={null} { "expo": { "scheme": "yourscheme" } } ``` 2. **Check you're using the correct prefix:** * Dev build: `exp+yourscheme://` * Prod build: `yourscheme://` 3. **Ensure the path is configured in your navigation:** * Check `apps/app/app/navigators/linking.ts` *** ## RevenueCat / Payments ### "Invalid API Key" Error If you see `[RevenueCat] There was a credentials issue. Invalid API Key.`: **Cause**: Your `.env` has placeholder values like `EXPO_PUBLIC_REVENUECAT_IOS_KEY=your-ios-key`. These trigger the SDK with invalid credentials instead of mock mode. **Solution**: Use empty values for mock mode: ```bash theme={null} # In apps/app/.env EXPO_PUBLIC_REVENUECAT_IOS_KEY= EXPO_PUBLIC_REVENUECAT_ANDROID_KEY= EXPO_PUBLIC_REVENUECAT_WEB_KEY= ``` The app now auto-detects placeholder values (starting with `your-`) and falls back to mock mode. ### StoreKit "No Active Account" (iOS Simulator) These errors are **expected and harmless**: ``` [StoreKit] Error Code=509 "No active account" ``` The simulator has no Apple ID. Purchases still work - you'll sign in with a sandbox account when testing. ### Purchase Fails 1. Verify API keys in `apps/app/.env` 2. Check products are configured in RevenueCat dashboard 3. Use sandbox/test accounts *** ## Performance ### Slow List Scrolling ```typescript theme={null} item.id.toString()} removeClippedSubviews={true} maxToRenderPerBatch={10} initialNumToRender={10} windowSize={5} /> ``` Use `useCallback` for `renderItem` and `React.memo` for list items. *** ## Development Tools ### TypeScript Not Working in VS Code 1. `Cmd+Shift+P` → "TypeScript: Restart TS Server" 2. Ensure using workspace TypeScript version 3. Reload window if needed ### Hot Reload Not Working ```bash theme={null} yarn app:start --clear ``` *** ## Supabase Issues ### Profile 404 Errors After Signup If you see 404 errors in the browser console when fetching profile data after signup: ``` GET https://your-project.supabase.co/rest/v1/profiles?select=... 404 ``` **This is usually expected and handled gracefully by the app.** The profile creation trigger may not have completed yet. **If the errors persist:** 1. **Verify database schema is applied** ```bash theme={null} supabase db push # Using migrations ``` Or run the schema manually in SQL Editor 2. **Check RLS policies** - Ensure users can read their own profiles 3. **Verify the trigger is working** - The `handle_new_user()` function should auto-create profiles 4. **Configure redirect URLs** - Set to correct port (8081 for Expo, not 3000) in Supabase Dashboard → Authentication → URL Configuration See `supabase/TROUBLESHOOTING.md` in your project for detailed solutions. ### TypeScript Errors with React Native If you see errors like: ``` error TS6200: Definitions conflict with those in another file: BlobPart, FormData... error TS2403: Variable 'require' must be of type 'Require'... ``` **This is already fixed in the boilerplate.** If you encounter it: * Ensure `tsconfig.json` has `lib: ["esnext"]` (no "dom") * Ensure `types: ["jest"]` (no "node") * Remove `typeRoots` if present ### Migration Errors If migrations fail with "already exists" errors: ```bash theme={null} # Use IF NOT EXISTS in your migrations CREATE TABLE IF NOT EXISTS public.my_table (...); # Or mark migration as applied (if schema already matches) supabase migration repair --status applied MIGRATION_ID ``` *** ## Vercel Deployment Issues ### Missing Environment Variables on Vercel If your Vercel deployment fails with: ``` Missing required environment variables: supabaseUrl, supabasePublishableKey ``` **This is already fixed in the boilerplate.** The `app.config.ts` includes an `extra` section that passes environment variables through to `Constants.expoConfig.extra` for web builds. If you're on an older boilerplate version, see [Web Deployment → Troubleshooting](/deployment/web#troubleshooting) for the fix. *** ## Getting Help 1. [Expo Documentation](https://docs.expo.dev/) 2. [Shipnative GitHub Issues](https://github.com/shipnativeapp/shipnative/issues) 3. Check Sentry dashboard for production errors **Debug tip:** ```bash theme={null} EXPO_DEBUG=true yarn app:ios ```