Skip to content

Commit 0416951

Browse files
authored
Add fetchNotificationToken() (#1)
* Add fetchNotificationToken() * Update README.md * Check for UIKit canImport before using UNUserNotificationCenter
1 parent 7c73f6d commit 0416951

File tree

3 files changed

+126
-7
lines changed

3 files changed

+126
-7
lines changed

Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ let package = Package(
1010
],
1111
dependencies: [
1212
.package(url: "https://source.skip.tools/skip.git", from: "1.0.0"),
13-
.package(url: "https://source.skip.tools/skip-foundation.git", from: "1.0.0"),
13+
.package(url: "https://source.skip.tools/skip-kit.git", "0.0.0"..<"2.0.0"),
1414
],
1515
targets: [
16-
.target(name: "SkipNotify", dependencies: [.product(name: "SkipFoundation", package: "skip-foundation")], plugins: [.plugin(name: "skipstone", package: "skip")]),
16+
.target(name: "SkipNotify", dependencies: [.product(name: "SkipKit", package: "skip-kit")], plugins: [.plugin(name: "skipstone", package: "skip")]),
1717
.testTarget(name: "SkipNotifyTests", dependencies: [
1818
"SkipNotify",
1919
.product(name: "SkipTest", package: "skip")
@@ -22,9 +22,9 @@ let package = Package(
2222
)
2323

2424
if Context.environment["SKIP_BRIDGE"] ?? "0" != "0" {
25-
package.dependencies += [.package(url: "https://source.skip.tools/skip-bridge.git", "0.0.0"..<"2.0.0")]
25+
package.dependencies += [.package(url: "https://source.skip.tools/skip-fuse-ui.git", from: "1.0.0")]
2626
package.targets.forEach({ target in
27-
target.dependencies += [.product(name: "SkipBridge", package: "skip-bridge")]
27+
target.dependencies += [.product(name: "SkipFuseUI", package: "skip-fuse-ui")]
2828
})
2929
// all library types must be dynamic to support bridging
3030
package.products = package.products.map({ product in

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SkipNotify
22

3-
3+
The is a Skip framework that provides support for notifications
4+
on iOS and Android.
45

56
## Setup
67

@@ -24,6 +25,24 @@ let package = Package(
2425
)
2526
```
2627

28+
## Configuration
29+
30+
Enabling push notifications in your app requires a series of steps that differ
31+
between iOS and Android. Following is an outline of the tasks required to
32+
activate and configure push notifications.
33+
34+
### iOS
35+
36+
Follow the steps described in the
37+
[Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
38+
documentation:
39+
40+
- Select your app from the App Store Connect [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/) page and select "Capabilites" and turn on Push Notifications then click "Save"
41+
- Use the [Push Notifications Console](https://developer.apple.com/notifications/push-notifications-console/) to send a test message to your app.
42+
43+
### Android
44+
45+
2746
## License
2847

2948
This software is licensed under the
Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,106 @@
11
// Copyright 2023–2025 Skip
22
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
3+
#if !SKIP_BRIDGE
4+
#if canImport(UIKit) // UNUserNotificationCenter does not exist on macOS
5+
import Foundation
6+
#if !SKIP
7+
import UIKit
8+
import OSLog
9+
#else
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.tasks.await
314

4-
public class SkipNotifyModule {
5-
15+
import com.google.firebase.messaging.FirebaseMessagingService
16+
import com.google.firebase.messaging.FirebaseMessaging
17+
import com.google.firebase.messaging.RemoteMessage
18+
#endif
19+
20+
private let logger: Logger = Logger(subsystem: "skip.notify", category: "SkipNotify") // adb logcat '*:S' 'skip.notify.SkipNotify:V'
21+
22+
public class SkipNotify {
23+
public static let shared = SkipNotify()
24+
25+
private init() {
26+
}
27+
28+
public func fetchNotificationToken() async throws -> String {
29+
#if SKIP
30+
FirebaseMessaging.getInstance().token.await()
31+
#else
32+
UNUserNotificationCenter.current().delegate = notificationCenterDelegate
33+
34+
// these notifications are added to the default UIApplicationDelegate
35+
// created by skip init/skip create in Main.swift;
36+
// other project structures will need to manually add them as so:
37+
/** ```
38+
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
39+
NotificationCenter.default.post(name: NSNotification.Name("didRegisterForRemoteNotificationsWithDeviceToken"), object: application, userInfo: ["deviceToken": deviceToken])
40+
}
41+
42+
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
43+
NotificationCenter.default.post(name: NSNotification.Name("didFailToRegisterForRemoteNotificationsWithError"), object: application, userInfo: ["error": error])
44+
}
45+
``` */
46+
let didRegisterNotification = NSNotification.Name("didRegisterForRemoteNotificationsWithDeviceToken")
47+
let didFailToRegisterNotification = NSNotification.Name("didFailToRegisterForRemoteNotificationsWithError")
48+
49+
var observers: [Any] = []
50+
func clearObservers() {
51+
observers.forEach({ NotificationCenter.default.removeObserver($0) })
52+
}
53+
return try await withCheckedThrowingContinuation { continuation in
54+
observers += [NotificationCenter.default.addObserver(forName: didRegisterNotification, object: nil, queue: .main) { note in
55+
logger.log("recevied \(didRegisterNotification.rawValue) with userInfo: \(note.userInfo ?? [:])")
56+
guard let deviceToken = note.userInfo?["deviceToken"] as? Data else {
57+
return
58+
}
59+
// hex-encoded form of the deviceToken data
60+
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
61+
clearObservers()
62+
continuation.resume(returning: tokenString)
63+
}]
64+
observers += [NotificationCenter.default.addObserver(forName: didFailToRegisterNotification, object: nil, queue: .main) { note in
65+
logger.log("recevied \(didFailToRegisterNotification.rawValue) with userInfo: \(note.userInfo ?? [:])")
66+
guard let error = note.userInfo?["error"] as? Error else {
67+
return
68+
}
69+
clearObservers()
70+
continuation.resume(throwing: error)
71+
}]
72+
Task { @MainActor in
73+
logger.log("calling UIApplication.shared.registerForRemoteNotifications()")
74+
UIApplication.shared.registerForRemoteNotifications()
75+
}
76+
}
77+
#endif
78+
}
79+
80+
#if !SKIP
81+
let notificationCenterDelegate = NotificationCenterDelegate()
82+
83+
class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
84+
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
85+
// Callback when app is in foreground and notification arrives
86+
// e.g., userNotificationCenter willPresent notification: <UNNotification: 0x1117e18f0; source: org.appfair.app.Showcase date: 2025-12-18 22:04:52 +0000, request: <UNNotificationRequest: 0x1117e1890; identifier: 8B514E79-74CD-4399-BBF5-C2F4F9663292, content: <UNNotificationContent: 0x111835400; title: <redacted>, subtitle: <redacted>, body: <redacted>, attributedBody: (null), summaryArgument: (null), summaryArgumentCount: 0, categoryIdentifier: , launchImageName: , threadIdentifier: , attachments: (), badge: (null), sound: (null), realert: 0, interruptionLevel: 1, relevanceScore: 0.00, filterCriteria: (null), screenCaptureProhibited: 0, speechLanguage: (null), trigger: <UNPushNotificationTrigger: 0x104ac3440; contentAvailable: NO, mutableContent: NO>>, intents: (
87+
logger.log("userNotificationCenter willPresent notification: \(notification)")
88+
return UNNotificationPresentationOptions.banner
89+
}
90+
91+
// Callback when app is in background and user taps notification to open the app
92+
// userNotificationCenter didReceive response: <UNNotificationResponse: 0x11186c510; actionIdentifier: com.apple.UNNotificationDefaultActionIdentifier, notification: <UNNotification: 0x11186f270; source: org.appfair.app.Showcase date: 2025-12-18 22:06:22 +0000, request: <UNNotificationRequest: 0x11186f360; identifier: 79B5A08E-84D4-4A03-A7D4-ABC60B8AAF77, content: <UNNotificationContent: 0x110dccc80; title: <redacted>, subtitle: <redacted>, body: <redacted>, attributedBody: (null), summaryArgument: (null), summaryArgumentCount: 0, categoryIdentifier: , launchImageName: , threadIdentifier: , attachments: (), badge: (null), sound: (null), realert: 0, interruptionLevel: 1, relevanceScore: 0.00, filterCriteria: (null), screenCaptureProhibited: 0, speechLanguage: (null), trigger: <UNPushNotificationTrigger: 0x111ab4550; contentAvailable: NO, mutableContent: NO>>, intents: ()>>
93+
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
94+
logger.log("userNotificationCenter didReceive response: \(response)")
95+
}
96+
97+
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
98+
logger.log("userNotificationCenter openSettingsFor notification: \(notification)")
99+
}
100+
101+
}
102+
#endif
6103
}
104+
105+
#endif
106+
#endif

0 commit comments

Comments
 (0)