> ## 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.

# Widgets

> Native iOS and Android widgets with Supabase integration

# 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):

```bash theme={null}
EXPO_PUBLIC_ENABLE_WIDGETS=true
APP_GROUP_IDENTIFIER=group.com.yourcompany.yourapp # yarn setup will prompt
APPLE_TEAM_ID=XXXXXXXXXX # Required for iOS widgets
```

<Note>
  **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.
</Note>

Or set it in your environment:

```bash theme={null}
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:

```bash theme={null}
cd apps/app
yarn install
```

### 3. Run Prebuild

Widgets require native code, so you need to run prebuild:

```bash theme={null}
yarn prebuild:clean
```

This will generate the native iOS and Android projects with widget extensions.

### 4. Build and Run

```bash theme={null}
# 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:

```typescript theme={null}
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:

```typescript theme={null}
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):**

```swift theme={null}
// 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):**

```kotlin theme={null}
// 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:

```swift theme={null}
// 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:

```xml theme={null}
<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:

```sql theme={null}
-- 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:

```typescript theme={null}
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:

```swift theme={null}
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:

```kotlin theme={null}
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

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

### Displaying Recent Posts

```kotlin theme={null}
// 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

```typescript theme={null}
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

```typescript theme={null}
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

* Read [Security Guide](/core-features/widgets#security-best-practices) for security considerations
* Check [Widget Examples](/core-features/widgets#examples) for implementation patterns
* Review [Troubleshooting](/core-features/widgets#troubleshooting) for common issues
