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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pierrecapo/expo-notification-service-extension

v0.1.0

Published

Expo module for iOS Communication Notifications with avatar support via Notification Service Extension

Readme

@pierrecapo/expo-notification-service-extension

An Expo module that adds iOS Communication Notifications to your app. When a push notification arrives, the Notification Service Extension downloads the sender's avatar and displays it as a circular image alongside the notification — just like iMessage or WhatsApp.

iOS only | iOS 15+ | Expo SDK 55+

Features

  • Circular sender avatar on push notifications via INSendMessageIntent
  • Group notification support (group avatar + sender name in body)
  • Automatic avatar caching with LRU eviction
  • Configurable payload key mapping (works with any backend)
  • Optional developer hook for custom notification processing (decryption, etc.)
  • Diagnostics API to verify setup

Installation

npx expo install @pierrecapo/expo-notification-service-extension

Setup

1. Add the config plugin

In your app.json or app.config.js:

{
  "expo": {
    "plugins": [
      ["@pierrecapo/expo-notification-service-extension", {
        "enableLogging": true
      }]
    ]
  }
}

2. Rebuild native project

npx expo prebuild --platform ios --clean

3. Configure key mapping

Call this once on app startup to tell the extension where to find fields in your push payload:

import { configureNotificationExtension } from "@pierrecapo/expo-notification-service-extension";

configureNotificationExtension({
  senderIdKey: "data.senderId",             // required — dot-path to sender ID
  avatarUrlKey: "data.senderAvatar",        // optional — dot-path to avatar URL
  conversationIdKey: "data.conversationId", // optional — groups notifications
  groupNameKey: "data.groupName",           // optional — enables group mode, shown as subtitle
  groupAvatarUrlKey: "data.groupAvatar",    // optional — group avatar (circular icon)
});

4. Send a push notification

Your push payload must include "mutable-content": 1 in the aps dictionary for the extension to intercept it. The notification title is used as the sender display name.

DM notification — shows the sender's circular avatar, their name as the title, and the message body. No subtitle.

{
  "aps": {
    "alert": {
      "title": "Alice",
      "body": "Hey! Are you coming tonight?"
    },
    "mutable-content": 1
  },
  "data": {
    "senderId": "user-123",
    "senderAvatar": "https://example.com/alice-avatar.jpg",
    "conversationId": "conv-456"
  }
}

Group notification — shows the group's circular avatar, the group name as subtitle, and sender attribution in the body text (like WhatsApp). The groupName value becomes the notification subtitle, and groupAvatar becomes the circular icon:

{
  "aps": {
    "alert": {
      "title": "Fitness Squad",
      "body": "~ Alice: Who's coming to the gym?"
    },
    "mutable-content": 1
  },
  "data": {
    "senderId": "user-123",
    "conversationId": "group-789",
    "groupName": "Fitness Squad",
    "groupAvatar": "https://example.com/squad-avatar.jpg"
  }
}

API

configureNotificationExtension(config)

Writes the payload key mapping to the shared App Group so the extension can read it. Must be called at least once before notifications can be enhanced. If the config hasn't been written yet, the extension passes notifications through unchanged.

type NotificationExtensionConfig = {
  senderIdKey: string;        // required
  avatarUrlKey?: string;
  conversationIdKey?: string;
  groupNameKey?: string;      // shown as notification subtitle
  groupAvatarUrlKey?: string; // circular icon in group mode
};

All keys support dot-path notation (e.g., "data.sender.id") for nested payloads.

prefetchAvatar(url, id)

Pre-downloads an avatar image into the shared cache. The extension will use it instead of downloading at notification time. Useful for warming the cache on app launch or when opening a conversation.

import { prefetchAvatar } from "@pierrecapo/expo-notification-service-extension";

await prefetchAvatar("https://example.com/alice.jpg", "user-123");

clearAvatarCache()

Deletes all cached avatar images.

import { clearAvatarCache } from "@pierrecapo/expo-notification-service-extension";

await clearAvatarCache();

validateSetup(samplePayload?)

Runs diagnostics to verify the extension is correctly configured. Optionally pass a sample payload to dry-run key extraction.

import { validateSetup } from "@pierrecapo/expo-notification-service-extension";

const result = await validateSetup({
  data: { senderId: "user-123", senderAvatar: "https://example.com/avatar.jpg" },
});

// result:
// {
//   configValid: true,
//   appGroupAccessible: true,
//   cachedAvatarCount: 3,
//   payloadParseResult: {
//     senderId: "user-123",
//     avatarUrl: "https://example.com/avatar.jpg",
//     conversationId: null,
//     groupName: null,
//     groupAvatarUrl: null
//   }
// }

Plugin Options

All options are optional — sensible defaults are provided.

["@pierrecapo/expo-notification-service-extension", {
  // Bundle ID for the NSE target
  // Default: "{appBundleId}.NotificationServiceExtension"
  extensionBundleId: "com.myapp.NotificationServiceExtension",

  // App Group identifier (shared between app and extension)
  // Default: "group.{appBundleId}"
  appGroupId: "group.com.myapp",

  // Path to a default avatar image used when download fails or no URL is provided
  defaultAvatarImage: "./assets/default-avatar.png",

  // Max avatar cache size in MB (LRU eviction when exceeded)
  // Default: 50
  cacheSizeLimitMB: 50,

  // Path to a Swift file with custom notification processing logic
  extensionHookFile: "./ios/MyNotificationHook.swift",

  // Enable NSLog output in the extension (visible in Console.app)
  // Default: false
  enableLogging: false,

  // Code signing configuration for the extension target
  signing: {
    style: "automatic",           // "automatic" | "manual"
    developmentTeam: "TEAM_ID",   // inherited from main app if not set
    provisioningProfile: "...",   // manual signing only
    codeSignIdentity: "...",      // manual signing only
  }
}]

How It Works

The config plugin adds a Notification Service Extension target to your Xcode project. This is a separate iOS process that intercepts push notifications before they are displayed.

When a push with mutable-content: 1 arrives:

  1. The extension reads the key mapping config from the shared App Group
  2. It extracts the sender ID, avatar URL, and optional group fields from the payload
  3. It resolves the avatar (cache hit, download, or fallback to bundled default)
  4. It builds an INSendMessageIntent with the sender's INPerson and avatar
  5. It updates the notification content with the intent
  6. iOS renders the notification with the circular avatar and app icon badge

The plugin automatically configures:

  • A Notification Service Extension Xcode target with all source files
  • App Group entitlement on both the main app and extension targets
  • Communication Notifications entitlement on the main app
  • INSendMessageIntent in the main app's NSUserActivityTypes
  • Intents and UserNotifications frameworks linked to the extension
  • Target dependency so the extension is built and embedded with the app

Avatar Caching

The extension caches downloaded avatars in the App Group container, keyed by {id}_{urlHash}. On subsequent notifications:

  • Same URL: uses the cached file (no network request)
  • Different URL (e.g., user changed their avatar): re-downloads and replaces the cache
  • Download fails: falls back to stale cache if available, then to the bundled default avatar

When the cache exceeds the size limit (default 50MB), the oldest files by access date are evicted (LRU). You can also pre-warm the cache from JS with prefetchAvatar() or clear it entirely with clearAvatarCache().

Developer Hook

For advanced use cases (e.g., decrypting an end-to-end encrypted notification body), provide a Swift file via extensionHookFile. The extension calls your function before processing the notification:

// ios/MyNotificationHook.swift
import UserNotifications

extension NotificationHook {
  static func willProcessNotification(
    content: UNMutableNotificationContent
  ) -> UNMutableNotificationContent {
    // Decrypt, modify, or enrich the content here
    return content
  }
}

Troubleshooting

Notification shows without avatar

  1. Verify "mutable-content": 1 is in your aps payload
  2. The app must be backgrounded or closed — the NSE doesn't run when the app is in the foreground
  3. Run validateSetup() to check that the config is written and the App Group is accessible
  4. Enable logging (enableLogging: true in plugin options) and check Console.app on your Mac, filtering by your extension's bundle ID

Extension doesn't seem to run at all

  1. Run npx expo prebuild --platform ios --clean to regenerate the native project
  2. In Xcode, verify the NotificationServiceExtension target exists and is listed under the main app's target dependencies
  3. Check that both targets have the same development team in Signing & Capabilities
  4. The extension's bundle ID must be a child of the main app's (e.g., com.myapp.NotificationServiceExtension)

"messaging notifications are not allowed" in Console.app

The main app is missing required capabilities. The plugin adds these automatically, but if you see this error after a manual Xcode change:

  1. Select the main app target > Signing & Capabilities
  2. Ensure Communication Notifications is listed (add it via + Capability if missing)
  3. Ensure the main app's Info.plist contains NSUserActivityTypes with INSendMessageIntent
  4. Clean build (Cmd+Shift+K) and re-run

Limitations

  • iOS only — Android uses a different notification system
  • One NSE per app — iOS allows only one Notification Service Extension per app. If you need additional NSE functionality (rich media attachments, payload decryption), use the extensionHookFile option
  • 30-second time limit — iOS gives the extension ~30 seconds to process each notification. Avatar downloads are typically well under 1 second
  • Foreground notifications — the NSE only runs when the app is backgrounded or closed. Foreground notifications bypass the extension entirely

License

MIT