Skip to content

Commit

Permalink
Add fallback to enable notifications (#3199)
Browse files Browse the repository at this point in the history
* Add fallback to enable notifications

* Remove enable airship usage

* Fix
  • Loading branch information
rlepinski authored Sep 10, 2024
1 parent ec1d0a0 commit 8a2d229
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 136 deletions.
6 changes: 3 additions & 3 deletions Airship Sample/Airship Sample/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ struct HomeView: View {
@MainActor
func togglePushEnabled() {
if (!pushEnabled) {
Airship.privacyManager.enableFeatures(.push)
Airship.push.userPushNotificationsEnabled = true
Airship.push.backgroundPushNotificationsEnabled = true
Task {
await Airship.push.enableUserPushNotifications(fallback: .systemSettings)
}
} else {
Airship.push.userPushNotificationsEnabled = false
}
Expand Down
4 changes: 0 additions & 4 deletions Airship/Airship.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@
3CC95B2B2696549B00FE2ACD /* AirshipPushableComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC95B2A2696549B00FE2ACD /* AirshipPushableComponent.swift */; };
3CC95B2C2696549B00FE2ACD /* AirshipPushableComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC95B2A2696549B00FE2ACD /* AirshipPushableComponent.swift */; };
45A8ADF023134B38004AD8CA /* testMCColorsCatalog.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 45A8ADD123133E51004AD8CA /* testMCColorsCatalog.xcassets */; };
54DE2901247B2AF059E46862 /* Pods_AirshipTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBF83042659DAF0D42AD7A9E /* Pods_AirshipTests.framework */; };
6014AD672C1B5F540072DCF0 /* ChallengeResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6014AD662C1B5F540072DCF0 /* ChallengeResolver.swift */; };
6014AD6C2C2032730072DCF0 /* ChallengeResolverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6014AD6A2C2032360072DCF0 /* ChallengeResolverTest.swift */; };
6014AD752C20410B0072DCF0 /* airship.der in Resources */ = {isa = PBXBuildFile; fileRef = 6014AD742C20410A0072DCF0 /* airship.der */; };
Expand Down Expand Up @@ -3045,7 +3044,6 @@
buildActionMask = 2147483647;
files = (
CC64F0591D8B77E3009CEF27 /* AirshipCore.framework in Frameworks */,
54DE2901247B2AF059E46862 /* Pods_AirshipTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -9117,7 +9115,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 0F55D47F540C142B0F469570 /* Pods-AirshipTests.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
Expand Down Expand Up @@ -9158,7 +9155,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 62C67C80125440E0C4F9982D /* Pods-AirshipTests.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
Expand Down
4 changes: 2 additions & 2 deletions Airship/AirshipCore/Source/EnableFeatureAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public final class EnableFeatureAction: AirshipAction {
public func perform(arguments: ActionArguments) async throws -> AirshipJSON? {
let permission = try parsePermission(arguments: arguments)

let (start, end) = await self.permissionPrompter()
let result = await self.permissionPrompter()
.prompt(
permission: permission,
enableAirshipUsage: true,
Expand All @@ -71,7 +71,7 @@ public final class EnableFeatureAction: AirshipAction {
EnableFeatureAction.resultReceiverMetadataKey
] as? PermissionResultReceiver

await resultReceiver?(permission, start, end)
await resultReceiver?(permission, result.startStatus, result.endStatus)

return nil
}
Expand Down
68 changes: 8 additions & 60 deletions Airship/AirshipCore/Source/PermissionPrompter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,81 +13,29 @@ protocol PermissionPrompter: Sendable {
permission: AirshipPermission,
enableAirshipUsage: Bool,
fallbackSystemSettings: Bool
) async -> (AirshipPermissionStatus, AirshipPermissionStatus)

) async -> AirshipPermissionResult
}

struct AirshipPermissionPrompter: PermissionPrompter {

private let permissionsManager: AirshipPermissionsManager
private let notificationCenter: NotificationCenter

init(
permissionsManager: AirshipPermissionsManager,
notificationCenter: NotificationCenter = NotificationCenter.default
permissionsManager: AirshipPermissionsManager
) {
self.permissionsManager = permissionsManager
self.notificationCenter = notificationCenter
}

@MainActor
func prompt(
permission: AirshipPermission,
enableAirshipUsage: Bool,
fallbackSystemSettings: Bool
) async -> (AirshipPermissionStatus, AirshipPermissionStatus) {

let startResult = await self.permissionsManager.checkPermissionStatus(permission)
if fallbackSystemSettings && startResult == .denied {
#if !os(watchOS)
let endResult = await self.requestSystemSettingsChange(permission: permission)
#else
let endResult = await self.permissionsManager.requestPermission(
permission,
enableAirshipUsageOnGrant: enableAirshipUsage
)
#endif
return (startResult, endResult)

} else {
let endResult = await self.permissionsManager.requestPermission(
permission,
enableAirshipUsageOnGrant: enableAirshipUsage
)

return (startResult, endResult)
}
}

#if !os(watchOS)

@MainActor
private func requestSystemSettingsChange(
permission: AirshipPermission
) async -> AirshipPermissionStatus {
if let url = URL(string: UIApplication.openSettingsURLString) {
await UIApplication.shared.open(url, options: [:])
await waitNextOpen()
} else {
AirshipLogger.error("Unable to navigate to system settings.")
}

return await self.permissionsManager.checkPermissionStatus(permission)
) async -> AirshipPermissionResult {
return await self.permissionsManager.requestPermission(
permission,
enableAirshipUsageOnGrant: enableAirshipUsage,
fallback: fallbackSystemSettings ? .systemSettings : .none
)
}


@MainActor
private func waitNextOpen() async {
var subscription: AnyCancellable?
await withCheckedContinuation { continuation in
subscription = self.notificationCenter.publisher(for: AppStateTracker.didBecomeActiveNotification)
.sink { _ in
continuation.resume()
}
}

subscription?.cancel()
}
#endif

}
169 changes: 147 additions & 22 deletions Airship/AirshipCore/Source/PermissionsManager.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* Copyright Airship and Contributors */

import Foundation
import Combine

/// Airship permissions manager.
///
Expand All @@ -22,18 +21,26 @@ public final class AirshipPermissionsManager: NSObject, @unchecked Sendable {
] = [:]

private let statusUpdates: AirshipAsyncChannel<(AirshipPermission, AirshipPermissionStatus)> = AirshipAsyncChannel()
private var notificationListener: AnyCancellable?

init(notificationCenter: NotificationCenter = .default) {
private let appStateTracker: AppStateTrackerProtocol
private let systemSettingsNavigator: SystemSettingsNavigatorProtocol

@MainActor
init(
appStateTracker: AppStateTrackerProtocol? = nil,
systemSettingsNavigator: SystemSettingsNavigatorProtocol = SystemSettingsNavigator()
) {
self.appStateTracker = appStateTracker ?? AppStateTracker.shared
self.systemSettingsNavigator = systemSettingsNavigator
super.init()
notificationListener = notificationCenter
.publisher(for: AppStateTracker.didBecomeActiveNotification)
.sink(receiveValue: { _ in
Task { @MainActor [weak self] in

Task { @MainActor [weak self] in
guard let updates = self?.appStateTracker.stateUpdates else { return }
for await update in updates {
if (update == .active) {
await self?.refreshPermissionStatuses()
}
})
}
}
}

var configuredPermissions: Set<AirshipPermission> {
Expand Down Expand Up @@ -141,40 +148,84 @@ public final class AirshipPermissionsManager: NSObject, @unchecked Sendable {
///
/// - Parameters:
/// - permission: The permission.
/// - enableAirshipUsageOnGrant: `true` to allow any Airship features that need the permission to be enabled as well, e.g., enabling push privacy manager feature and user notifications if `.postNotifications` is granted.
/// - enableAirshipUsageOnGrant: `true` to allow any Airship features that need the permission to be enabled as well, e.g., enabling push privacy manager feature and user notifications if `.displayNotifications` is granted.
/// - completionHandler: The completion handler.
@objc
@MainActor
public func requestPermission(
_ permission: AirshipPermission,
enableAirshipUsageOnGrant: Bool
) async -> AirshipPermissionStatus {
let status: AirshipPermissionStatus? = try? await self.queue.run { @MainActor in
return await requestPermission(
permission,
enableAirshipUsageOnGrant: enableAirshipUsageOnGrant,
fallback: .none
).endStatus
}

/// Requests a permission.
///
/// - Parameters:
/// - permission: The permission.
/// - enableAirshipUsageOnGrant: `true` to allow any Airship features that need the permission to be enabled as well, e.g., enabling push privacy manager feature and user notifications if `.displayNotifications` is granted.
/// - fallback: The fallback behavior if the permission is alreay denied.
/// - Returns: A `AirshipPermissionResult` with the starting and ending status If no permission delegate is
/// set for the given permission the status will be `.notDetermined`
@MainActor
public func requestPermission(
_ permission: AirshipPermission,
enableAirshipUsageOnGrant: Bool,
fallback: PromptPermissionFallback
) async -> AirshipPermissionResult {
let status: AirshipPermissionResult? = try? await self.queue.run { @MainActor in
guard let delegate = self.permissionDelegate(permission) else {
return .notDetermined
return AirshipPermissionResult.notDetermined
}

let status = await delegate.requestPermission()
let startingStatus = await delegate.checkPermissionStatus()
var endStatus: AirshipPermissionStatus = .notDetermined

if status == .granted {
switch(startingStatus) {
case .granted:
endStatus = .granted
case .notDetermined:
endStatus = await delegate.requestPermission()
case .denied:
switch fallback {
case .none:
endStatus = .denied
case .systemSettings:
if await self.systemSettingsNavigator.open(for: permission) {
await self.appStateTracker.waitForActive()
endStatus = await delegate.checkPermissionStatus()
} else {
endStatus = .denied
}
case .callback(let callback):
await callback()
endStatus = await delegate.checkPermissionStatus()
}
}

if endStatus == .granted {
await self.onPermissionEnabled(
permission,
enableAirshipUsage: enableAirshipUsageOnGrant
)
}

await self.onExtend(permission: permission, status: status)
await self.onExtend(permission: permission, status: endStatus)

return status
return AirshipPermissionResult(startStatus: startingStatus, endStatus: endStatus)
}
let result = status ?? .notDetermined
await statusUpdates.send((permission, result))

let result = status ?? AirshipPermissionResult.notDetermined

await statusUpdates.send((permission, result.endStatus))

return result
}

/// - Note: for internal use only. :nodoc:
func addRequestExtender(
permission: AirshipPermission,
Expand Down Expand Up @@ -251,5 +302,79 @@ public final class AirshipPermissionsManager: NSObject, @unchecked Sendable {
await extender(status)
}
}
}

public struct AirshipPermissionResult: Sendable {
/// Starting status
public var startStatus: AirshipPermissionStatus

/// Ending status
public var endStatus: AirshipPermissionStatus

public init(startStatus: AirshipPermissionStatus, endStatus: AirshipPermissionStatus) {
self.startStatus = startStatus
self.endStatus = endStatus
}

static var notDetermined: AirshipPermissionResult {
AirshipPermissionResult(startStatus: .notDetermined, endStatus: .notDetermined)
}
}



/// Prompt permission fallback to be used if the requested permission is already denied.
public enum PromptPermissionFallback: Sendable {
/// No fallback
case none
/// Navigate to system settings
case systemSettings
// Custom callback
case callback(@MainActor @Sendable () async -> Void)
}



protocol SystemSettingsNavigatorProtocol: Sendable {
@MainActor
func open(for: AirshipPermission) async -> Bool
}

struct SystemSettingsNavigator: SystemSettingsNavigatorProtocol {
#if !os(watchOS)
@MainActor
func open(for permission: AirshipPermission) async -> Bool {
if let url = systemSettingURLForPermission(permission) {
return await UIApplication.shared.open(url, options: [:])
} else {
return false
}
}

@MainActor
private func systemSettingURLForPermission(_ permission: AirshipPermission) -> URL? {
let string = switch(permission) {
case .displayNotifications:
if #available(iOS 16.0, tvOS 16.0, macCatalyst 16.0, visionOS 1.0, *) {
UIApplication.openNotificationSettingsURLString
} else if #available(iOS 15.4, tvOS 15.4, macCatalyst 15.4, *) {
UIApplicationOpenNotificationSettingsURLString
} else {
UIApplication.openSettingsURLString
}
case .location:
UIApplication.openSettingsURLString
}

return URL(string: string)
}
#else

@MainActor
func open(for permission: AirshipPermission) async -> Bool {
return false
}

#endif

}
Loading

0 comments on commit 8a2d229

Please sign in to comment.