소스 정보
- 저장소
- tomevault-io/skills-registry
- 최근 소스 활동
- 2026년 7월 3일 19:45
- 감지된 SKILL.md 언어
- 영어
- 스타
- 0
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/tomevault-io/skills-registry --skill mobile-push-notifications명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
| Use when this capability is needed.
> Use when this capability is needed.
Review architecture and API design for the vfs-s3 project. Use when the user mentions @architect, asks to review an issue's design, discuss module boundaries, API shape, or architectural decisions for vfs-s3. Also trigger when the user wants to create an ADR (Architecture Decision Record) or evaluate a technical approach for the project. Intended for dispatch from Codex automation or Claude routines; GitHub trigger phrase: @vfs-s3-bot please prepare design doc Use when this capability is needed.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | mobile-push-notifications |
| description | > Use when this capability is needed. |
You are a senior mobile engineer. Help the user design, implement, or review push notification systems with platform-specific best practices.
| Question | Why It Matters |
|---|---|
| What types of notifications? (transactional, marketing, real-time alerts) | Determines priority and channel strategy |
| Does the app need to deep link from notifications? | Payload design and navigation handling |
| Are there rich media needs? (images, action buttons, custom UI) | Notification extension / custom layout |
| Is the app cross-platform? | FCM covers both, but APNs-specific features may be needed |
| Are there notification preferences? (per-topic opt-in/out) | Topic subscription or server-side filtering |
| What is the expected volume? | Batching, throttling, server architecture |
Backend Server
→ Push Provider (FCM / APNs / both)
→ Device
→ OS Notification System
→ App (foreground: in-app handling / background: system tray)
→ User taps → Deep link → Specific screen
Key components:
| Component | Responsibility |
|---|---|
| Backend | Decides when/what to send, calls FCM/APNs API, manages device tokens |
| FCM (Firebase Cloud Messaging) | Cross-platform delivery (Android + iOS), topic subscriptions, analytics |
| APNs (Apple Push Notification service) | iOS delivery, required even when using FCM on iOS (FCM wraps APNs) |
| Device token | Unique identifier per device per app — must be refreshed and synced to backend |
| Notification payload | Title, body, data, image, actions, deep link |
// Using firebase_messaging
final messaging = FirebaseMessaging.instance;
// Request permission (required on iOS, Android 13+)
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false, // set true for quiet delivery on iOS
announcement: false,
carPlay: false,
criticalAlert: false, // requires Apple entitlement
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
} else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
print('User granted provisional permission');
} else {
print('User declined permission');
}
// Get device token
final token = await messaging.getToken();
await sendTokenToServer(token);
// Listen for token refresh
messaging.onTokenRefresh.listen((newToken) {
sendTokenToServer(newToken);
});
// Android 13+ (API 33) requires runtime permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
getAndSendToken()
} else {
// Explain why notifications are useful, offer settings link
}
}
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
// Get FCM token
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
sendTokenToServer(token)
}
// AndroidManifest.xml
// <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
// Request permission (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_CODE);
}
// Get FCM token
FirebaseMessaging.getInstance().getToken().addOnSuccessListener(token -> {
sendTokenToServer(token);
});
import UserNotifications
// Request permission
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
// AppDelegate — receive device token
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
sendTokenToServer(token)
// Also set FCM token if using Firebase
Messaging.messaging().apnsToken = deviceToken
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register: \(error)")
}
// Request permission
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError *error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
}
}];
// Receive token
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// Convert to string and send to server
}
// Foreground — app is open
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Show in-app notification (snackbar, overlay, local notification)
if (message.notification != null) {
showInAppNotification(message.notification!);
}
// Handle data payload
handleDataPayload(message.data);
});
// Background — app is in background (but not terminated)
FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
@pragma('vm:entry-point')
Future<void> _backgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
// Process data silently (NO UI updates — different isolate)
await processBackgroundData(message.data);
}
// Notification tap — app opened from notification
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
navigateToScreen(message.data['deepLink']);
});
// App launched from terminated state via notification
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
navigateToScreen(initialMessage.data['deepLink']);
}
// FirebaseMessagingService — handles all incoming messages
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// Token refreshed — send to server
sendTokenToServer(token)
}
override fun onMessageReceived(message: RemoteMessage) {
// Data message — always delivered here (foreground + background)
message.data.let { data ->
if (data.isNotEmpty()) {
processData(data)
}
}
// Notification message — only delivered here if app is in FOREGROUND
// In background, system shows it automatically
message.notification?.let { notification ->
showNotification(notification.title, notification.body, message.data)
}
}
private fun showNotification(title: String?, body: String?, data: Map<String, String>) {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("deepLink", data["deepLink"])
}
val pendingIntent = PendingIntent.getActivity(
, , intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
notification = NotificationCompat.Builder(, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel()
.setContentIntent(pendingIntent)
.build()
NotificationManagerCompat.from().notify(notificationId, notification)
}
}
// UNUserNotificationCenterDelegate
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
// Foreground — app is open
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let data = notification.request.content.userInfo
// Show banner even in foreground
completionHandler([.banner, .sound, .badge])
}
// User tapped notification
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler : () -> ) {
data response.notification.request.content.userInfo
deepLink data[] {
navigateToScreen(deepLink)
}
completionHandler()
}
}
Android 8+ (API 26) requires notification channels. Users can control each channel independently.
// Create channels at app startup (idempotent — safe to call repeatedly)
fun createNotificationChannels(context: Context) {
val manager = context.getSystemService(NotificationManager::class.java)
val channels = listOf(
NotificationChannel("orders", "Order Updates", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Updates about your orders"
enableVibration(true)
},
NotificationChannel("promotions", "Promotions", NotificationManager.IMPORTANCE_LOW).apply {
description = "Deals and special offers"
enableVibration(false)
},
NotificationChannel("chat", "Messages", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Chat messages"
enableVibration(true)
setShowBadge(true)
},
)
manager.createNotificationChannels(channels)
}
// Use channel when building notification
NotificationCompat.Builder(context, "orders") // channel ID
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Order Shipped")
.setContentText("Your order #1234 has been shipped")
.build()
Channel strategy:
| Channel | Importance | Use Case |
|---|---|---|
orders | HIGH | Transactional — order status, delivery |
chat | HIGH | Real-time messages |
reminders | DEFAULT | Scheduled reminders |
promotions | LOW | Marketing, deals (easy for user to mute) |
system | MIN | Background sync status, non-urgent updates |
Flutter:
// FCM payload with image
// Server sends: { "notification": { "title": "...", "body": "...", "image": "https://..." } }
// flutter_local_notifications for custom display
await flutterLocalNotificationsPlugin.show(
id,
title,
body,
NotificationDetails(
android: AndroidNotificationDetails(
'channel_id', 'Channel Name',
styleInformation: BigPictureStyleInformation(
DrawableResourceAndroidBitmap('large_image'),
),
),
iOS: DarwinNotificationDetails(
attachments: [DarwinNotificationAttachment(imagePath)],
),
),
);
Android — Big Picture / Big Text:
val style = NotificationCompat.BigPictureStyle()
.bigPicture(downloadedBitmap)
.bigLargeIcon(null as Bitmap?) // hide large icon when expanded
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("New Product")
.setContentText("Check out our latest arrival")
.setStyle(style)
.build()
iOS — Notification Service Extension (for images):
// NotificationService.swift (Notification Service Extension target)
class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent,
let imageURLString = content.userInfo["image_url"] as? String,
let imageURL = URL(string: imageURLString) else {
contentHandler(request.content)
return
}
// Download and attach image
downloadImage(from: imageURL) { localURL in
if let localURL = localURL,
let attachment = try? UNNotificationAttachment(identifier: "image", url: localURL) {
content.attachments = [attachment]
}
contentHandler(content)
}
}
}
Android:
val approveIntent = PendingIntent.getBroadcast(context, 0,
Intent(context, NotificationActionReceiver::class.java).putExtra("action", "approve"),
PendingIntent.FLAG_IMMUTABLE)
val rejectIntent = PendingIntent.getBroadcast(context, 0,
Intent(context, NotificationActionReceiver::class.java).putExtra("action", "reject"),
PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, channelId)
.setContentTitle("Approval Request")
.setContentText("John wants to join your team")
.addAction(R.drawable.ic_check, "Approve", approveIntent)
.addAction(R.drawable.ic_close, "Reject", rejectIntent)
.build()
iOS:
// Register action category
let approveAction = UNNotificationAction(identifier: "APPROVE", title: "Approve",
options: [.authenticationRequired])
let rejectAction = UNNotificationAction(identifier: "REJECT", title: "Reject",
options: [.destructive])
let category = UNNotificationCategory(identifier: "APPROVAL",
actions: [approveAction, rejectAction],
intentIdentifiers: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
// Handle action in delegate
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse, ...) {
switch response.actionIdentifier {
case "APPROVE": handleApprove(response.notification.request.content.userInfo)
case "REJECT": handleReject(response.notification.request.content.userInfo)
default: break
}
}
For scheduled reminders, timers, and offline triggers.
Flutter — flutter_local_notifications:
// Schedule a notification
await flutterLocalNotificationsPlugin.zonedSchedule(
id,
'Reminder',
'Don\'t forget to check your order',
tz.TZDateTime.now(tz.local).add(const Duration(hours: 1)),
const NotificationDetails(
android: AndroidNotificationDetails('reminders', 'Reminders'),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
Android:
// Using WorkManager for reliable scheduled notifications
val notificationWork = OneTimeWorkRequestBuilder<NotificationWorker>()
.setInitialDelay(1, TimeUnit.HOURS)
.setInputData(workDataOf("title" to "Reminder", "body" to "Check your order"))
.build()
WorkManager.getInstance(context).enqueue(notificationWork)
iOS:
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Don't forget to check your order"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString,
content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
For background data sync without user-visible notification.
FCM payload (data-only — no notification key):
{
"to": "device_token",
"data": {
"type": "sync",
"resource": "orders",
"timestamp": "2024-01-15T10:00:00Z"
}
}
iOS — enable background modes:
content-available: 1 in APNs payload// Flutter — subscribe to topic
await FirebaseMessaging.instance.subscribeToTopic('order_updates');
await FirebaseMessaging.instance.unsubscribeFromTopic('promotions');
// Android
FirebaseMessaging.getInstance().subscribeToTopic("order_updates")
FirebaseMessaging.getInstance().unsubscribeFromTopic("promotions")
// iOS
Messaging.messaging().subscribe(toTopic: "order_updates")
Messaging.messaging().unsubscribe(fromTopic: "promotions")
Topic vs. token targeting:
| Approach | Use Case | Server Complexity |
|---|---|---|
| Token-based | Personalized notifications (user-specific) | Server stores tokens, targets individually |
| Topic-based | Broadcast to groups (all iOS users, all premium users) | Simple, FCM handles distribution |
| Condition-based | Combine topics ('orders' in topics && 'premium' in topics) | Medium |
## Notification Architecture
- **Platform:** [Flutter / Android / iOS]
- **Push Provider:** [FCM / APNs / both]
- **Types:** [Transactional / Marketing / Real-time / Local]
## Implementation Plan
[Which notification types, channels, and features to implement]
## Payload Design
[Sample payloads for each notification type]
## Deep Link Mapping
[Notification type → target screen]
## User Preferences
[How users control notification settings]
onMessageReceived even in background; notification messages are NOT (system handles display)Source: ashutoshsrivastava17/skill-library — distributed by TomeVault.