progress-notifications
v0.0.2
Published
Custom progress notifications with Rich UI, action buttons, and RTL support.
Maintainers
Readme
progress-notifications
A Capacitor plugin for showing download/upload progress from your app to the user — natively, while the app is in the background or the device is locked.
| Platform | Mechanism | Surfaces |
|---|---|---|
| iOS 16.2+ | Live Activity via ActivityKit | Lock screen banner, Dynamic Island (compact / minimal / expanded) |
| iOS < 16.2 | (rejected) | API calls reject with a clear error — no fallback |
| Android | Foreground progress notification via NotificationCompat.Builder.setProgress | Notification shade, lock screen, status bar icon |
| Web | No-op stubs (logs to console) | — |
Install
npm install progress-notifications
npx cap syncThen follow the platform setup sections below.
iOS setup
1. Info.plist
Add NSSupportsLiveActivities to your app's ios/App/App/Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>Without this, Activity.request(...) silently fails.
2. Add a Widget Extension target
Live Activities require a Widget Extension target inside your app's Xcode project — Capacitor plugins can't ship one. One-time setup, ~3 minutes.
- Open your app's workspace:
npx cap open ios. - File → New → Target…
- Pick Widget Extension → Next.
- Fill in:
- Product Name:
ProgressLiveActivity(any name works) - Include Live Activity: ✓ check this box
- Include Configuration Intent: leave unchecked
- Product Name:
- Click Finish. If asked "Activate scheme?", click Cancel.
- Xcode generates a folder under
ios/App/<ProductName>/. Delete the auto-generated<ProductName>LiveActivity.swiftand<ProductName>Bundle.swiftfiles inside it (otherwise you'll get a duplicate@mainerror). KeepInfo.plistandAssets.xcassets. - Drag the two files from
node_modules/progress-notifications/widget-extension-template/into the new target's folder in Xcode:ProgressActivityAttributes.swiftProgressLiveActivityWidget.swift- In the dialog: Copy items if needed = ON, Add to targets = your widget target only (NOT the App target).
- Build (⌘B). Should succeed.
Why are there two copies of
ProgressActivityAttributes? ActivityKit requires the attribute struct to exist in both the plugin (so it can callActivity.request) and the widget extension (so it can render). The two definitions must stay byte-for-byte identical — they're matched at runtime via Codable serialization. If you add or rename a field in one, change the other.
3. iOS deployment target
The plugin's deployment target is iOS 15.0. Live Activities themselves require iOS 16.2 at runtime — gated automatically. Make sure your app's minimum deployment target is 15.0 or higher (App target → General → Minimum Deployments in Xcode).
Android setup
1. Notification permission (Android 13+)
Android 13+ requires runtime permission for notifications. Add to your android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Then request the permission at runtime before calling show() for the first time. Most apps use @capacitor/local-notifications's requestPermissions() for this since it's already a permission request flow your users know.
2. Custom icon (optional)
If you want a custom small icon (instead of the system download icon), drop a drawable into android/app/src/main/res/drawable/ (e.g. ic_stat_download.xml) and pass its name as androidIcon:
ProgressNotification.show({ id: 1, androidIcon: 'ic_stat_download', ... });If the name doesn't resolve, the plugin falls back to android.R.drawable.stat_sys_download silently.
3. Foreground service (optional)
If your work continues while the app is fully backgrounded for more than ~5 minutes, pair this plugin with a foreground service (e.g. @capawesome-team/capacitor-android-foreground-service) so Android doesn't kill your process before progress completes.
Usage
import { ProgressNotification } from 'progress-notifications';
// Start the activity / notification
await ProgressNotification.show({
id: 10,
title: 'Downloading album',
subtitle: 'Adele — 30',
body: 'Track 4 of 12',
icon: 'arrow.down.circle.fill', // iOS: SF Symbol name
androidIcon: 'ic_stat_download', // Android: drawable resource name
});
// Update — call as often as you like (silent, no banner re-presentation)
ProgressNotification.update({
id: 10,
progress: 42, // 0-100
body: 'Track 5 of 12', // optional override
});
// Mark done — Live Activity dismisses itself, notification flips to "complete" state
ProgressNotification.complete({
id: 10,
title: 'Download complete',
body: 'Tap to open',
icon: 'checkmark.circle.fill',
androidIcon: 'ic_stat_done',
});API
show(options)
Starts a new progress activity / notification.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | number | ✓ | Unique identifier. Pass the same id to update/complete to target this activity. |
| title | string | | Defaults to "Downloading...". |
| subtitle | string | | Rendered under the title. |
| body | string | | Rendered under the progress bar (iOS) / as content text (Android). |
| icon | string | | iOS: SF Symbol name (e.g. "arrow.down.circle.fill"). Default: "arrow.down.circle.fill". Ignored on Android. |
| androidIcon | string | | Android: drawable resource name in your app (e.g. "ic_stat_download"). Default: system download icon. Ignored on iOS. |
Returns Promise<void>. Rejects on iOS < 16.2, when Live Activities are disabled in Settings, or if Activity.request fails.
update(options)
Updates the progress and (optionally) any text. Cached values from show are reused for any field you omit.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | number | ✓ | Must match a prior show() call. |
| progress | number | ✓ | 0–100. |
| title / subtitle / body | string | | Optional overrides; previous value kept if omitted. |
Rejects if no activity is running for the given id.
complete(options)
Marks the activity / notification as complete. iOS dismisses the Live Activity per the system's default policy. Android keeps a final notification with no progress bar.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | number | ✓ | |
| title / subtitle / body | string | | Optional final text. |
| icon / androidIcon | string | | Optional final icon (e.g. "checkmark.circle.fill"). |
Limitations & quirks
iOS
- Live Activities require iOS 16.2+. Older iOS rejects.
- Live Activities can be disabled by the user globally (Settings → Face ID & Passcode → Live Activities) or per-app (Settings → YourApp → Live Activities). When disabled, calls reject — handle this in your app.
- The system limits how long a Live Activity can stay active (~8 hours, then auto-ends).
- Updates from the app are limited to a few per second by the system. For very fast updates, throttle on the JS side.
- The icon string must be a valid SF Symbol name. Invalid names render as blank — they don't throw.
Android
- No Live Activity / Dynamic Island equivalent exists. The plugin uses a foreground progress notification, which appears in the notification shade and the lock screen.
- The notification channel is created once at
IMPORTANCE_LOW(no sound / vibration). The firstshow()posts the notification; subsequentupdate()calls revise it silently viasetOnlyAlertOnce(true). - Calling
update()orcomplete()without a priorshow()rejects.
Both
- The
icon(iOS) andandroidIcon(Android) namespaces are completely separate. Passing the same string to both fields will not work. SF Symbols are not Android drawables and vice versa.
Troubleshooting
iOS: "Cannot find type 'ProgressActivityAttributes' in scope" Xcode has a stale package cache. File → Packages → Reset Package Caches, then Product → Clean Build Folder.
iOS: Live Activity never appears
- Confirm
NSSupportsLiveActivities = trueis in your app's Info.plist. - Confirm the user accepted the prompt and Live Activities aren't disabled in Settings.
- Confirm the Widget Extension target was added and the two template Swift files were added to its target membership (not the App target).
- Confirm there is no leftover auto-generated
@main WidgetBundlefrom the Xcode template (deletion in step 6 of iOS setup).
iOS: Activity appears but never updates
The two ProgressActivityAttributes definitions (plugin copy + widget copy) have diverged. Diff them — they must match field-for-field.
iOS: "Missing package product 'CapApp-SPM'"
Your ios/App/CapApp-SPM/Package.swift was rewritten with an iOS deployment target newer than your swift-tools-version allows. Either lower the platform line or bump the swift-tools-version comment.
Android: notification doesn't appear on Android 13+
You haven't requested POST_NOTIFICATIONS runtime permission yet. See Android setup → Notification permission.
Android: androidIcon ignored
The drawable name doesn't exist in your app's resources, or the resource ID lookup returned 0. Confirm the drawable is at android/app/src/main/res/drawable/<name>.xml and you passed exactly <name> (no extension, no path).
Local development
npm install
npm run build # tsc + rollup → dist/
npm run lintThe plugin uses Capacitor's local-package linking. To consume it from another local app: add "progress-notifications": "file:../path/to/progress-notifications" to that app's package.json and run npm install.
License
See package.json.
