Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rich push deep links #45

Merged
merged 28 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8675931
add notification service extension
levibostian Sep 13, 2021
37ad320
add build configuration flags to still compile swift
levibostian Sep 14, 2021
f19e3b0
add rich push deep links to push
levibostian Sep 14, 2021
535f3ee
make lint happy
levibostian Sep 14, 2021
f120306
fix: convert APN device token to string
levibostian Sep 15, 2021
25d512c
Merge branch 'fix-apn-token' into rich-push-links
levibostian Sep 15, 2021
f969b26
format
levibostian Sep 15, 2021
d18155c
fix: remove apn device token profile request body
levibostian Sep 15, 2021
3246a5e
Merge branch 'fix-apn-delete' into rich-push-links
levibostian Sep 15, 2021
dc6f5bf
Merge branch 'alpha' into rich-push-links
levibostian Sep 15, 2021
8b8e36c
fix push taking while to show
levibostian Sep 16, 2021
8467cba
fix deep link saving to push
levibostian Sep 16, 2021
73fc45c
add logging for debugging
levibostian Sep 16, 2021
8a63717
try to give modified notification content to completion handler
levibostian Sep 20, 2021
10ca04e
fix setting link
levibostian Sep 20, 2021
8f2a599
refactor to remove similar classes
levibostian Sep 20, 2021
f0b20f4
fix tests
levibostian Sep 21, 2021
c6ebebd
increase min ios version to 13
levibostian Sep 21, 2021
8f627bc
add note request finish [skip ci]
levibostian Sep 21, 2021
f148aee
Merge branch 'alpha' into rich-push-links
levibostian Sep 21, 2021
633f68c
make lint happy
levibostian Sep 21, 2021
b443ca4
fix tests
levibostian Sep 21, 2021
64e34c1
code review edits
levibostian Sep 22, 2021
bfc6d85
refactor parsing JSON push payload
levibostian Sep 23, 2021
ead3d90
fix tests
levibostian Sep 23, 2021
42d3687
fix tests
levibostian Sep 23, 2021
6229677
format
levibostian Sep 23, 2021
c5322c0
fix tests linux
levibostian Sep 23, 2021
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import PackageDescription
let package = Package(
name: "Customer.io",
platforms: [
.iOS(.v9)
.iOS(.v13)
],
products: [ // externally visible products for clients to install.
// library name is the name given when installing the SDK.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[s-cioerrorparse]: https://github.com/customerio/RemoteHabits-iOS/blob/1.0.0/Remote%20Habits/Util/CustomerIOErrorUtil.swift

![min swift version is 5.3](https://img.shields.io/badge/min%20Swift%20version-5.3-orange)
![min ios version is 9](https://img.shields.io/badge/min%20iOS%20version-9-blue)
![min ios version is 13](https://img.shields.io/badge/min%20iOS%20version-13-blue)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md)

# Customer.io iOS SDK
Expand Down
42 changes: 42 additions & 0 deletions Sources/MessagingPush/MessagingPush+NotificationCenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
#if canImport(UserNotifications) && canImport(UIKit)
import CioTracking
import UIKit
import UserNotifications

/**
For rich push handling. Methods to call when a rich push UI is interacted with.
*/
public extension MessagingPush {
/**
A push notification was interacted with.

- returns: If the SDK called the completion handler for you indicating if the SDK took care of the request or not.
*/
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) -> Bool {
guard let pushContent = PushContent.parse(notificationContent: response.notification.request.content,
jsonAdapter: DITracking.shared.jsonAdapter)
else {
return false
}

switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier: // push notification was touched.
if let deepLinkurl = pushContent.deepLink {
UIApplication.shared.open(url: deepLinkurl)

levibostian marked this conversation as resolved.
Show resolved Hide resolved
completionHandler()

return true
}
default: break
}

return false
}
}
#endif
92 changes: 92 additions & 0 deletions Sources/MessagingPush/PushContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import CioTracking
import Foundation
#if canImport(UserNotifications)
import UserNotifications

/**
The content of a push notification. Single source of truth for getting properties of a push notification.
*/
public class PushContent {
public var title: String {
get {
mutableNotificationContent.title
}
set {
mutableNotificationContent.title = newValue
}
}

public var body: String {
get {
mutableNotificationContent.body
}
set {
mutableNotificationContent.body = newValue
}
}

public var deepLink: URL? {
get {
cio.push.link?.url
}
set {
cioPush = cioPush.linkSet(newValue?.absoluteString)
}
}

private var cio: CioPushPayload {
get {
// Disable swiftlint rule because this class can't initialize without this being valid +
// setter wont set unless a valid Object is created.
// swiftlint:disable:next force_cast
jsonAdapter.fromDictionary(mutableNotificationContent.userInfo["CIO"] as! [AnyHashable: Any])!
}
set {
// assert we have a valid payload before we set it as the new value
guard let newUserInfo = jsonAdapter.toDictionary(newValue) else {
return
}
mutableNotificationContent.userInfo["CIO"] = newUserInfo
}
}

private var cioPush: CioPushPayload.Push {
get {
cio.push
}
set {
cio = CioPushPayload(push: newValue)
}
}

private let jsonAdapter: JsonAdapter
public let mutableNotificationContent: UNMutableNotificationContent

public static func parse(notificationContent: UNNotificationContent, jsonAdapter: JsonAdapter) -> PushContent? {
let raw = notificationContent.userInfo

guard let cioUserInfo = raw["CIO"] as? [AnyHashable: Any],
let _: CioPushPayload = jsonAdapter.fromDictionary(cioUserInfo),
levibostian marked this conversation as resolved.
Show resolved Hide resolved
let mutableNotificationContent = notificationContent.mutableCopy() as? UNMutableNotificationContent
else {
return nil
}

return PushContent(mutableNotificationContent: mutableNotificationContent, jsonAdapter: jsonAdapter)
}

// Used when modifying push content before showing and for parsing after displaying.
private init(mutableNotificationContent: UNMutableNotificationContent, jsonAdapter: JsonAdapter) {
self.mutableNotificationContent = mutableNotificationContent
self.jsonAdapter = jsonAdapter
}
}
#endif

struct CioPushPayload: Codable {
let push: Push

struct Push: Codable, AutoLenses {
let link: String?
}
}
12 changes: 12 additions & 0 deletions Sources/MessagingPush/autogenerated/AutoLenses.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ func |> <A, B>(x: A, f: (A) -> B) -> B {
func |> <A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
{ g(f($0)) }
}

extension CioPushPayload.Push {
static let linkLens = Lens<CioPushPayload.Push, String?>(get: { $0.link },
set: { link, existing in
CioPushPayload.Push(link: link)
})

// Convenient set functions to edit a property of the immutable object
func linkSet(_ link: String?) -> CioPushPayload.Push {
CioPushPayload.Push(link: link)
}
}
11 changes: 11 additions & 0 deletions Sources/MessagingPush/autogenerated/Autogenerated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/**
View `AutoMockable.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoMockable {}

/**
View `AutoLenses.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoLenses {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import CioMessagingPush
import CioTracking
import Foundation
#if canImport(UserNotifications)
import UserNotifications

/**
Functions called in app's Notification Service target.
*/
public extension MessagingPush {
/**
- returns:
Bool indicating if this push notification is one handled by Customer.io SDK or not.
If function returns `false`, `contentHandler` will *not* be called by the SDK.
*/
@discardableResult
func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) -> Bool {
guard let pushContent = PushContent.parse(notificationContent: request.content,
jsonAdapter: DITracking.shared.jsonAdapter)
else {
// push is not sent by CIO. Therefore, end early.
return false
}

RichPushRequestHandler.shared.startRequest(request, content: pushContent, completionHandler: contentHandler)

return true
}

/**
iOS OS telling the notification service to hurry up and stop modifying the push notifications.
Stop all network requests and modifying and show the push for what it looks like now.
*/
func serviceExtensionTimeWillExpire() {
RichPushRequestHandler.shared.stopAll()
}
}
#endif
31 changes: 31 additions & 0 deletions Sources/MessagingPushAPN/RichPush/RichPushRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import CioMessagingPush
import Foundation
#if canImport(UserNotifications)
import UserNotifications

internal class RichPushRequest {
private let completionHandler: (UNNotificationContent) -> Void
private let pushContent: PushContent

init(
pushContent: PushContent,
request: UNNotificationRequest,
completionHandler: @escaping (UNNotificationContent) -> Void
) {
self.completionHandler = completionHandler
self.pushContent = pushContent
}

func start() {
// no async operations or modifications to the notification to do. Therefore, let's just finish.

finishImmediately()
}

func finishImmediately() {
// XXX: stop async operations and finish the rich push request.

completionHandler(pushContent.mutableNotificationContent)
}
}
#endif
38 changes: 38 additions & 0 deletions Sources/MessagingPushAPN/RichPush/RichPushRequestHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import CioMessagingPush
import CioTracking
import Foundation
#if canImport(UserNotifications)
import UserNotifications

internal class RichPushRequestHandler {
static let shared = RichPushRequestHandler()

@Atomic private var requests: [String: RichPushRequest] = [:]

private init() {}

func startRequest(
_ request: UNNotificationRequest,
content: PushContent,
completionHandler: @escaping (UNNotificationContent) -> Void
) {
let requestId = request.identifier

let existingRequest = requests[requestId]
if existingRequest != nil { return }

let newRequest = RichPushRequest(pushContent: content, request: request, completionHandler: completionHandler)
requests[requestId] = newRequest

newRequest.start()
}

func stopAll() {
requests.forEach {
$0.value.finishImmediately()
}

requests = [:]
}
}
#endif
11 changes: 11 additions & 0 deletions Sources/MessagingPushAPN/autogenerated/Autogenerated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/**
View `AutoMockable.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoMockable {}

/**
View `AutoLenses.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoLenses {}
11 changes: 11 additions & 0 deletions Sources/MessagingPushFCM/autogenerated/Autogenerated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/**
View `AutoMockable.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoMockable {}

/**
View `AutoLenses.generated.swift` file to learn how to use this protocol.
*/
public protocol AutoLenses {}
4 changes: 4 additions & 0 deletions Sources/Tracking/Extensions/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public extension String {
data(using: .utf8)
}

var url: URL? {
URL(string: self)
}

static var abcLetters: String {
"abcdefghijklmnopqrstuvwxyz"
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/Tracking/Extensions/UIApplicationExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
#if canImport(UIKit)
import UIKit

public extension UIApplication {
func open(url: URL) {
open(url, options: [:]) { _ in }
}
}
#endif
1 change: 0 additions & 1 deletion Sources/Tracking/Repository/Event.swift

This file was deleted.

29 changes: 28 additions & 1 deletion Sources/Tracking/Util/JsonAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ public class JsonAdapter {
self.log = log
}

public func fromDictionary<T: Decodable>(_ dictionary: [AnyHashable: Any],
decoder override: JSONDecoder? = nil) -> T? {
do {
let jsonData = try JSONSerialization.data(withJSONObject: dictionary)

return fromJson(jsonData, decoder: decoder)
} catch {
log.error("\(error.localizedDescription), dictionary: \(dictionary)")
}

return nil
}

public func toDictionary<T: Encodable>(_ obj: T, encoder override: JSONEncoder? = nil) -> [AnyHashable: Any]? {
guard let data = toJson(obj, encoder: encoder) else {
return nil
}

do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any]
} catch {
log.error("\(error.localizedDescription), object: \(obj)")
}

return nil
}

/**
Returns optional to be more convenient then try/catch all over the code base.

Expand Down Expand Up @@ -87,7 +114,7 @@ public class JsonAdapter {
"""
} catch {
errorStringToLog = """
Generic decide error. \(error.localizedDescription), json: \(json.string ?? "(error getting json string)")
Generic decode error. \(error.localizedDescription), json: \(json.string ?? "(error getting json string)")
"""
}

Expand Down
Empty file.
Loading