Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt

Use this file to discover all available pages before exploring further.

Native Widgets

Shipnative includes preconfigured native widgets for both iOS and Android platforms, with seamless Supabase integration for real-time data display.

Overview

Widgets allow users to view key information from your app directly on their home screen without opening the app. Shipnative provides:
  • iOS Widgets - Built with SwiftUI, supporting all widget sizes
  • Android Widgets - Built with Kotlin, supporting all widget sizes
  • Supabase Integration - Automatic data fetching with authentication
  • Easy Styling - Theme-aware widgets that match your app design
  • Feature Flag - Easily enable/disable widgets via configuration

Quick Start

1. Enable Widgets

Widgets are disabled by default. To enable them, add to your .env file (or run yarn setup and choose Widgets):
EXPO_PUBLIC_ENABLE_WIDGETS=true
APP_GROUP_IDENTIFIER=group.com.yourcompany.yourapp # yarn setup will prompt
APPLE_TEAM_ID=XXXXXXXXXX # Required for iOS widgets
Finding your Apple Team ID: Open Xcode, go to Preferences → Accounts, select your Apple ID, and look for your team. The Team ID is a 10-character alphanumeric string. You can also find it in your Xcode project under Signing & Capabilities after selecting a team.
Or set it in your environment:
export EXPO_PUBLIC_ENABLE_WIDGETS=true
export APPLE_TEAM_ID=XXXXXXXXXX

2. Install Dependencies

The widget package is already included in package.json. If you need to reinstall:
cd apps/app
yarn install

3. Run Prebuild

Widgets require native code, so you need to run prebuild:
yarn prebuild:clean
This will generate the native iOS and Android projects with widget extensions.

4. Build and Run

# iOS
yarn ios

# Android
yarn android

Widget Structure

Widgets are located in app/widgets/:
app/widgets/
├── ios/
│   └── ExampleWidget.swift      # iOS SwiftUI widget
├── android/
│   ├── ExampleWidget.kt         # Android Kotlin widget
│   ├── widget_info.xml          # Widget configuration
│   └── example_widget.xml       # Widget layout
├── README.md                     # Widget documentation
└── SECURITY.md                   # Security best practices

Using Widgets in Your App

Fetching Widget Data

Use the useWidgetData hook to fetch data for widgets:
import { useWidgetData } from "@/hooks/useWidgetData"

function WidgetSettingsScreen() {
  const { data, loading, error, refetch } = useWidgetData({
    table: "profiles",
    select: "id, first_name, avatar_url",
    filters: { user_id: currentUserId },
    limit: 1,
    requireAuth: true,
    refreshInterval: 15 * 60 * 1000, // 15 minutes
  })

  if (loading) return <Spinner />
  if (error) return <Text>Error: {error.message}</Text>

  return (
    <View>
      <Text>Welcome {data?.first_name}</Text>
      <Button onPress={refetch} title="Refresh" />
    </View>
  )
}

Widget Service

The widget service handles secure data fetching with caching:
import { fetchWidgetData, getWidgetConfig } from "@/services/widgets"

// Fetch data
const { data, error } = await fetchWidgetData({
  table: "posts",
  select: "id, title, created_at",
  filters: { published: true },
  limit: 5,
  orderBy: { column: "created_at", ascending: false },
  requireAuth: false, // Public data
})

// Get widget configuration
const config = getWidgetConfig()
// { supabaseUrl, supabaseKey, isMock }

Supabase Integration

Sharing Session Tokens

Widgets need access to Supabase session tokens to fetch authenticated data. The app automatically shares tokens via:
  • iOS: App Groups (UserDefaults)
  • Android: SharedPreferences

Setting Up App Groups (iOS)

  1. Run yarn setup → enable Widgets → enter your App Group when prompted (saved as APP_GROUP_IDENTIFIER).
  2. In Xcode, select your app target
  3. Go to “Signing & Capabilities”
  4. Add “App Groups” capability
  5. Ensure the group matches APP_GROUP_IDENTIFIER (default: group.<bundleId>). The widget derives the App Group from the bundle ID if no override is set.

Data Fetching in Widgets

Widgets fetch data directly from Supabase using the REST API: iOS (Swift):
// Get shared UserDefaults
let userDefaults = UserDefaults(suiteName: "group.com.yourcompany.yourapp")

// Get Supabase config
let supabaseUrl = userDefaults?.string(forKey: "supabase_url")
let supabaseKey = userDefaults?.string(forKey: "supabase_key")
let sessionToken = userDefaults?.string(forKey: "supabase_session_token")

// Make HTTP request to Supabase
// GET {supabaseUrl}/rest/v1/{table}?select=*
// Headers: apikey, Authorization (if token available)
Android (Kotlin):
// Get shared preferences
val prefs = context.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)

// Get Supabase config
val supabaseUrl = prefs.getString("supabase_url", null)
val supabaseKey = prefs.getString("supabase_key", null)
val sessionToken = prefs.getString("supabase_session_token", null)

// Make HTTP request to Supabase
// Similar to iOS implementation

Styling Widgets

iOS Widget Styling

Widgets use SwiftUI with theme colors:
// Theme colors
private let primaryColor = Color(red: 0.2, green: 0.4, blue: 0.8)
private let backgroundColor = Color(red: 0.95, green: 0.95, blue: 0.97)
private let textColor = Color(red: 0.1, green: 0.1, blue: 0.1)

// Use in views
Text("Title")
    .font(.headline)
    .foregroundColor(textColor)

Android Widget Styling

Widgets use XML layouts with theme colors:
<TextView
    android:text="Title"
    android:textSize="16sp"
    android:textStyle="bold"
    android:textColor="#1a1a1a" />
Update colors in example_widget.xml to match your app theme.

Security Best Practices

1. Row Level Security (RLS)

Enable RLS policies in Supabase for widget-accessible tables:
-- Example: Allow widgets to read public posts
CREATE POLICY "Widgets can read public posts"
ON posts FOR SELECT
USING (published = true);

2. Token Management

  • Store session tokens securely in App Groups/SharedPreferences
  • Tokens are automatically refreshed by the main app
  • Widgets use read-only access to tokens

3. Data Validation

Always validate data before displaying in widgets:
import { validateWidgetData } from "@/services/widgets"

const isValid = validateWidgetData(data, (d) => {
  return d && typeof d.title === "string" && d.title.length > 0
})

4. Rate Limiting

Widget updates are rate-limited to prevent excessive API calls:
  • Minimum 5 minutes between updates
  • 15-minute cache duration
  • Maximum 10 cached items

Creating Custom Widgets

iOS Widget

  1. Create a new Swift file in app/widgets/ios/
  2. Implement TimelineProvider protocol
  3. Create SwiftUI view for widget content
  4. Register widget in ExampleWidget.swift
Example structure:
struct MyWidgetProvider: TimelineProvider {
    func placeholder(in context: Context) -> MyEntry {
        // Return placeholder data
    }
    
    func getSnapshot(in context: Context, completion: @escaping (MyEntry) -> Void) {
        // Return snapshot data
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<MyEntry>) -> Void) {
        // Fetch data and create timeline
    }
}

Android Widget

  1. Create a new Kotlin file in app/widgets/android/
  2. Extend AppWidgetProvider
  3. Create XML layout in res/layout/
  4. Register widget in widget_info.xml
Example structure:
class MyWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // Update widget
    }
}

Troubleshooting

Widget Not Showing

  1. Check feature flag: Ensure EXPO_PUBLIC_ENABLE_WIDGETS=true
  2. Run prebuild: Widgets require native code generation
  3. Check logs: Look for widget-related errors in Xcode/Android Studio

Data Not Loading

  1. Check Supabase config: Verify URL and key are set
  2. Check session token: Ensure user is authenticated
  3. Check RLS policies: Widgets need appropriate permissions
  4. Check network: Widgets need internet access

Build Errors

  1. iOS: Check App Group identifier matches in app and widget
  2. Android: Check package name matches in widget files
  3. Missing files: Ensure all widget files are in correct locations

Examples

Displaying User Profile

// iOS
struct ProfileWidget: View {
    var entry: ProfileEntry
    
    var body: some View {
        VStack {
            Text(entry.userName)
            Text(entry.email)
        }
    }
}

Displaying Recent Posts

// Android
fun updatePostsWidget(context: Context, posts: List<Post>) {
    val views = RemoteViews(context.packageName, R.layout.posts_widget)
    views.setTextViewText(R.id.post_count, "${posts.size} posts")
    // Update widget
}

API Reference

useWidgetData Hook

useWidgetData<T>(options: UseWidgetDataOptions<T>): UseWidgetDataReturn<T>
Options:
  • table: string - Supabase table name
  • select?: string - Columns to select (default: ”*”)
  • filters?: Record<string, any> - Filter conditions
  • limit?: number - Maximum rows (default: 10)
  • orderBy?: { column: string; ascending?: boolean } - Sort order
  • requireAuth?: boolean - Require authentication (default: false)
  • cacheKey?: string - Custom cache key
  • refreshInterval?: number - Auto-refresh interval in ms
  • enabled?: boolean - Enable/disable hook (default: true)
Returns:
  • data: T | null - Fetched data
  • loading: boolean - Loading state
  • error: Error | null - Error if any
  • refetch: () => Promise<void> - Manual refresh
  • clearCache: () => void - Clear cache
  • config: WidgetConfig - Widget configuration

Widget Service

fetchWidgetData<T>(options: FetchOptions): Promise<{ data: T | null; error: Error | null }>
getWidgetConfig(): WidgetConfig
clearWidgetCache(key?: string): void
validateWidgetData<T>(data: T | null, validator?: (data: T) => boolean): boolean

Next Steps