Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Flipcash/Core/Controllers/PushController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,18 @@ class PushController {

/// Re-fetches the notification authorization status from the system.
func refreshAuthorizationStatus() async {
authorizationStatus = await Self.fetchStatus()
let current = await Self.fetchStatus()
let previous = UserDefaults.lastNotificationAuthStatus
.flatMap(UNAuthorizationStatus.init(rawValue:))

authorizationStatus = current

if let from = current.previousIfDenied(from: previous) {
Analytics.pushPermissionDenied(from: from)
}
if previous != current {
UserDefaults.lastNotificationAuthStatus = current.rawValue
}
}

// MARK: - Firebase -
Expand Down Expand Up @@ -245,3 +256,9 @@ private class NotificationDelegate: NSObject, @preconcurrency UNUserNotification
extension PushController {
static let mock: PushController = PushController(owner: .mock, client: .mock)
}

@MainActor
extension UserDefaults {
@Defaults(.lastNotificationAuthStatus)
static var lastNotificationAuthStatus: Int?
}
4 changes: 3 additions & 1 deletion Flipcash/Keychain/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ enum DefaultsKey: String {
case storedTokenMint = "com.flipcash.token.storedTokenMint"

case betaFlags = "com.flipcash.betaFlags"

case pendingPurchase = "com.flipcash.iap.pendingPurchase"

case lastNotificationAuthStatus = "com.flipcash.push.lastAuthorizationStatus"

// Legacy

Expand Down
38 changes: 38 additions & 0 deletions Flipcash/Utilities/Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import UserNotifications
import FlipcashCore

// MARK: - Domain Event Enums -
Expand Down Expand Up @@ -89,6 +90,10 @@ extension Analytics {
case parse = "Deeplink: Parse"
case routed = "Deeplink: Routed"
}

enum PushPermissionEvent: String, AnalyticsEvent {
case denied = "Push Permission Denied"
}
}

// MARK: - General -
Expand Down Expand Up @@ -271,6 +276,37 @@ extension Analytics {
}
}

// MARK: - Push Permission -

extension Analytics {
static func pushPermissionDenied(from previous: UNAuthorizationStatus) {
track(event: PushPermissionEvent.denied, properties: [
.from: previous.analyticsName,
])
}
}

extension UNAuthorizationStatus {
var analyticsName: String {
switch self {
case .notDetermined: "notDetermined"
case .denied: "denied"
case .authorized: "authorized"
case .provisional: "provisional"
case .ephemeral: "ephemeral"
@unknown default: "unknown"
}
}

/// Silent on `nil` baseline so that shipping this update does not emit a
/// spurious denial event for users whose push is already denied at first
/// foreground after install.
func previousIfDenied(from previous: UNAuthorizationStatus?) -> UNAuthorizationStatus? {
guard let previous, previous != .denied, self == .denied else { return nil }
return previous
}
}

// MARK: - Deeplinks -

extension Analytics {
Expand Down Expand Up @@ -326,6 +362,8 @@ extension Analytics {
case type = "Type"
case error = "Error"
case url = "URL"

case from = "From"
}
}

Expand Down
48 changes: 48 additions & 0 deletions FlipcashTests/PushPermissionAnalyticsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// PushPermissionAnalyticsTests.swift
// FlipcashTests
//

import Foundation
import Testing
import UserNotifications
@testable import Flipcash

@Suite("Push Permission Analytics")
struct PushPermissionAnalyticsTests {

@Test(
"previousIfDenied returns previous only on transition into .denied",
arguments: [
(UNAuthorizationStatus?.none, UNAuthorizationStatus.denied, UNAuthorizationStatus?.none),
(.some(.authorized), .authorized, nil),
(.some(.authorized), .denied, .authorized),
(.some(.notDetermined), .denied, .notDetermined),
(.some(.provisional), .denied, .provisional),
(.some(.denied), .authorized, nil),
(.some(.denied), .notDetermined, nil),
(.some(.authorized), .provisional, nil),
] as [(UNAuthorizationStatus?, UNAuthorizationStatus, UNAuthorizationStatus?)]
)
func previousIfDenied(
previous: UNAuthorizationStatus?,
current: UNAuthorizationStatus,
expected: UNAuthorizationStatus?
) {
#expect(current.previousIfDenied(from: previous) == expected)
}

@Test(
"analyticsName covers every known UNAuthorizationStatus case",
arguments: [
(UNAuthorizationStatus.notDetermined, "notDetermined"),
(.denied, "denied"),
(.authorized, "authorized"),
(.provisional, "provisional"),
(.ephemeral, "ephemeral"),
]
)
func analyticsName(status: UNAuthorizationStatus, expected: String) {
#expect(status.analyticsName == expected)
}
}