Skip to content

Commit

Permalink
✨ Add push response handler
Browse files Browse the repository at this point in the history
  • Loading branch information
mmaatttt committed May 6, 2024
1 parent 5c3de3a commit 2b10b53
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Sources/AppcuesKit/Appcues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ public class Appcues: NSObject {
return container.resolve(DeepLinkHandling.self).didHandleURL(url)
}

public func didReceiveNotification(response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool {
let userInfo = response.notification.request.content.userInfo
return container.resolve(PushMonitoring.self).didReceiveNotification(userInfo: userInfo, completionHandler: completionHandler)
}

func initializeContainer() {
container.owner = self
container.register(Config.self, value: config)
Expand Down
4 changes: 4 additions & 0 deletions Sources/AppcuesKit/Data/Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ internal enum Events {
case deviceUnregistered = "appcues:device_unregistered"
}

enum Push: String {
case pushOpened = "appcues:push_opened"
}

enum Experience: String {
case stepSeen = "appcues:v2:step_seen"
case stepInteraction = "appcues:v2:step_interaction"
Expand Down
6 changes: 6 additions & 0 deletions Sources/AppcuesKit/Presentation/Actions/ActionRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ internal class ActionRegistry {
execute(transformQueue(actionInstances), completion: completion)
}

/// Enqueue the action instances.
/// This version is used for push action handlers.
func enqueue(actionInstances: [AppcuesExperienceAction], completion: @escaping () -> Void) {
execute(transformQueue(actionInstances), completion: completion)
}

/// Enqueue the action instances generated from a factory function to be executed.
/// This version is used for post-completion actions on an experience.
func enqueue(actionFactory: (Appcues?) -> [AppcuesExperienceAction]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ internal struct LoggedEvent: Identifiable {
case let .event(name, _) where Events.Device(rawValue: name) != nil:
self.type = .device
self.name = name.prettifiedEventName
case let .event(name, _) where Events.Push(rawValue: name) != nil:
self.type = .push
self.name = name.prettifiedEventName
case let .event(name, _):
self.type = .custom
self.name = name
Expand Down Expand Up @@ -134,6 +137,7 @@ extension LoggedEvent {
case session
case experience
case device
case push

var description: String {
switch self {
Expand All @@ -144,6 +148,7 @@ extension LoggedEvent {
case .session: return "Session"
case .experience: return "Experience"
case .device: return "Device"
case .push: return "Push"
}
}

Expand All @@ -156,6 +161,7 @@ extension LoggedEvent {
case .session: return "clock.arrow.2.circlepath"
case .experience: return "arrow.right.square"
case .device: return "iphone"
case .push: return "bell.badge"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal enum ExperienceTrigger: Equatable {
case qualification(reason: QualifyResponse.QualificationReason?)
case experienceCompletionAction(fromExperienceID: UUID?)
case launchExperienceAction(fromExperienceID: UUID?)
case push
case showCall
case deepLink
case preview
Expand All @@ -24,7 +25,7 @@ internal enum ExperienceTrigger: Equatable {
switch self {
case .qualification:
return false
case .experienceCompletionAction, .launchExperienceAction, .showCall, .deepLink, .preview:
case .experienceCompletionAction, .launchExperienceAction, .push, .showCall, .deepLink, .preview:
return true
}
}
Expand Down
49 changes: 49 additions & 0 deletions Sources/AppcuesKit/Push/ParsedNotification.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ParsedNotification.swift
// AppcuesKit
//
// Created by Matt on 2024-03-05.
// Copyright © 2024 Appcues. All rights reserved.
//

import Foundation

internal struct ParsedNotification {
let accountID: String
let userID: String
let notificationID: String
let workflowID: String
let workflowTaskID: String
let transactionID: String
let deepLinkURL: URL?
let experienceID: String?
let attachmentURL: URL?
let attachmentType: String?
let isTest: Bool

init?(userInfo: [AnyHashable: Any]) {
guard let accountID = userInfo["appcues_account_id"] as? String,
let userID = userInfo["appcues_user_id"] as? String,
let notificationID = userInfo["appcues_notification_id"] as? String,
let workflowID = userInfo["appcues_workflow_id"] as? String,
let workflowTaskID = userInfo["appcues_workflow_task_id"] as? String,
let transactionID = userInfo["appcues_transaction_id"] as? String else {
return nil
}

self.accountID = accountID
self.userID = userID
self.notificationID = notificationID
self.workflowID = workflowID
self.workflowTaskID = workflowTaskID
self.transactionID = transactionID

self.deepLinkURL = (userInfo["appcues_deep_link_url"] as? String)
.flatMap { URL(string: $0) }
self.experienceID = userInfo["appcues_experience_id"] as? String
self.attachmentURL = (userInfo["appcues_attachment_url"] as? String)
.flatMap { URL(string: $0) }
self.attachmentType = userInfo["appcues_attachment_type"] as? String
self.isTest = userInfo["appcues_test"] as? Bool ?? false
}
}
57 changes: 57 additions & 0 deletions Sources/AppcuesKit/Push/PushMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ internal protocol PushMonitoring: AnyObject {
var pushPrimerEligible: Bool { get }

func refreshPushStatus(completion: ((UNAuthorizationStatus) -> Void)?)

// Using `userInfo` as a parameter to be able to mock notification data.
func didReceiveNotification(userInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) -> Bool
}

internal class PushMonitor: PushMonitoring {

private weak var appcues: Appcues?
private let storage: DataStoring

private var pushAuthorizationStatus: UNAuthorizationStatus = .notDetermined
Expand All @@ -35,6 +39,7 @@ internal class PushMonitor: PushMonitoring {
}

init(container: DIContainer) {
self.appcues = container.owner
self.storage = container.resolve(DataStoring.self)

refreshPushStatus()
Expand Down Expand Up @@ -67,6 +72,58 @@ internal class PushMonitor: PushMonitoring {
}
}

// `completionHandler` should be called iff the function returns true.
func didReceiveNotification(userInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) -> Bool {
guard let parsedNotification = ParsedNotification(userInfo: userInfo) else {
// Not an Appcues push
return false
}

guard let appcues = appcues else {
return false
}

// If there's a user ID mismatch, don't do anything with the notification
guard parsedNotification.userID == storage.userID else {
completionHandler()
return true
}

// If no session, start one for the user in the notification
if !appcues.isActive {
storage.userID = parsedNotification.userID
storage.isAnonymous = false
}

let analyticsPublisher = appcues.container.resolve(AnalyticsPublishing.self)
analyticsPublisher.publish(TrackingUpdate(
type: .event(name: Events.Push.pushOpened.rawValue, interactive: false),
properties: [
"notification_id": parsedNotification.notificationID
],
isInternal: true
))

if #available(iOS 13.0, *) {
var actions: [AppcuesExperienceAction] = []

if let deepLinkURL = parsedNotification.deepLinkURL {
actions.append(AppcuesLinkAction(appcues: appcues, url: deepLinkURL))
}

if let experienceID = parsedNotification.experienceID {
actions.append(AppcuesLaunchExperienceAction(appcues: appcues, experienceID: experienceID, trigger: .push))
}

let actionRegistry = appcues.container.resolve(ActionRegistry.self)
actionRegistry.enqueue(actionInstances: actions, completion: completionHandler)
} else {
completionHandler()
}

return true
}

#if DEBUG
func mockPushStatus(_ status: UNAuthorizationStatus) {
pushAuthorizationStatus = status
Expand Down

0 comments on commit 2b10b53

Please sign in to comment.