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

# Push Notifications

> Local and remote push notifications for iOS and Android

Local notifications work out of the box. Remote push requires APNs (iOS) and FCM (Android) credentials.

## iOS Setup (APNs)

EAS Build handles APNs certificates automatically. Just ensure:

1. Push Notifications capability is enabled in Apple Developer Portal
2. `app.json` includes:
   ```json theme={null}
   {
     "ios": {
       "infoPlist": {
         "UIBackgroundModes": ["remote-notification"]
       }
     }
   }
   ```

Build with `eas build --platform ios`.

## Android Setup (FCM)

1. Create a Firebase project at [console.firebase.google.com](https://console.firebase.google.com/)
2. Add your Android app (use package name from `app.json`)
3. Download `google-services.json` to `apps/app/`
4. For server-side sending, set up FCM credentials in your backend:

   **Supabase:** Upload your Firebase service account JSON in the Supabase Dashboard under Integrations → Firebase.

   **Convex:** Add your Firebase service account JSON as an environment variable in the Convex Dashboard.

<Note>
  The legacy FCM Server Key is deprecated. Firebase now uses HTTP v1 API with OAuth 2.0 service account authentication. All server credentials should be configured in your backend dashboard, not in client-side environment variables.
</Note>

See [Expo Push Notifications](https://docs.expo.dev/push-notifications/overview/) for detailed setup.

***

## Usage

### Request Permission

```typescript theme={null}
import { useNotificationStore } from '@/stores/notificationStore'

function EnableNotifications() {
  const { requestPermission, permissionStatus } = useNotificationStore()

  const handleEnable = async () => {
    const granted = await requestPermission()
    // If denied, an alert prompts user to open Settings
  }

  return permissionStatus === 'undetermined' && (
    <Button onPress={handleEnable} title="Enable Notifications" />
  )
}
```

### Toggle Push Notifications

```typescript theme={null}
const { togglePush, isPushEnabled } = useNotificationStore()

// Toggle on/off - handles permission request automatically
await togglePush(user?.id) // Pass userId to sync preference to backend
```

### Schedule Local Notification

```typescript theme={null}
import { scheduleNotification } from '@/services/notifications'

await scheduleNotification({
  title: 'Reminder',
  body: 'Time to check your progress!',
  data: { screen: 'Profile' },
  trigger: { seconds: 3600 },
})
```

### Send Remote Notification (Backend)

Get the push token from your app:

```typescript theme={null}
const { pushToken } = useNotificationStore()
// Send pushToken to your backend
```

Send from Node.js backend:

```javascript theme={null}
import { Expo } from 'expo-server-sdk'

const expo = new Expo()
await expo.sendPushNotificationsAsync([{
  to: pushToken,
  title: 'New Message',
  body: 'You have a new message',
  data: { screen: 'Messages' },
}])
```

See [Expo Push Service](https://docs.expo.dev/push-notifications/sending-notifications/).

### Handle Notification Taps

The notification store handles this automatically via `initialize()`. For custom handling:

```typescript theme={null}
import { addNotificationResponseReceivedListener } from '@/services/notifications'
import { useNavigation } from '@react-navigation/native'

useEffect(() => {
  const subscription = addNotificationResponseReceivedListener(response => {
    const { screen } = response.notification.request.content.data
    if (screen) navigation.navigate(screen)
  })
  return () => subscription.remove()
}, [])
```

***

## Automatic Initialization

The notification store initializes automatically on app startup:

* Requests permission status
* Registers for push tokens (on physical devices)
* Sets up notification listeners
* Handles token refresh when tokens are invalidated
* Configures Android notification channels

No manual setup required - just use the store.

***

## API Reference

### Notification Store

```typescript theme={null}
const {
  permissionStatus,     // 'granted' | 'denied' | 'undetermined' | 'loading'
  pushToken,            // Expo push token for remote notifications
  isPushEnabled,        // User preference toggle
  notifications,        // Array of received notifications (last 50)
  unreadCount,          // Number of unread notifications

  // Actions
  initialize,           // Called automatically on app start
  cleanup,              // Called automatically on app unmount
  requestPermission,    // Request OS permission
  togglePush,           // Toggle notifications on/off
  scheduleNotification, // Schedule local notification
  cancelAllNotifications,
  setBadgeCount,
  markAsRead,
  markAllAsRead,
} = useNotificationStore()
```

### Notification Service

```typescript theme={null}
import {
  requestPermission,
  registerForPushNotifications,
  scheduleNotification,
  cancelNotification,
  addNotificationReceivedListener,
  addNotificationResponseReceivedListener,
  addPushTokenListener,
  showPermissionDeniedAlert,
  isPhysicalDevice,
} from '@/services/notifications'
```

***

## Features

### Physical Device Detection

Push tokens only work on physical devices. The service automatically detects emulators/simulators and returns mock tokens for development.

```typescript theme={null}
import { isPhysicalDevice } from '@/services/notifications'

if (!isPhysicalDevice()) {
  console.log('Running on emulator - using mock push token')
}
```

### Token Refresh Handling

Push tokens can be invalidated by the OS. The store automatically listens for token changes and re-registers when needed.

### Android Notification Channels

Android 8.0+ requires notification channels. Shipnative configures a default channel automatically with:

* Maximum importance (ensures notifications appear prominently)
* Vibration pattern
* Sound enabled
* Badge support
* Show on lockscreen

Channels are created automatically on first notification. No manual configuration needed.

### Permission Denied Alerts

When users deny permission, a helpful alert offers to open Settings:

```typescript theme={null}
import { showPermissionDeniedAlert } from '@/services/notifications'

// Called automatically by togglePush() and requestPermission()
// Or call manually:
showPermissionDeniedAlert()
```

The alert includes:

* Clear explanation of what permission enables
* "Open Settings" button (deep links to app settings)
* "Maybe Later" option
* Platform-specific instructions (iOS vs Android)

### Backend Synchronization

Push token is automatically synced to your backend:

```typescript theme={null}
// In services/notifications.ts - automatically called
const { pushToken } = useNotificationStore()

// Token is synced to:
// - Supabase: push_tokens table with user association
// - Convex: user.pushToken field
// - Or custom backend via env.backendUrl/push-tokens

// Backend can use token to send notifications
await fetch(`${backendUrl}/notifications/send`, {
  method: 'POST',
  body: JSON.stringify({
    token: pushToken,
    title: 'New Message',
    body: 'You have a new message!',
  })
})
```

### Notification Badge Management

Control app badge counts on iOS and Android:

```typescript theme={null}
const { setBadgeCount, unreadCount } = useNotificationStore()

// Set badge to unread count
setBadgeCount(unreadCount)

// Clear badge
setBadgeCount(0)

// Automatically cleared when marking notifications as read
markAllAsRead() // Also clears badge
```

### Analytics Integration

Notification events are automatically tracked with PostHog:

```typescript theme={null}
// Automatically tracked:
// - notification_permission_requested
// - notification_permission_granted
// - notification_permission_denied
// - notification_received (foreground)
// - notification_tapped (user interaction)
// - notification_scheduled (local)

// Events include metadata:
{
  title: 'Notification title',
  hasData: true,
  screen: 'HomeScreen', // If data includes screen
  platform: 'ios' | 'android' | 'web'
}
```

***

## Limitations

* **Web:** Push tokens return `null`. Remote push via Expo not supported on web.
* **Emulators:** Remote push doesn't work. Mock tokens returned for development.

***

## Troubleshooting

**Notifications not appearing?**

* Check `permissionStatus` is `'granted'`
* For iOS remote: use physical device, verify APNs setup
* For Android: verify `google-services.json` is in place

**Push token is null?**

* Expected on web platform
* On mobile: check permission was granted first
* On emulator: mock token is returned (check logs)

**Permission denied?**

* The store shows an alert prompting users to open Settings
* Check `permissionStatus` for current state

**Token changed unexpectedly?**

* Normal behavior - tokens can be invalidated by the OS
* The store handles this automatically via `addPushTokenListener`

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Mock Services" icon="flask" href="/development/mock-services">
    Test notifications without credentials
  </Card>

  <Card title="Analytics" icon="chart-line" href="/core-features/analytics">
    Track notification opens and engagement
  </Card>
</CardGroup>
