Skip to content
Merged
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
15 changes: 15 additions & 0 deletions Sources/Helpers/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ internal extension Knock {

return jsonString
}

static func convertTokenToString(token: Data) -> String {
let tokenParts = token.map { data -> String in
return String(format: "%02.2hhx", data)
}
return tokenParts.joined()
}

func performActionWithUserId<T>(_ action: @escaping (String, @escaping (Result<T, Error>) -> Void) -> Void, completionHandler: @escaping (Result<T, Error>) -> Void) {
guard let userId = self.userId else {
completionHandler(.failure(KnockError.userIdError))
return
}
action(userId, completionHandler)
}
}

struct DynamicCodingKey: CodingKey {
Expand Down
68 changes: 47 additions & 21 deletions Sources/Knock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,63 @@

import SwiftUI

// Knock client SDK.
public class Knock {
public let publishableKey: String
public let userId: String
public let userToken: String?
internal static let clientVersion = "1.0.0"
internal static let loggingSubsytem = "knock-swift"

internal let api: KnockAPI

public var feedManager: FeedManager?

public enum KnockError: Error {
case runtimeError(String)
}

// MARK: Constructor
internal var api: KnockAPI

public internal(set) var feedManager: FeedManager?
public internal(set) var userId: String?
public internal(set) var pushChannelId: String?
public internal(set) var userDeviceToken: String?

/**
Returns a new instance of the Knock Client

- Parameters:
- publishableKey: your public API key
- userId: the user-id that will be used in the subsequent method calls
- userToken: [optional] user token. Used in production when enhanced security is enabled
- hostname: [optional] custom hostname of the API, including schema (https://)
- options: [optional] Options for customizing the Knock instance.
*/
public init(publishableKey: String, userId: String, userToken: String? = nil, hostname: String? = nil) throws {
guard publishableKey.hasPrefix("sk_") == false else { throw KnockError.runtimeError("[Knock] You are using your secret API key on the client. Please use the public key.") }

self.publishableKey = publishableKey
self.userId = userId
self.userToken = userToken
public init(publishableKey: String, options: KnockOptions? = nil) {
self.api = KnockAPI(publishableKey: publishableKey, hostname: options?.host)
}

internal func resetInstance() {
self.userId = nil
self.feedManager = nil
self.userDeviceToken = nil
self.pushChannelId = nil
self.api.userToken = nil
}
}

public extension Knock {
// Configuration options for the Knock client SDK.
struct KnockOptions {
var host: String?

self.api = KnockAPI(publishableKey: publishableKey, userToken: userToken, hostname: hostname)
public init(host: String? = nil) {
self.host = host
}
}

enum KnockError: Error {
case runtimeError(String)
case userIdError
}
}

extension Knock.KnockError: LocalizedError {
public var errorDescription: String? {
switch self {
case .runtimeError(let message):
return message
case .userIdError:
return "UserId not found. Please authenticate your userId with Knock.authenticate()."
}
}
}
34 changes: 16 additions & 18 deletions Sources/KnockAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,48 @@
import Foundation

class KnockAPI {
private let publishableKey: String
private let userToken: String?
public var hostname = "https://api.knock.app"
internal let publishableKey: String
internal private(set) var host = "https://api.knock.app"
public internal(set) var userToken: String?

private var apiBasePath: String {
"\(hostname)/v1"
"\(host)/v1"
}

static let clientVersion = "0.2.0"

init(publishableKey: String, userToken: String? = nil, hostname: String? = nil) {
self.publishableKey = publishableKey
self.userToken = userToken


internal init(publishableKey: String, hostname: String? = nil) {
if let customHostname = hostname {
self.hostname = customHostname
self.host = customHostname
}
self.publishableKey = publishableKey
}

// MARK: Decode functions, they encapsulate making the request and decoding the data

func decodeFromGet<T:Codable>(_ type: T.Type, path: String, queryItems: [URLQueryItem]?, then handler: @escaping (Result<T, Error>) -> Void) {
internal func decodeFromGet<T:Codable>(_ type: T.Type, path: String, queryItems: [URLQueryItem]?, then handler: @escaping (Result<T, Error>) -> Void) {
get(path: path, queryItems: queryItems) { (result) in
self.decodeData(result, handler: handler)
}
}

func decodeFromPost<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
internal func decodeFromPost<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
post(path: path, body: body) { (result) in
self.decodeData(result, handler: handler)
}
}

func decodeFromPut<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
internal func decodeFromPut<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
put(path: path, body: body) { (result) in
self.decodeData(result, handler: handler)
}
}

func decodeFromDelete<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
internal func decodeFromDelete<T:Codable>(_ type: T.Type, path: String, body: Encodable?, then handler: @escaping (Result<T, Error>) -> Void) {
delete(path: path, body: body) { (result) in
self.decodeData(result, handler: handler)
}
}

private func decodeData<T:Codable>(_ result: Result<Data, Error>, handler: @escaping (Result<T, Error>) -> Void) {
internal func decodeData<T:Codable>(_ result: Result<Data, Error>, handler: @escaping (Result<T, Error>) -> Void) {
switch result {
case .success(let data):
let decoder = JSONDecoder()
Expand Down Expand Up @@ -123,6 +120,7 @@ class KnockAPI {
- then: the code to execute when the response is received
*/
private func makeGeneralRequest(method: String, path: String, queryItems: [URLQueryItem]?, body: Encodable?, then handler: @escaping (Result<Data, Error>) -> Void) {

let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard var URL = URL(string: "\(apiBasePath)\(path)") else {return}
Expand All @@ -144,7 +142,7 @@ class KnockAPI {

// Headers

request.addValue("knock-swift@\(KnockAPI.clientVersion)", forHTTPHeaderField: "User-Agent")
request.addValue("knock-swift@\(Knock.clientVersion)", forHTTPHeaderField: "User-Agent")

request.addValue("Bearer \(publishableKey)", forHTTPHeaderField: "Authorization")
if let userToken = userToken {
Expand Down
90 changes: 90 additions & 0 deletions Sources/KnockAppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// KnockAppDelegate.swift
//
//
// Created by Matt Gardner on 1/22/24.
//

import Foundation
import UIKit
import OSLog

@available(iOSApplicationExtension, unavailable)
open class KnockAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

private let logger = Logger(subsystem: "knock-swift", category: "KnockAppDelegate")

// MARK: Init

override init() {
super.init()

// Register to ensure device token can be fetched
UIApplication.shared.registerForRemoteNotifications()
UNUserNotificationCenter.current().delegate = self

}

// MARK: Launching

open func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return true
}

// MARK: Notifications

open func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
logger.debug("userNotificationCenter willPresent notification: \(notification)")

let userInfo = notification.request.content.userInfo

let presentationOptions = pushNotificationDeliveredInForeground(userInfo: userInfo)
completionHandler(presentationOptions)
}

open func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
logger.debug("didReceiveNotificationResponse: \(response)")

let userInfo = response.notification.request.content.userInfo

if response.actionIdentifier == UNNotificationDismissActionIdentifier {
pushNotificationDismissed(userInfo: userInfo)
} else {
pushNotificationTapped(userInfo: userInfo)
}
completionHandler()
}

// MARK: Token Management

open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
logger.error("Failed to register for notifications: \(error.localizedDescription)")
}

open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
logger.debug("Successfully registered for notifications!")

// 1. Convert device token to string
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
// 2. Print device token to use for PNs payloads
logger.debug("Device Token: \(token)")

let defaults = UserDefaults.standard
defaults.set(token, forKey: "device_push_token")
// deviceTokenDidChange(apnsToken: token, isDebugging: isDebuggerAttached)
// self.pushToken = token
}

// MARK: Functions

open func deviceTokenDidChange(apnsToken: String, isDebugging: Bool) {}

open func pushNotificationDeliveredInForeground(userInfo: [AnyHashable : Any]) -> UNNotificationPresentationOptions { return [] }

open func pushNotificationTapped(userInfo: [AnyHashable : Any]) {}

open func pushNotificationDismissed(userInfo: [AnyHashable : Any]) {}
}
Loading