Skip to content

Commit

Permalink
Adapt implementation, add XCT targets and ui tests and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Sep 30, 2024
1 parent d399ef4 commit 9228bb0
Show file tree
Hide file tree
Showing 41 changed files with 1,574 additions and 267 deletions.
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This source file is part of the SpeziNotifications open source project
#
# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
# SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
#
# SPDX-License-Identifier: MIT
#
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# This source file is part of the SpeziNotifications open source project
#
# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
# SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
#
# SPDX-License-Identifier: MIT
#
Expand Down
4 changes: 2 additions & 2 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
MIT License

Copyright (c) 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
Copyright (c) 2024 Stanford University and the project authors (see CONTRIBUTORS.md)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 changes: 22 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// This source file is part of the SpeziNotifications open source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//
Expand All @@ -14,6 +14,7 @@ import PackageDescription

let package = Package(
name: "SpeziNotifications",
defaultLocalization: "en",
platforms: [
.iOS(.v17),
.watchOS(.v10),
Expand All @@ -22,10 +23,13 @@ let package = Package(
.macOS(.v14)
],
products: [
.library(name: "SpeziNotifications", targets: ["SpeziNotifications"])
.library(name: "SpeziNotifications", targets: ["SpeziNotifications"]),
.library(name: "XCTSpeziNotifications", targets: ["XCTSpeziNotifications"]),
.library(name: "XCTSpeziNotificationsUI", targets: ["XCTSpeziNotificationsUI"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi.git", branch: "feature/application-for-swiftui")
.package(url: "https://github.com/StanfordSpezi/Spezi.git", branch: "feature/application-for-swiftui"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", branch: "feature/additional-infrastructure")
] + swiftLintPackage(),
targets: [
.target(
Expand All @@ -35,6 +39,21 @@ let package = Package(
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "XCTSpeziNotifications",
dependencies: [
.target(name: "SpeziNotifications")
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "XCTSpeziNotificationsUI",
dependencies: [
.target(name: "SpeziNotifications"),
.product(name: "SpeziViews", package: "SpeziViews")
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziNotificationsTests",
dependencies: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This source file is part of the SpeziNotifications open source project
SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
SPDX-License-Identifier: MIT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,17 @@
// SPDX-License-Identifier: MIT
//

@_spi(APISupport)
import Spezi
import SpeziFoundation
import SwiftUI


@MainActor
private final class RemoteNotificationContinuation: KnowledgeSource, Sendable {
typealias Anchor = SpeziAnchor

fileprivate(set) var continuation: CheckedContinuation<Data, Error>?
fileprivate(set) var access = AsyncSemaphore()


init() {}


@MainActor
func resume(with result: Result<Data, Error>) {
if let continuation {
self.continuation = nil
access.signal()
continuation.resume(with: result)
}
}
}


extension Spezi {
/// Registers to receive remote notifications through Apple Push Notification service.
///
/// Refer to the documentation of ``Spezi/registerRemoteNotifications``.
public struct RegisterForRemoteNotificationsAction: Sendable {
public struct RegisterForRemoteNotificationsAction {
private weak var spezi: Spezi?

fileprivate init(_ spezi: Spezi) {
Expand All @@ -61,28 +40,7 @@ extension Spezi {
preconditionFailure("RegisterRemoteNotificationsAction was used in a scope where Spezi was not available anymore!")
}

let registration: RemoteNotificationContinuation
if let existing = spezi.storage[RemoteNotificationContinuation.self] {
registration = existing
} else {
registration = RemoteNotificationContinuation()
spezi.storage[RemoteNotificationContinuation.self] = registration
}

try await registration.access.waitCheckingCancellation()

#if targetEnvironment(simulator)
async let _ = withTimeout(of: .seconds(5)) { @MainActor in
spezi.logger.warning("Registering for remote notifications seems to be not possible on this simulator device. Timing out ...")
spezi.storage[RemoteNotificationContinuation.self]?.resume(with: .failure(TimeoutError()))
}
#endif

return try await withCheckedThrowingContinuation { continuation in
assert(registration.continuation == nil, "continuation wasn't nil")
registration.continuation = continuation
_Application.shared.registerForRemoteNotifications()
}
return try await spezi.remoteNotificationRegistrationSupport()
}
}

Expand All @@ -109,8 +67,7 @@ extension Spezi {
/// var registerRemoteNotifications
///
/// func handleNotificationsPermissions() async throws {
/// // Make sure to request notifications permissions before registering for remote notifications
/// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
/// // Make sure to request notifications permissions before registering for remote notifications ...
///
///
/// do {
Expand All @@ -128,41 +85,74 @@ extension Spezi {
/// }
/// ```
///
/// > Tip: Make sure to request authorization by calling [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:))
/// > Tip: Make sure to request authorization by calling ``requestNotificationAuthorization``
/// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently.
///
/// ## Topics
/// ### Action
/// - ``RegisterRemoteNotificationsAction``
/// - ``RegisterForRemoteNotificationsAction``
public var registerRemoteNotifications: RegisterForRemoteNotificationsAction {
RegisterForRemoteNotificationsAction(self)
}
}


extension Spezi.RegisterForRemoteNotificationsAction {
@MainActor
static func handleDeviceTokenUpdate(_ spezi: Spezi, _ deviceToken: Data) {
guard let registration = spezi.storage[RemoteNotificationContinuation.self] else {
return
extension EnvironmentValues {
/// Registers to receive remote notifications through Apple Push Notification service.
///
/// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications)
/// documentation for `UIApplication` or for the respective equivalent for your current platform.
///
/// - Note: For more information on the general topic on how to register your app with APNs,
/// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
/// article.
///
/// Below is a short code example on how to use this action within your ``Module``.
///
/// - Warning: Registering for Remote Notifications on Simulator devices might not be possible if your are not signed into an Apple ID on the host machine.
/// The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror)
/// in such a case.
///
/// ```swift
/// import SpeziFoundation
///
/// struct ExampleView: View {
/// @Environment(\.registerRemoteNotifications)
/// private var registerRemoteNotifications
///
/// var body: some View {
/// // ...
/// }
///
/// private func handleNotificationsPermissions() async throws {
/// // Make sure to request notifications permissions before registering for remote notifications
/// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
///
///
/// do {
/// let deviceToken = try await registerRemoteNotifications()
/// } catch let error as TimeoutError {
/// #if targetEnvironment(simulator)
/// return // override logic when running within a simulator
/// #else
/// throw error
/// #endif
/// }
///
/// // .. send the device token to your remote server that generates push notifications
/// }
/// }
/// ```
///
/// > Tip: Make sure to request authorization by calling ``requestNotificationAuthorization``
/// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently.
@MainActor public var registerRemoteNotifications: Spezi.RegisterForRemoteNotificationsAction {
guard let spezi = SpeziAppDelegate.spezi else {
preconditionFailure("@Environment(\\.registerRemoteNotifications) can only be accessed within a Spezi application.")
}

// might also be called if, e.g., app is restored from backup and is automatically registered for remote notifications.
// This can be handled through the `NotificationHandler` protocol.

registration.resume(with: .success(deviceToken))
return Spezi.RegisterForRemoteNotificationsAction(spezi)
}
}

@MainActor
static func handleFailedRegistration(_ spezi: Spezi, _ error: Error) {
guard let registration = spezi.storage[RemoteNotificationContinuation.self] else {
return
}

if registration.continuation == nil {
spezi.logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.")
}

registration.resume(with: .failure(error))
}
}
extension Spezi.RegisterForRemoteNotificationsAction: Sendable {}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension Spezi {
///
/// Refer to ``Spezi/requestNotificationAuthorization`` for documentation.
public struct RequestNotificationAuthorizationAction {
fileprivate init() {} // TODO: apply this everywhere!
fileprivate init() {}

/// Request notification authorization.
/// - Parameter options: The authorization options your app is requesting.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension Spezi {
/// Unregisters for all remote notifications received through Apple Push Notification service.
///
/// Refer to the documentation of ``Spezi/unregisterRemoteNotifications``.
public struct UnregisterForRemoteNotificationsAction: Sendable {
public struct UnregisterForRemoteNotificationsAction {
fileprivate init() {}


Expand Down Expand Up @@ -46,8 +46,39 @@ extension Spezi {
///
/// ## Topics
/// ### Action
/// - ``UnregisterRemoteNotificationsAction``
/// - ``UnregisterForRemoteNotificationsAction``
public var unregisterRemoteNotifications: UnregisterForRemoteNotificationsAction {
UnregisterForRemoteNotificationsAction()
}
}


extension EnvironmentValues {
/// Unregisters for all remote notifications received through Apple Push Notification service.
///
/// For more information refer to the [`unregisterForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications)
/// documentation for `UIApplication` or for the respective equivalent for your current platform.
///
/// Below is a short code example on how to use this action within your `View`.
///
/// ```swift
/// struct ExampleView: View {
/// @Environment(\.unregisterRemoteNotifications)
/// private var unregisterRemoteNotifications
///
/// var body: some View {
/// Button("Disable Notifications") {
/// Task {
/// try await unregisterRemoteNotifications()
/// }
/// }
/// }
/// }
/// ```
public var unregisterRemoteNotifications: Spezi.UnregisterForRemoteNotificationsAction {
Spezi.UnregisterForRemoteNotificationsAction()
}
}


extension Spezi.UnregisterForRemoteNotificationsAction: Sendable {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import UserNotifications


extension UNAuthorizationStatus: @retroactive CustomStringConvertible, @retroactive CustomDebugStringConvertible {
public var description: String {
switch self {
case .notDetermined:
"notDetermined"
case .denied:
"denied"
case .authorized:
"authorized"
case .provisional:
"provisional"
case .ephemeral:
"ephemeral"
@unknown default:
"unknown(\(rawValue))"
}
}

public var debugDescription: String {
description
}
}
Loading

0 comments on commit 9228bb0

Please sign in to comment.