Token导航 LogoToken导航TokenDH.com
研究检索需要联网github未标认证来源可访问许可证需确认审计提醒

axiom-push-notifications公理推送通知

Agent Skill

axiom-push-notifications 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

1,630

周安装

70

GitHub Stars

873

下载量

571
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:axiom-push-notifications(公理推送通知)
来源仓库:https://github.com/charleswiltgen/axiom
仓库路径:skills/axiom-push-notifications
安装命令:
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-push-notifications
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-push-notifications

简介

提供远程推送通知的完整实现方案,包括 APNs 注册和令牌管理。

  • 适用于需要发送消息提醒、状态更新或实时通信功能的移动应用。
  • 支持富媒体通知、操作按钮和 Live Activity 等高级推送特性。
  • 需确保服务器端正确配置证书和环境匹配以避免推送失败。
  • 建议结合诊断技能排查 token 注册和 payload 结构问题。

SKILL.md

Push Notifications

Remote and local notification patterns for iOS. Covers permission flow, APNs registration, token management, payload design, actionable notifications, rich notifications with service extensions, communication notifications, Focus interaction, and Live Activity push transport.

When to Use This Skill

Use when you need to:

  • ☑ Implementing remote (APNs) push notifications
  • ☑ Requesting notification permissions
  • ☑ Managing device tokens and server sync
  • ☑ Adding actionable notifications with categories/actions
  • ☑ Building rich notifications with service extensions
  • ☑ Communication notifications with avatars (iOS 15+)
  • ☑ Time Sensitive or Critical alerts
  • ☑ Updating Live Activities via push (transport layer)
  • ☑ Broadcast push for large audiences (iOS 18+)
  • ☑ Local notification scheduling

Example Prompts

"How do I set up push notifications?" "When should I ask for notification permission?" "My push notifications aren't arriving" "How do I add buttons to notifications?" "How do I show images in push notifications?" "How do I send push updates to a Live Activity?" "What's the difference between APNs token and FCM token?" "How do I make notifications break through Focus mode?" "My notifications work in development but not production" "How do I handle notification taps to open a specific screen?"

Red Flags

Signs you're making this harder than it needs to be:

  • ❌ Requesting permission on first launch before user understands value
  • ❌ Caching device tokens locally instead of requesting fresh each launch
  • ❌ Sending the same token to sandbox AND production APNs
  • ❌ Using content-available: 1 without understanding silent push throttling (~2-3/hour)
  • ❌ Not implementing serviceExtensionTimeWillExpire fallback
  • ❌ Force-unwrapping device token or assuming registration always succeeds
  • ❌ Hardcoding APNs host instead of switching sandbox/production by environment
  • ❌ Setting apns-priority: 10 for all notifications (drains battery, gets throttled)
  • ❌ Exceeding 4KB payload without realizing APNs silently rejects it
  • ❌ Using FCM without disabling method swizzling when you have custom delegate handling
  • ❌ Not handling foreground notification presentation (notifications silently dropped)
  • ❌ Overusing Time Sensitive interruption level (erodes user trust, they'll disable all notifications)

Mandatory First Steps

Before implementing any push notification feature:

1. Enable Push Notification Capability

  • Xcode: Target → Signing & Capabilities → + Push Notifications
  • Adds aps-environment entitlement
  • Verify in Apple Developer Portal that the App ID has Push Notifications enabled

2. Register for Remote Notifications

// AppDelegate
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UNUserNotificationCenter.current().delegate = self
    UIApplication.shared.registerForRemoteNotifications()
    return true
}

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    sendTokenToServer(token) // Never cache locally — tokens change
}

func application(_ application: UIApplication,
                 didFailToRegisterForRemoteNotificationsWithError error: Error) {
    // Simulator cannot register. Log, don't crash.
}

3. Request Authorization (in context, not at launch)

let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
    await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
}

Request when user action makes notification value obvious (e.g., after scheduling a reminder, subscribing to updates). The system prompts only once — bad timing means permanent denial.

Permission Flow

Pattern 1: Standard Authorization

Request in context after user understands the value:

func subscribeToUpdates() async {
    let center = UNUserNotificationCenter.current()
    let settings = await center.notificationSettings()

    switch settings.authorizationStatus {
    case .notDetermined:
        let granted = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
        if granted == true {
            await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
        }
    case .authorized, .provisional:
        // Already have permission
        break
    case .denied:
        // Redirect to Settings
        promptToOpenSettings()
    case .ephemeral:
        break
    @unknown default:
        break
    }
}

Pattern 2: Provisional Authorization (iOS 12+)

Notifications appear quietly in Notification Center with Keep/Turn Off buttons. No permission dialog shown to user.

let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])

Good for apps where users haven't yet discovered notification value. They see notifications quietly and choose to promote them.

Pattern 3: Authorization Status Check

Always check before scheduling or assuming permission:

let settings = await UNUserNotificationCenter.current().notificationSettings()
guard settings.authorizationStatus == .authorized else {
    // Handle missing permission
    return
}

Pattern 4: Handling Denial

Redirect to Settings when user has denied:

func promptToOpenSettings() {
    // iOS 16+
    if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
        UIApplication.shared.open(url)
    } else {
        // Fallback: general app settings
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.open(url)
        }
    }
}

Token Management

Token Format

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    sendTokenToServer(token)
}

Key Rules

  • Never cache locally — request fresh at every app launch via registerForRemoteNotifications()
  • Sandbox ≠ production — tokens are different per APNs environment, endpoints are different (api.sandbox.push.apple.com vs api.push.apple.com)
  • Server sync — send token + bundle ID + user ID + environment (sandbox/production) to your server
  • Tokens change after backup restore, device migration, reinstall, or OS updates

Notification Types Decision Tree

What type of notification?
│
├─ Alert (user-visible)
│  ├─ Passive — informational, no sound, appears in history
│  │  interruption-level: "passive"
│  │
│  ├─ Active — default, sound + banner
│  │  interruption-level: "active" (or omit, it's default)
│  │
│  ├─ Time Sensitive — breaks through scheduled summary, not Focus
│  │  interruption-level: "time-sensitive"
│  │  Requires: Time Sensitive Notifications capability
│  │
│  └─ Critical — breaks through Do Not Disturb and mute switch
│     interruption-level: "critical"
│     Requires: Apple entitlement approval (medical, safety, security)
│
├─ Communication (iOS 15+)
│  Shows sender avatar, name, breaks Focus for allowed contacts
│  Requires: INSendMessageIntent + Communication Notifications capability
│  Configured in service extension via content.updating(from: intent)
│
├─ Silent / Background
│  content-available: 1, no alert/sound/badge
│  Throttled to ~2-3 per hour
│  apns-priority: 5 (MUST be 5, not 10)
│  App gets ~30 seconds background execution
│
└─ Live Activity
   apns-push-type: liveactivity
   apns-topic: {bundleID}.push-type.liveactivity
   Updates/starts/ends Live Activities remotely

Payload Design Patterns

Basic Alert

{
  "aps": {
    "alert": {
      "title": "New Message",
      "subtitle": "From Alice",
      "body": "Hey, are you free for lunch?"
    },
    "sound": "default",
    "badge": 3
  }
}

Sound Options

{
  "aps": {
    "alert": { "title": "Notification", "body": "With custom sound" },
    "sound": "custom-sound.aiff"
  }
}

Critical alert (requires Apple entitlement):

{
  "aps": {
    "alert": { "title": "Emergency", "body": "Critical alert" },
    "sound": {
      "critical": 1,
      "name": "alarm.aiff",
      "volume": 0.8
    }
  }
}

Badge

{
  "aps": {
    "badge": 5
  }
}

Set to 0 to remove badge.

Localized Content

{
  "aps": {
    "alert": {
      "loc-key": "NEW_MESSAGE_FORMAT",
      "loc-args": ["Alice", "lunch"]
    }
  }
}

Custom Data

Place custom data outside the aps dictionary:

{
  "aps": {
    "alert": { "title": "Order Update", "body": "Your order shipped" }
  },
  "orderId": "12345",
  "deepLink": "/orders/12345"
}

Relevance Score and Thread ID

{
  "aps": {
    "alert": { "title": "Breaking News", "body": "..." },
    "relevance-score": 0.8,
    "thread-id": "news-breaking",
    "interruption-level": "time-sensitive"
  }
}
  • relevance-score (0.0–1.0): ranking for notification summary (iOS 15+)
  • thread-id: groups notifications into conversations in Notification Center

Payload Size Limits

TypeMax Size
Standard push4KB
VoIP push5KB
Live Activity4KB

APNs silently rejects oversized payloads. No error returned to sender.

Categories and Actions

Register Categories at Launch

func registerNotificationCategories() {
    let replyAction = UNTextInputNotificationAction(
        identifier: "REPLY_ACTION",
        title: "Reply",
        options: [])

    // iOS 15+: actions with icons
    let likeIcon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")
    let likeAction = UNNotificationAction(
        identifier: "LIKE_ACTION",
        title: "Like",
        options: [],
        icon: likeIcon)

    let messageCategory = UNNotificationCategory(
        identifier: "MESSAGE",
        actions: [replyAction, likeAction],
        intentIdentifiers: [],
        options: [.customDismissAction])

    UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}

Set Category in Payload

{
  "aps": {
    "alert": { "title": "Alice", "body": "Are you free?" },
    "category": "MESSAGE"
  }
}

Handle Action Response

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo

        switch response.actionIdentifier {
        case "REPLY_ACTION":
            if let textResponse = response as? UNTextInputNotificationResponse {
                handleReply(text: textResponse.userText, userInfo: userInfo)
            }
        case "LIKE_ACTION":
            handleLike(userInfo: userInfo)
        case UNNotificationDefaultActionIdentifier:
            // User tapped the notification itself
            handleNotificationTap(userInfo: userInfo)
        case UNNotificationDismissActionIdentifier:
            // User dismissed (requires .customDismissAction on category)
            handleDismiss(userInfo: userInfo)
        default:
            break
        }

        completionHandler()
    }
}

Service Extension Patterns

Pattern 1: Media Enrichment

Download and attach images, audio, or video to notifications.

Payload requirement: Must include "mutable-content": 1:

{
  "aps": {
    "alert": { "title": "Photo", "body": "Alice sent a photo" },
    "mutable-content": 1
  },
  "imageURL": "https://example.com/photo.jpg"
}

Service extension:

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest,
                             withContentHandler contentHandler:
                                @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        guard let bestAttemptContent,
              let imageURLString = bestAttemptContent.userInfo["imageURL"] as? String,
              let imageURL = URL(string: imageURLString) else {
            contentHandler(request.content)
            return
        }

        // Download image
        let task = URLSession.shared.downloadTask(with: imageURL) { [weak self] url, _, error in
            guard let self, let url, error == nil else {
                contentHandler(self?.bestAttemptContent ?? request.content)
                return
            }

            // Move to tmp with proper extension
            let tmpURL = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)
                .appendingPathExtension("jpg")
            try? FileManager.default.moveItem(at: url, to: tmpURL)

            if let attachment = try? UNNotificationAttachment(identifier: "image",
                                                               url: tmpURL,
                                                               options: nil) {
                bestAttemptContent.attachments = [attachment]
            }

            contentHandler(bestAttemptContent)
        }
        task.resume()
    }

    override func serviceExtensionTimeWillExpire() {
        // 30-second window exceeded — deliver what we have
        if let contentHandler, let bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

Pattern 2: End-to-End Decryption

Decrypt payload in service extension before display:

override func didReceive(_ request: UNNotificationRequest,
                         withContentHandler contentHandler:
                            @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

    guard let bestAttemptContent,
          let encryptedBody = bestAttemptContent.userInfo["encryptedBody"] as? String else {
        contentHandler(request.content)
        return
    }

    if let decrypted = decrypt(encryptedBody) {
        bestAttemptContent.body = decrypted
    } else {
        bestAttemptContent.body = "(Encrypted message)"
    }

    contentHandler(bestAttemptContent)
}

override func serviceExtensionTimeWillExpire() {
    if let contentHandler, let bestAttemptContent {
        bestAttemptContent.body = "(Encrypted message)"
        contentHandler(bestAttemptContent)
    }
}

30-second processing window: If didReceive doesn't call contentHandler within ~30 seconds, serviceExtensionTimeWillExpire is called. Always deliver bestAttemptContent as fallback — if neither method calls the handler, the notification vanishes entirely.

Communication Notifications (iOS 15+)

Show sender avatar and name. Can break through Focus for allowed contacts.

// In your Notification Service Extension
import Intents

override func didReceive(_ request: UNNotificationRequest,
                         withContentHandler contentHandler:
                            @escaping (UNNotificationContent) -> Void) {
    guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
        contentHandler(request.content)
        return
    }

    // 1. Create sender persona
    let senderImage = INImage(url: avatarURL) // or INImage(imageData:)
    let sender = INPerson(
        personHandle: INPersonHandle(value: "alice@example.com", type: .emailAddress),
        nameComponents: nil,
        displayName: "Alice",
        image: senderImage,
        contactIdentifier: nil,
        customIdentifier: "user-alice-123"
    )

    // 2. Create message intent
    let intent = INSendMessageIntent(
        recipients: nil,       // nil for 1:1, set for group
        outgoingMessageType: .outgoingMessageText,
        content: bestAttemptContent.body,
        speakableGroupName: nil, // set for group conversations
        conversationIdentifier: "conversation-123",
        serviceName: nil,
        sender: sender,
        attachments: nil
    )

    // 3. Donate interaction
    let interaction = INInteraction(intent: intent, response: nil)
    interaction.direction = .incoming
    interaction.donate(completion: nil)

    // 4. Update content with intent
    do {
        let updatedContent = try bestAttemptContent.updating(from: intent)
        contentHandler(updatedContent)
    } catch {
        contentHandler(bestAttemptContent)
    }
}

Requirements:

  • Communication Notifications capability in Xcode
  • Notification Service Extension target
  • mutable-content: 1 in payload

Focus breakthrough: Communication notifications from contacts the user has allowed in Focus settings will break through. Use sparingly — overuse erodes trust.

Foreground Notification Handling

Without this delegate method, notifications received while the app is in foreground are silently dropped:

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler:
                                    @escaping (UNNotificationPresentationOptions) -> Void) {
        // Show notification even when app is in foreground
        completionHandler([.banner, .sound, .badge])
    }
}

Set UNUserNotificationCenter.current().delegate = self in didFinishLaunchingWithOptions — before the app finishes launching.

Live Activity Push Transport

Push updates to Live Activities remotely via APNs.

Observe Push Token

let activity = try Activity<OrderAttributes>.request(
    attributes: attributes,
    content: initialContent,
    pushType: .token
)

Task {
    for await pushToken in activity.pushTokenUpdates {
        let pushTokenString = pushToken.reduce("") {
            $0 + String(format: "%02x", $1)
        }
        try await sendPushToken(pushTokenString: pushTokenString)
    }
}

APNs Headers for Live Activity

HeaderValue
apns-push-typeliveactivity
apns-topic{bundleID}.push-type.liveactivity
apns-priority5 (routine) or 10 (time-sensitive)

Update Payload

{
  "aps": {
    "timestamp": 1234567890,
    "event": "update",
    "content-state": {
      "currentStep": "outForDelivery",
      "estimatedArrival": "2:30 PM"
    },
    "stale-date": 1234571490,
    "dismissal-date": 1234575090,
    "relevance-score": 75.0
  }
}

Event Types

EventPurpose
updateUpdate content-state
startStart a new Live Activity remotely (iOS 17.2+)
endEnd the activity

Key Rules

  • content-state must match ActivityAttributes.ContentState exactly — no custom JSON encoding strategies, property names must be identical
  • timestamp is required — APNs uses it to discard stale updates
  • stale-date shows a visual indicator that data is outdated
  • dismissal-date controls when an ended activity disappears from Lock Screen
  • relevance-score orders multiple active Live Activities
  • Priority budget enforced — excessive apns-priority: 10 gets throttled
  • Add NSSupportsLiveActivitiesFrequentUpdates to Info.plist for high-frequency apps (sports, navigation)

For ActivityKit UI, attributes, and Dynamic Island layout, see axiom-extensions-widgets.

Broadcast Push (iOS 18+)

Channel-based delivery for large audiences (sports scores, flight status, breaking news).

Setup

  1. Enable Broadcast Push Notifications capability in Apple Developer Portal
  2. Create channels via Apple's Broadcast Push API

Subscribe to Channel

let activity = try Activity<ScoreAttributes>.request(
    attributes: attributes,
    content: initialContent,
    pushType: .channel(channelId)
)

Server Sends to Channel

POST /4/broadcasts/apps/{TOPIC}

Key Rules

  • Only available for Live Activities (not regular push)
  • Channel storage policies: No Storage (higher budget) vs Most Recent Message (stores last update)
  • Manage channel lifecycle — delete unused channels (total active channels are limited)
  • Channels are identified by opaque IDs, not user-facing names
  • More efficient than per-device token delivery for 1-to-many scenarios

FCM as Provider

If using Firebase Cloud Messaging as your push provider, watch for these gotchas:

1. Swizzling Trap

FCM swizzles UNUserNotificationCenterDelegate methods and didRegisterForRemoteNotifications by default. If you have custom delegate handling, they conflict.

Fix: Set in Info.plist:

<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

Then manually pass the APNs token to FCM:

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Messaging.messaging().apnsToken = deviceToken
}

2. Dual Token Confusion

FCM token ≠ APNs device token. They are completely different. Send the correct one to the correct backend.

  • FCM token → your server (for sending via FCM)
  • APNs token → only needed if sending directly via APNs

3. APNs Auth Key Upload

Upload your.p8 APNs authentication key to Firebase Console → Project Settings → Cloud Messaging. Without this, development builds work (FCM uses sandbox automatically) but production builds silently fail.

4. Silent Push Payload Size

FCM's content_available maps to APNs content-available, but FCM may add extra fields to the payload. Monitor total size to avoid exceeding the 4KB limit.

Anti-Patterns

Anti-Pattern 1: Requesting Permission at App Launch

Wrong:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { _, _ in }
    return true
}

Right:

// After user taps "Subscribe to updates" or completes onboarding
func subscribeButtonTapped() async {
    let granted = try? await UNUserNotificationCenter.current()
        .requestAuthorization(options: [.alert, .sound, .badge])
    if granted == true {
        await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
    }
}

Why it matters: The system only shows the permission dialog once. If the user hasn't seen value yet, they tap "Don't Allow" reflexively. ~60% of users who deny never re-enable in Settings. You get one shot.

Anti-Pattern 2: Caching Device Tokens

Wrong:

func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    let tokenString = token.map { String(format: "%02x", $0) }.joined()
    UserDefaults.standard.set(tokenString, forKey: "pushToken") // Stale after restore
}

Right:

func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    let tokenString = token.map { String(format: "%02x", $0) }.joined()
    sendTokenToServer(tokenString) // Fresh every launch
}

Why it matters: Tokens change after backup restore, device migration, reinstall, and sometimes after OS updates. A stale cached token means your server sends to a token APNs no longer recognizes — notifications silently vanish.

Anti-Pattern 3: Ignoring Service Extension Timeout

Wrong:

override func didReceive(_ request: UNNotificationRequest,
                         withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    // Download large image, no timeout handling
    downloadImage(from: url) { image in
        // If this takes >30 seconds, notification vanishes entirely
        contentHandler(modifiedContent)
    }
}
// serviceExtensionTimeWillExpire not implemented

Right:

override func serviceExtensionTimeWillExpire() {
    if let contentHandler, let bestAttemptContent {
        // Deliver whatever we have — text without image is better than nothing
        contentHandler(bestAttemptContent)
    }
}

Why it matters: The service extension has a ~30 second window. If neither didReceive nor serviceExtensionTimeWillExpire calls the content handler, the notification disappears completely. Users never see it.

Anti-Pattern 4: Using Time Sensitive for Everything

Wrong:

{
  "aps": {
    "alert": { "title": "Weekly Newsletter", "body": "Check out this week's articles" },
    "interruption-level": "time-sensitive"
  }
}

Right:

{
  "aps": {
    "alert": { "title": "Weekly Newsletter", "body": "Check out this week's articles" },
    "interruption-level": "passive"
  }
}

Why it matters: iOS shows users which apps overuse Time Sensitive. Users who feel interrupted will disable ALL notifications from your app — not just Time Sensitive ones. Reserve it for genuinely time-bound events (delivery arriving, meeting starting, security alerts). Apple can also revoke the capability.

Pressure Scenarios

Scenario 1: "Ship Push Notifications by Friday"

Context: PM needs push notifications working for a demo.

Pressure: "Just ask for permission at launch, we'll fix it later."

Reality: The system only prompts once. If the user denies, you need them to manually enable in Settings. ~60% of users never do. "Fix it later" means permanently lower opt-in rates.

Correct action:

  1. Implement push registration and delivery without the permission prompt first
  2. Add contextual permission request after a user action that makes notification value obvious
  3. Test both grant and deny flows end-to-end

Push-back template: "Permission timing directly affects our opt-in rate. A 2-hour investment now prevents a 30% lower notification reach permanently. Let me implement the contextual prompt — it's the same amount of code, just in the right place."

Scenario 2: "Notifications Work in Dev but Not Production"

Context: App Store build doesn't receive push notifications.

Pressure: "Something is wrong with APNs, let's file a radar."

Reality: 95% of the time it's a sandbox/production token mismatch. Dev builds use api.sandbox.push.apple.com, production uses api.push.apple.com. Tokens are different per environment. The same token sent to the wrong endpoint silently fails.

Correct action:

  1. Verify server is using the correct APNs endpoint for the build type
  2. Check that the token was obtained from a production build (not a dev/TestFlight token sent to production endpoint)
  3. If using FCM, verify the APNs authentication key (.p8) is uploaded to Firebase Console

Push-back template: "Before filing a radar, let me verify our token/environment configuration. This is the number one cause of 'works in dev, not production' and takes 5 minutes to check."

Scenario 3: "Just Send Everything as Time Sensitive"

Context: Product wants maximum notification visibility.

Pressure: "Users need to see our notifications immediately."

Reality: iOS shows users which apps overuse Time Sensitive. Users who feel interrupted will disable ALL notifications from your app — not just Time Sensitive. Apple can also revoke the entitlement for abuse.

Correct action:

  1. Classify notifications by genuine urgency
  2. Use passive for informational, active (default) for normal engagement, time-sensitive only for truly time-bound events
  3. Document the classification for the team so backend engineers apply the right level

Push-back template: "Overusing Time Sensitive will cause users to disable our notifications entirely. Let's classify by urgency — most notifications should be active, with time-sensitive reserved for genuinely time-bound events like delivery arrivals or expiring offers."

Checklist

Before shipping push notifications:

Entitlements:

  • ☑ Push Notifications capability added in Xcode
  • ☑ Provisioning profile includes aps-environment
  • ☑ Communication Notifications capability (if using communication type)

Permissions:

  • ☑ Authorization requested in context (not at launch)
  • ☑ Denial handled gracefully (Settings redirect or degraded experience)
  • ☑ Authorization status checked before scheduling
  • ☑ Provisional authorization considered for trial period

Token Management:

  • ☑ Token sent to server on every launch (never cached)
  • ☑ Server stores token per environment (sandbox/production)
  • ☑ Token refresh handled (pushTokenUpdates for Live Activities)

Payload:

  • ☑ Payload under 4KB (5KB for VoIP)
  • ☑ Category identifier matches registered categories
  • ☑ Interruption level appropriate for content urgency
  • ☑ Custom data placed outside aps dictionary

Service Extension (if applicable):

  • ☑ mutable-content: 1 set in payload
  • ☑ serviceExtensionTimeWillExpire delivers fallback content
  • ☑ App group configured for shared data access

Testing:

  • ☑ Tested with Push Notifications Console or curl
  • ☑ Tested both foreground and background delivery
  • ☑ Tested on physical device (Simulator has no APNs token)

Resources

WWDC: 2021-10091, 2023-10025, 2023-10185, 2024-10069

Docs: /usernotifications, /usernotifications/unusernotificationcenter, /activitykit

Skills: axiom-push-notifications-ref, axiom-push-notifications-diag, axiom-extensions-widgets, axiom-background-processing

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

37.09%
按下载量换算212

Claude

31.07%
按下载量换算177

Cursor

18.58%
按下载量换算106

Gemini CLI

9.92%
按下载量换算57

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills