Skip to content

Commit

Permalink
✨ Add support for push trigger deep links
Browse files Browse the repository at this point in the history
  • Loading branch information
mmaatttt authored and iujames committed Sep 17, 2024
1 parent 2b84042 commit 559790c
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 16 deletions.
38 changes: 38 additions & 0 deletions Sources/AppcuesKit/Data/Models/PushRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// PushRequest.swift
// AppcuesKit
//
// Created by Matt on 2024-03-18.
// Copyright © 2024 Appcues. All rights reserved.
//

import Foundation

internal struct PushRequest {
let deviceID: String
let additionalData: [String: Any]

init(deviceID: String, queryItems: [URLQueryItem] = []) {
self.deviceID = deviceID
self.additionalData = queryItems.reduce(into: [:]) { result, item in
if let value = item.value {
result[item.name] = value
}
}
}
}

extension PushRequest: Encodable {
enum CodingKeys: String, CodingKey {
case deviceID = "device_id"
}

func encode(to encoder: Encoder) throws {
var dynamicContainer = encoder.container(keyedBy: DynamicCodingKeys.self)
try dynamicContainer.encodeSkippingInvalid(additionalData)

// Encode device_id last just to ensure it wins in case the query items have the same key
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.deviceID, forKey: .deviceID)
}
}
6 changes: 6 additions & 0 deletions Sources/AppcuesKit/Data/Networking/Endpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ internal enum APIEndpoint: Endpoint {
case qualify(userID: String)
case content(experienceID: String, queryItems: [URLQueryItem] = [])
case preview(experienceID: String, queryItems: [URLQueryItem] = [])
case pushContent(id: String)
case pushPreview(id: String)
case pushTest
case health

Expand All @@ -37,6 +39,10 @@ internal enum APIEndpoint: Endpoint {
components.path = "/v1/accounts/\(config.accountID)/users/\(storage.userID)/experience_preview/\(experienceID)"
}
components.queryItems = queryItems
case let .pushContent(id):
components.path = "/v1/accounts/\(config.accountID)/push_notification/\(id)/send"
case let .pushPreview(id):
components.path = "/v1/accounts/\(config.accountID)/push_notification/\(id)/preview"
case .pushTest:
components.path = "/v1/accounts/\(config.accountID)/push_notification_test"
case .health:
Expand Down
15 changes: 1 addition & 14 deletions Sources/AppcuesKit/Presentation/Debugger/PushVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ internal class PushVerifier {
}

private func verifyServerComponents(token: String) {
let body = PushTest(
let body = PushRequest(
deviceID: storage.deviceID
)

Expand Down Expand Up @@ -274,19 +274,6 @@ internal class PushVerifier {

@available(iOS 13.0, *)
private extension PushVerifier {
struct PushTest: Encodable {
let deviceID: String

enum CodingKeys: String, CodingKey {
case deviceID = "device_id"
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.deviceID, forKey: .deviceID)
}
}

struct PushTestResponse: Decodable {
let ok: Bool
}
Expand Down
30 changes: 28 additions & 2 deletions Sources/AppcuesKit/Presentation/DeepLinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ internal protocol DeepLinkHandling: AnyObject {
internal class DeepLinkHandler: DeepLinkHandling {

enum Action: Hashable {
case preview(experienceID: String, queryItems: [URLQueryItem]) // preview for draft content
case show(experienceID: String, queryItems: [URLQueryItem]) // published content
/// preview for draft content
case preview(experienceID: String, queryItems: [URLQueryItem])
/// published content
case show(experienceID: String, queryItems: [URLQueryItem])
/// preview for draft push content
case pushPreview(id: String, queryItems: [URLQueryItem])
/// published push content
case pushContent(id: String)
case debugger(destination: DebugDestination?)
case verifyInstall(id: String)
case captureScreen(token: String)
Expand All @@ -30,6 +36,8 @@ internal class DeepLinkHandler: DeepLinkHandling {
// supported paths:
// appcues-{app_id}://sdk/experience_preview/{experience_id}?locale_id={localeID}
// appcues-{app_id}://sdk/experience_content/{experience_id}
// appcues-{app_id}://sdk/push_preview/{id}?<query_params>
// appcues-{app_id}://sdk/push_content/{id}
// appcues-{app_id}://sdk/debugger
// appcues-{app_id}://sdk/debugger/fonts
// appcues-{app_id}://sdk/verify/{token}
Expand All @@ -42,6 +50,10 @@ internal class DeepLinkHandler: DeepLinkHandling {
} else if pathTokens.count == 2, pathTokens[0] == "experience_content", isSessionActive {
// can only show content via deep link when a session is active
self = .show(experienceID: pathTokens[1], queryItems: url.queryItems)
} else if pathTokens.count == 2, pathTokens[0] == "push_preview" {
self = .pushPreview(id: pathTokens[1], queryItems: url.queryItems)
} else if pathTokens.count == 2, pathTokens[0] == "push_content" {
self = .pushContent(id: pathTokens[1])
} else if pathTokens.count >= 1, pathTokens[0] == "debugger" {
self = .debugger(destination: DebugDestination(pathToken: pathTokens[safe: 1]))
} else if pathTokens.count == 2, pathTokens[0] == "verify" {
Expand Down Expand Up @@ -124,6 +136,20 @@ internal class DeepLinkHandler: DeepLinkHandling {
trigger: .deepLink,
completion: nil
)
case let .pushPreview(id, queryItems):
container?.resolve(ExperienceLoading.self).loadPush(
id: id,
published: false,
queryItems: queryItems,
completion: nil
)
case let .pushContent(id):
container?.resolve(ExperienceLoading.self).loadPush(
id: id,
published: true,
queryItems: [],
completion: nil
)
case .debugger(let destination):
container?.resolve(UIDebugging.self).show(mode: .debugger(destination))
case .verifyInstall(let token):
Expand Down
39 changes: 39 additions & 0 deletions Sources/AppcuesKit/Presentation/ExperienceLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ internal protocol ExperienceLoading: AnyObject {
trigger: ExperienceTrigger,
completion: ((Result<Void, Error>) -> Void)?
)

func loadPush(
id: String,
published: Bool,
queryItems: [URLQueryItem],
completion: ((Result<Void, Error>) -> Void)?
)
}

@available(iOS 13.0, *)
Expand Down Expand Up @@ -74,6 +81,38 @@ internal class ExperienceLoader: ExperienceLoading {
}
}

func loadPush(
id: String,
published: Bool,
queryItems: [URLQueryItem],
completion: ((Result<Void, Error>) -> Void)?
) {
let endpoint = published ?
APIEndpoint.pushContent(id: id) :
APIEndpoint.pushPreview(id: id)

let body = PushRequest(
deviceID: storage.deviceID,
queryItems: queryItems
)

let data = try? NetworkClient.encoder.encode(body)

networking.post(
to: endpoint,
authorization: Authorization(bearerToken: storage.userSignature),
body: data
) { [weak self] (result: Result<Void, Error>) in
switch result {
case .success(let experience):
break
case .failure(let error):
self?.config.logger.error("Loading push %{public}@ failed with error %{public}@", id, "\(error)")
completion?(.failure(error))
}
}
}

@objc
private func refreshPreview(notification: Notification) {
guard let experienceID = lastPreviewExperienceID else { return }
Expand Down

0 comments on commit 559790c

Please sign in to comment.