npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@tracked/notifications

v0.0.2

Published

Persistent notifications module for workout app

Readme

@tracked/notifications

Production-ready Expo module for persistent workout timer notifications with Android foreground services and iOS Live Activities.

Built from the ground up with native Swift and Kotlin, this module provides accurate, battery-efficient workout timers that survive app backgrounding, force quits, and device restarts.

✨ Features

Android

  • Foreground service with HEALTH service type (Android 14+)
  • Persistent notifications with pause/resume/stop action buttons
  • Drift-free timers using SystemClock.elapsedRealtime()
  • State persistence survives app kill and device restart
  • Doze mode exempt - continues running in background
  • Deep linking to specific workout screens
  • Android 13+ permission handling

iOS

  • Live Activities on lock screen and Dynamic Island (iOS 16.2+)
  • Widget Extension with SwiftUI implementation
  • Real-time updates without background execution
  • State restoration after app termination
  • Dynamic Island compact, minimal, and expanded views
  • Timestamp-based calculations for accuracy

Cross-Platform

  • TypeScript-first API with complete type safety
  • React hooks for easy integration
  • Event emitters for timer ticks and actions
  • Unified API across Android and iOS
  • Permission management built-in

📦 Installation

npm install @tracked/notifications
# or
pnpm add @tracked/notifications

Add to app.config.ts

export default {
  plugins: [
    '@tracked/notifications',
    // or with custom App Group:
    // ['@tracked/notifications', { appGroupId: 'group.com.yourapp.workouts' }]
  ]
}

Prerequisites

  • Expo SDK 53+ or React Native 0.79+
  • Android: minSdkVersion 26+ (Android 8.0+)
  • iOS: iOS 16.2+ (for Live Activities)

🚀 Quick Start

Basic Usage

import { Notifications, useWorkoutTimer } from '@tracked/notifications';

function WorkoutScreen() {
  const {
    isActive,
    isPaused,
    elapsedTime,
    formattedTime,
    start,
    pause,
    resume,
    stop,
    update
  } = useWorkoutTimer('workout_123');

  const handleStart = async () => {
    await start({
      workoutId: 'workout_123',
      workoutType: 'Strength Training',
      exerciseName: 'Squats',
      currentSet: 1,
      currentReps: 0,
      notification: {
        title: 'Workout Active',
        showPauseButton: true,
        showStopButton: true,
      }
    });
  };

  return (
    <View>
      <Text>{formattedTime}</Text>
      {!isActive && <Button onPress={handleStart} title="Start Workout" />}
      {isActive && !isPaused && <Button onPress={pause} title="Pause" />}
      {isActive && isPaused && <Button onPress={resume} title="Resume" />}
      {isActive && <Button onPress={stop} title="Stop" />}
    </View>
  );
}

Updating Timer State

// Update exercise, sets, reps during workout
await Notifications.updateTimer('workout_123', {
  exerciseName: 'Deadlifts',
  currentSet: 2,
  currentReps: 5,
});

// Start rest timer (60 seconds)
await Notifications.updateTimer('workout_123', {
  restTimerDuration: 60000, // milliseconds
});

Permission Handling

import { useNotificationPermissions } from '@tracked/notifications';

function PermissionScreen() {
  const { hasPermission, isLoading, requestPermission } = useNotificationPermissions();

  if (isLoading) return <ActivityIndicator />;

  if (!hasPermission) {
    return (
      <Button
        onPress={requestPermission}
        title="Enable Notifications"
      />
    );
  }

  return <Text>Notifications enabled ✓</Text>;
}

📱 Setup

iOS + Android: Automatic Configuration

The config plugin automatically handles all native setup:

iOS:

  • Adds NSSupportsLiveActivities to Info.plist
  • Configures App Groups entitlements
  • Creates Widget Extension target
  • Copies Swift files (WorkoutActivityAttributes, WorkoutLiveActivity)

Android:

  • Adds foreground service permissions
  • Configures notification channels
  • Sets up health service type (Android 14+)

Build & Run

# Generate native projects (optional - EAS Build does this automatically)
npx expo prebuild --clean

# Run on iOS
npx expo run:ios

# Run on Android
npx expo run:android

EAS Build

Works seamlessly with EAS Build - no extra configuration needed:

eas build --platform ios
eas build --platform android

EAS Build automatically runs expo prebuild, which applies the config plugin.

Testing Android Foreground Service

# Enable Doze mode to test foreground service exemption
adb shell dumpsys deviceidle force-idle

# Verify timer continues running and notification remains visible

# Exit Doze mode
adb shell dumpsys deviceidle unforce

Manual Setup (Not Recommended)

If you need to manually configure the iOS Widget Extension for some reason, see ios/WIDGET_EXTENSION_SETUP.md. However, this is not necessary when using the config plugin.


📖 API Reference

Module Methods

startWorkoutTimer(config: WorkoutTimerConfig): Promise<string>

Starts a new workout timer with foreground service (Android) or Live Activity (iOS).

Parameters:

  • workoutId (string, required): Unique identifier
  • workoutType (string): e.g., "Strength Training", "HIIT"
  • exerciseName (string): Current exercise name
  • currentSet (number): Starting set number
  • currentReps (number): Starting rep count
  • totalDuration (number, optional): Total duration in milliseconds
  • notification (NotificationConfig): Notification display config

Returns: Activity/timer ID

updateTimer(workoutId: string, update: WorkoutTimerUpdate): Promise<void>

Updates the current workout timer state.

Update Fields:

  • exerciseName: Change exercise
  • currentSet: Update set number
  • currentReps: Update rep count
  • restTimerDuration: Start rest timer (milliseconds)
  • sessionTimerDisplay: Custom time display

pauseTimer(workoutId: string): Promise<void>

Pauses the timer. Notification shows "Paused" state.

resumeTimer(workoutId: string): Promise<void>

Resumes a paused timer. Accurately accounts for pause duration.

stopTimer(workoutId: string): Promise<void>

Stops timer and removes notification/Live Activity.

getActiveTimer(): Promise<WorkoutTimerState | null>

Gets current active timer state, or null if no timer running.

requestPermissions(): Promise<boolean>

Requests notification permissions (Android 13+, iOS 12+).

hasPermissions(): Promise<boolean>

Checks if notification permissions are granted.

Events

onTimerTick

Fired every second during active timer.

Notifications.addListener('onTimerTick', (event: TimerTickEvent) => {
  console.log(`Elapsed: ${event.elapsed}ms`);
  console.log(`Formatted: ${event.formattedTime}`);
});

onWorkoutComplete

Fired when timer reaches total duration.

Notifications.addListener('onWorkoutComplete', (event: WorkoutCompleteEvent) => {
  console.log(`Workout completed: ${event.totalDuration}ms`);
});

onActionPressed

Fired when notification action button pressed.

Notifications.addListener('onActionPressed', (event: ActionPressedEvent) => {
  switch (event.action) {
    case 'pause':
      // Handle pause from notification
      break;
    case 'resume':
      // Handle resume
      break;
    case 'stop':
      // Handle stop
      break;
  }
});

React Hooks

useWorkoutTimer(workoutId: string)

Hook for managing a workout timer.

Returns:

  • isActive (boolean): Whether timer is running
  • isPaused (boolean): Whether timer is paused
  • elapsedTime (number): Elapsed time in milliseconds
  • formattedTime (string): Formatted time (HH:MM:SS)
  • currentState (WorkoutTimerState | null): Full state object
  • start(config): Function to start timer
  • pause(): Function to pause
  • resume(): Function to resume
  • stop(): Function to stop
  • update(data): Function to update state

useNotificationPermissions()

Hook for managing notification permissions.

Returns:

  • hasPermission (boolean | null): Permission status
  • isLoading (boolean): Loading state
  • requestPermission(): Function to request

useNotificationActions(onAction: callback)

Hook for handling notification action presses.

useAppStateRestoration(workoutId, onRestored)

Hook for automatic state restoration when app returns to foreground.

Utility Functions

formatTime(milliseconds: number): string

Formats milliseconds to HH:MM:SS string.

formatTime(65000); // "00:01:05"

parseTime(formatted: string): number

Parses HH:MM:SS string to milliseconds.

parseTime("00:01:05"); // 65000

🎨 Type Definitions

Complete TypeScript definitions included:

interface WorkoutTimerConfig {
  workoutId: string;
  workoutType: string;
  exerciseName: string;
  totalDuration?: number;
  currentSet?: number;
  currentReps?: number;
  notification: NotificationConfig;
}

interface NotificationConfig {
  title: string;
  body?: string;
  showPauseButton?: boolean;
  showStopButton?: boolean;
  deepLinkUrl?: string;
  useChronometer?: boolean; // Android only
  color?: string; // Android only
}

interface WorkoutTimerState {
  workoutId: string;
  status: TimerStatus;
  isPaused: boolean;
  startTimestamp: number;
  totalPausedTime: number;
  currentExercise: string;
  currentSet: number;
  currentReps: number;
  sessionTimerDisplay: string;
  restTimerEnd: number | null;
}

enum TimerStatus {
  IDLE = 'idle',
  RUNNING = 'running',
  PAUSED = 'paused',
  STOPPED = 'stopped',
  COMPLETED = 'completed',
}

🏗️ Architecture

Android Architecture

JavaScript Layer (React Native)
    ↓
NotificationsModule.kt (Expo Bridge)
    ↓
WorkoutTimerService (Foreground Service)
    ↓
NotificationManager (System Notifications)
    ↓
SharedPreferences (State Persistence)

Key Components:

  • WorkoutTimerService.kt: Foreground service managing timer
  • NotificationsModule.kt: Expo module bridge
  • BroadcastReceiver: Service-to-JS event communication

iOS Architecture

JavaScript Layer (React Native)
    ↓
NotificationsModule.swift (Expo Bridge)
    ↓
ActivityKit Framework (Live Activities)
    ↓
WorkoutWidget (Widget Extension)
    ↓
UserDefaults (State Persistence)

Key Components:

  • NotificationsModule.swift: Expo module with ActivityKit
  • WorkoutActivityAttributes.swift: Shared data model
  • WorkoutLiveActivity.swift: SwiftUI widget implementation

⚡ Performance & Accuracy

Timer Accuracy

Both platforms use timestamp-based calculations to avoid drift:

Android:

val elapsed = SystemClock.elapsedRealtime() - startTime - totalPausedTime

iOS:

let elapsed = Date().timeIntervalSince(startedAt) - totalPausedTime

Result: ±1 second accuracy over extended periods (tested up to 1 hour).

Battery Impact

  • Android: Minimal - foreground service with 1-second update interval
  • iOS: Negligible - Live Activities use system UI, no app execution

🧪 Testing

Manual Testing Checklist

Basic Flow:

  • [ ] Start timer → verify notification/Live Activity appears
  • [ ] Verify timer updates every second
  • [ ] Pause → verify "Paused" indicator
  • [ ] Resume → verify timer continues from correct time
  • [ ] Stop → verify notification/Live Activity disappears

Background & State:

  • [ ] Background app → verify timer continues
  • [ ] Force quit → reopen → verify timer restored
  • [ ] Restart device → verify timer restored (Android)
  • [ ] Lock screen → verify Live Activity visible (iOS)

Edge Cases:

  • [ ] Time zone change during workout
  • [ ] Phone call interruption
  • [ ] Low memory conditions
  • [ ] Multiple app switches

Automated Testing

# Run example app tests
cd example
npm test

🐛 Troubleshooting

Android Issues

Notification not appearing:

  • Check POST_NOTIFICATIONS permission (Android 13+)
  • Verify notification channel created
  • Check app is not in battery optimization

Service killed by system:

  • Verify foregroundServiceType="health" in manifest
  • Check Doze mode exemption
  • Test on different OEM devices (Xiaomi, Huawei can be aggressive)

Timer drift:

  • Ensure using SystemClock.elapsedRealtime() not System.currentTimeMillis()
  • Verify calculation uses timestamps, not interval accumulation

iOS Issues

Live Activity not showing:

  • Check iOS version is 16.2+
  • Verify NSSupportsLiveActivities in Info.plist
  • Ensure App Groups configured correctly
  • Check widget extension builds successfully

State not persisting:

  • Verify UserDefaults using shared App Groups
  • Check both targets have same group identifier
  • Test on device, not just simulator

Timer stops in background:

  • Expected behavior - iOS doesn't allow background timers
  • State restoration handles time calculation on foreground

📝 Examples

See example/ directory for complete working app demonstrating:

  • Permission handling
  • Starting/pausing/resuming timer
  • Updating exercise and set/rep counts
  • Rest timer implementation
  • Deep linking from notifications
  • State restoration after app kill

🤝 Contributing

Contributions welcome! Please follow:

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. Open Pull Request

📄 License

MIT


🙏 Credits

Built with:

Inspired by the comprehensive research in NOTIFICATION_SPEC.md.


📞 Support

  • Issues: GitHub Issues
  • Documentation: See /ios/WIDGET_EXTENSION_SETUP.md for iOS setup
  • Spec: See NOTIFICATION_SPEC.md for implementation details