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

Notification Tracking #27

Merged
merged 18 commits into from
Feb 25, 2021
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
2 changes: 1 addition & 1 deletion AEPTarget.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ Pod::Spec.new do |s|

s.source_files = 'AEPTarget/Sources/**/*.swift'

end
end
308 changes: 273 additions & 35 deletions AEPTarget.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion AEPTarget/Sources/AEPTarget.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 Adobe. All rights reserved.
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Expand Down
216 changes: 216 additions & 0 deletions AEPTarget/Sources/DeliveryRequestBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename DeliveryRequestBuilder -> TargetDeliveryRequestBuilder

Copyright 2021 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import AEPIdentity
import AEPServices
import Foundation

enum DeliveryRequestBuilder {
private static var systemInfoService: SystemInfoService {
ServiceProvider.shared.systemInfoService
}

/// Builds the `DeliveryRequest` object
/// - Parameters:
/// - tntId: an UUID generated by the TNT server
/// - thirdPartyId: a string pointer containing the value of the third party id (custom visitor id)
/// - identitySharedState: the shared state of `Identity` extension
/// - lifecycleSharedState: the shared state of `Lifecycle` extension
/// - targetPrefetchArray: an array of ACPTargetPrefetch objects representing the desired mboxes to prefetch
/// - targetParameters: a TargetParameters object containing parameters for all the mboxes in the request array
/// - notifications: viewed mboxes that we cached
/// - environmentId: target environmentId
/// - propertyToken: String to be passed for all requests
/// - Returns: a `DeliveryRequest` object
static func build(tntId: String?, thirdPartyId: String?, identitySharedState: [String: Any]?, lifecycleSharedState: [String: Any]?, targetPrefetchArray: [TargetPrefetch]? = nil, targetParameters: TargetParameters? = nil, notifications: [Notification]? = nil, environmentId _: Int64 = 0, propertyToken _: String? = nil) -> TargetDeliveryRequest? {
let targetIDs = generateTargetIDsBy(tntid: tntId, thirdPartyId: thirdPartyId, identitySharedState: identitySharedState)
let experienceCloud = generateExperienceCloudInfoBy(identitySharedState: identitySharedState)
guard let context = generateTargetContext() else {
return nil
}

// prefetch
var prefetch: Prefetch?
if let tpArray: [TargetPrefetch] = targetPrefetchArray {
prefetch = generatePrefetchBy(targetPrefetchArray: tpArray, lifecycleSharedState: lifecycleSharedState, globalParameters: targetParameters) ?? nil
}

// TODO: removeATPropertyFromParameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might still need to do this, refer TGT-35089 & TGT-34666. I'll confirm this one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ravjain-adb Js checked with David, we need not provide this workaround anymore since we expose the setting via Launch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this TODO 👆

// TODO: Add property token

return TargetDeliveryRequest(id: targetIDs, context: context, experienceCloud: experienceCloud, prefetch: prefetch, notifications: notifications)
}

/// Creates the display notification object
/// - Parameters:
/// - mboxName: name of the mbox
/// - cachedMboxJson: the cached mbox
/// - parameters: TargetParameters object associated with the notification
/// - timestamp: timestamp associated with the event
/// - lifecycleContextData: payload for notification
/// - Returns: Notification object
static func getDisplayNotification(mboxName: String, cachedMboxJson: [String: Any]?, parameters: TargetParameters?, timestamp: Int64, lifecycleContextData: [String: String]?) -> Notification? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: rename parameters->targetParameters. parameters is mostly used for mbox parameters.

let id = UUID().uuidString

// Set parameters: getMboxParameters
let mBoxParameters: [String: String] = getMboxParameters(mboxParameters: parameters?.parameters, lifecycleContextData: lifecycleContextData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let mboxParameters = ...


// Set mBox
guard let mboxState = cachedMboxJson?[TargetConstants.TargetJson.Mbox.STATE] as? String, !mboxState.isEmpty else {
Log.debug(label: TargetDeliveryRequest.LOG_TAG, "Unable to get display notification, mbox state is invalid")
return nil
}
let mBox = Mbox(name: mboxName, state: mboxState)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: mBox->mbox


// set token
var tokens: [String] = []
if let optionsArray = cachedMboxJson?[TargetConstants.TargetJson.OPTIONS] as? [[String: Any?]?] {
for option in optionsArray {
guard let optionEventToken: String = option?[TargetConstants.TargetJson.Metric.EVENT_TOKEN] as? String else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guard let optionEventToken = ...

continue
}
tokens.append(optionEventToken)
}
}

if tokens.isEmpty {
Log.debug(label: TargetDeliveryRequest.LOG_TAG, TargetError.ERROR_DISPLAY_NOTIFICATION_TOKEN_EMPTY)
return nil
}

let notification = Notification(id: id, timestamp: timestamp, type: TargetConstants.TargetJson.MetricType.DISPLAY, mbox: mBox, tokens: tokens, parameters: mBoxParameters, profileParameters: parameters?.profileParameters, order: parameters?.order?.toInternalOrder(), product: parameters?.product?.toInternalProduct())

return notification
}

static func getClickedNotification(cachedMboxJson: [String: Any?], parameters: TargetParameters?, timestamp: Int64, lifecycleContextData: [String: String]?) -> Notification? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: rename parameters->targetParameters

let id = UUID().uuidString

// Set parameters: getMboxParameters
let mBoxparameters: [String: String] = getMboxParameters(mboxParameters: parameters?.parameters, lifecycleContextData: lifecycleContextData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let mboxparameters = ...


let mboxName = cachedMboxJson[TargetConstants.TargetJson.Mbox.NAME] as? String ?? ""

let mBox = Mbox(name: mboxName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mBox->mbox


guard let metrics = cachedMboxJson[TargetConstants.TargetJson.METRICS] as? [Any?] else {
return Notification(id: id, timestamp: timestamp, type: TargetConstants.TargetJson.MetricType.CLICK, mbox: mBox, parameters: mBoxparameters, profileParameters: parameters?.profileParameters, order: parameters?.order?.toInternalOrder(), product: parameters?.product?.toInternalProduct())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm we send click notifications even without tokens?

}

// set token
var tokens: [String] = []
for metricItem in metrics {
guard let metric = metricItem as? [String: Any?], TargetConstants.TargetJson.MetricType.CLICK == metric[TargetConstants.TargetJson.Metric.TYPE] as? String, let token = metric[TargetConstants.TargetJson.Metric.EVENT_TOKEN] as? String, !token.isEmpty else {
continue
}

tokens.append(token)
}

if tokens.isEmpty {
Log.warning(label: Target.LOG_TAG, "\(TargetError.ERROR_CLICK_NOTIFICATION_CREATE_FAILED) \(cachedMboxJson.description)")
return nil
}

return Notification(id: id, timestamp: timestamp, type: TargetConstants.TargetJson.MetricType.CLICK, mbox: mBox, tokens: tokens, parameters: mBoxparameters, profileParameters: parameters?.profileParameters, order: parameters?.order?.toInternalOrder(), product: parameters?.product?.toInternalProduct())
}

/// Creates the mbox parameters with the provided lifecycle data.
/// - Parameters:
/// - mboxParameters: the mbox parameters provided by the user
/// - lifecycleContextData: Lifecycle context data
/// - Returns: a dictionary [String: String]
private static func getMboxParameters(mboxParameters: [String: String]?, lifecycleContextData: [String: Any]?) -> [String: String] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we streamline API names here
getTargetIDs
getExperienceCloudInfo
getTargetContext etc

var mboxParametersCopy: [String: String] = mboxParameters ?? [:]

let l = lifecycleContextData as? [String: String]
mboxParametersCopy = merge(newDictionary: l, to: mboxParametersCopy) ?? [:]

return mboxParametersCopy
}

private static func generateTargetIDsBy(tntid: String?, thirdPartyId: String?, identitySharedState: [String: Any]?) -> TargetIDs? {
let customerIds = identitySharedState?[TargetConstants.Identity.SharedState.Keys.VISITOR_IDS_LIST] as? [CustomIdentity]
return TargetIDs(tntId: tntid, thirdPartyId: thirdPartyId, marketingCloudVisitorId: identitySharedState?[TargetConstants.Identity.SharedState.Keys.VISITOR_ID_MID] as? String, customerIds: CustomerID.from(customIdentities: customerIds))
}

private static func generateExperienceCloudInfoBy(identitySharedState: [String: Any]?) -> ExperienceCloudInfo {
let analytics = AnalyticsInfo(logging: .client_side)
if let identitySharedState = identitySharedState {
let audienceManager = AudienceManagerInfo(blob: identitySharedState[TargetConstants.Identity.SharedState.Keys.VISITOR_ID_BLOB] as? String, locationHint: identitySharedState[TargetConstants.Identity.SharedState.Keys.VISITOR_ID_LOCATION_HINT] as? String)
return ExperienceCloudInfo(audienceManager: audienceManager, analytics: analytics)
}

return ExperienceCloudInfo(audienceManager: nil, analytics: analytics)
}

private static func generateTargetContext() -> TargetContext? {
let deviceType: DeviceType = systemInfoService.getDeviceType() == AEPServices.DeviceType.PHONE ? .phone : .tablet
let mobilePlatform = MobilePlatform(deviceName: systemInfoService.getDeviceName(), deviceType: deviceType, platformType: .ios)
let application = AppInfo(id: systemInfoService.getApplicationBundleId(), name: systemInfoService.getApplicationName(), version: systemInfoService.getApplicationVersion())
let orientation: DeviceOrientation = systemInfoService.getCurrentOrientation() == AEPServices.DeviceOrientation.LANDSCAPE ? .landscape : .portrait
let screen = Screen(colorDepth: TargetConstants.TargetRequestValue.COLOR_DEPTH_32, width: systemInfoService.getDisplayInformation().width, height: systemInfoService.getDisplayInformation().height, orientation: orientation)
return TargetContext(channel: TargetConstants.TargetRequestValue.CHANNEL_MOBILE, userAgent: systemInfoService.getDefaultUserAgent(), mobilePlatform: mobilePlatform, application: application, screen: screen, timeOffsetInMinutes: Date().getUnixTimeInSeconds())
}

private static func generatePrefetchBy(targetPrefetchArray: [TargetPrefetch], lifecycleSharedState: [String: Any]?, globalParameters: TargetParameters?) -> Prefetch? {
let lifecycleDataDict = lifecycleSharedState as? [String: String]

var mboxes = [Mbox]()

for (index, prefetch) in targetPrefetchArray.enumerated() {
let parameterWithLifecycleData = merge(newDictionary: lifecycleDataDict, to: prefetch.targetParameters?.parameters)
let parameters = merge(newDictionary: globalParameters?.parameters, to: parameterWithLifecycleData)
let profileParameters = merge(newDictionary: globalParameters?.profileParameters, to: prefetch.targetParameters?.profileParameters)
let order = findFirstAvailableOrder(globalOrder: globalParameters?.order, order: prefetch.targetParameters?.order)
let product = findFirstAvailableProduct(product: prefetch.targetParameters?.product, globalProduct: globalParameters?.product)
let mbox = Mbox(name: prefetch.name, index: index, parameters: parameters, profileParameters: profileParameters, order: order, product: product)
mboxes.append(mbox)
}
return Prefetch(mboxes: mboxes)
}

/// Merges the given dictionaries, and only keeps values from the new dictionary for duplicated keys.
/// - Parameters:
/// - newDictionary: the new dictionary
/// - dictionary: the original dictionary
/// - Returns: a new dictionary with combined key-value pairs
private static func merge(newDictionary: [String: String]?, to dictionary: [String: String]?) -> [String: String]? {
guard let newDictionary = newDictionary else {
return dictionary
}
guard let dictionary = dictionary else {
return newDictionary
}
return dictionary.merging(newDictionary) { _, new in new }
}

private static func findFirstAvailableOrder(globalOrder: TargetOrder?, order: TargetOrder?) -> Order? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API name is not apt as this does not find the first available Order rather there is a preference for global > local. We can rename this to getOrderByPreference or simply getOrder

if let globalOrder = globalOrder {
return globalOrder.toInternalOrder()
}
if let order = order {
return order.toInternalOrder()
}
return nil
}

private static func findFirstAvailableProduct(product: TargetProduct?, globalProduct: TargetProduct?) -> Product? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename - getProduct

if let product = product {
return product.toInternalProduct()
}
if let globalProduct = globalProduct {
return globalProduct.toInternalProduct()
}
return nil
}
}
2 changes: 1 addition & 1 deletion AEPTarget/Sources/DeliveryResponse.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename DeliveryResponse -> TargetDeliveryResponse

Copyright 2020 Adobe. All rights reserved.
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Expand Down
12 changes: 11 additions & 1 deletion AEPTarget/Sources/Event+Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ extension Event {

/// Reads the `TargetParameters` from the event data
var targetParameters: TargetParameters? {
TargetParameters.from(dictionary: data?[TargetConstants.EventDataKeys.TARGET_PARAMETERS] as? [String: Any])
return TargetParameters.from(dictionary: data?[TargetConstants.EventDataKeys.TARGET_PARAMETERS] as? [String: Any])
}

/// Returns true if this event is a prefetch request event
var isPrefetchEvent: Bool {
return data?[TargetConstants.EventDataKeys.PREFETCH_REQUESTS] != nil ? true : false
}

/// Returns true if the event is location display request event
var isLocationsDisplayedEvent: Bool {
return data?[TargetConstants.EventDataKeys.IS_LOCATION_DISPLAYED] as? Bool ?? false
}

/// Returns true if the event is location clicked request event
var isLocationClickedEvent: Bool {
return data?[TargetConstants.EventDataKeys.IS_LOCATION_CLICKED] as? Bool ?? false
}
}
36 changes: 25 additions & 11 deletions AEPTarget/Sources/Target+PublicAPI.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 Adobe. All rights reserved.
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Expand Down Expand Up @@ -31,7 +31,7 @@ import Foundation
let completion = completion ?? { _ in }

guard !prefetchObjectArray.isEmpty else {
Log.error(label: Target.LOG_TAG, "Failed to prefetch Target request (the provided request list for mboxes is empty or null)")
Log.error(label: Target.LOG_TAG, "Failed to prefetch Target request (the provided request list for mboxes is empty or nil)")
completion(TargetError(message: TargetError.ERROR_EMPTY_PREFETCH_LIST))
return
}
Expand Down Expand Up @@ -149,10 +149,15 @@ import Foundation
/// - Parameters:
/// - mboxNames: (required) an array of displayed location names
/// - targetParameters: for the displayed location
static func locationsDisplayed(mboxNames: [String], targetParameters: TargetParameters) {
// TODO: need to verify input parameters
// TODO: need to convert "targetParameters" to [String:Any] array
let eventData = [TargetConstants.EventDataKeys.MBOX_NAMES: mboxNames, TargetConstants.EventDataKeys.IS_LOCATION_DISPLAYED: true, TargetConstants.EventDataKeys.TARGET_PARAMETERS: targetParameters] as [String: Any]
@objc(displayedLocations:withParameters:)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mbox parameters are referred to as parameters. Let's rename
displayedLocations:withTargetParameters:

static func displayedLocations(mboxNames: [String], targetParameters: TargetParameters?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Target has been trying to move away from mbox, so that is why the new APIs read location (for mbox)
retrieveLocationContent
locationClicked etc

let's update the API

static func displayedLocations(names: [String], targetParameters: TargetParameters?)

or to align with objC

static func displayedLocations(_ names: [String], with targetParameters: TargetParameters?)

Thoughts?

if mboxNames.isEmpty {
Log.error(label: LOG_TAG, "Failed to send display notification, List of Mbox names must not be empty.")
return
}

let eventData = [TargetConstants.EventDataKeys.MBOX_NAMES: mboxNames, TargetConstants.EventDataKeys.IS_LOCATION_DISPLAYED: true, TargetConstants.EventDataKeys.TARGET_PARAMETERS: targetParameters ?? TargetParameters()] as [String: Any]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you skip adding TargetParameters to event data if not set by the customer?


let event = Event(name: TargetConstants.EventName.LOCATIONS_DISPLAYED, type: EventType.target, source: EventSource.requestContent, data: eventData)
MobileCore.dispatch(event: event)
}
Expand All @@ -162,12 +167,21 @@ import Foundation
/// location before, indicating that the mbox was viewed. This request helps Target record the clicked event for the given location or mbox.
///
/// - Parameters:
/// - name: NSString value representing the name for location/mbox
/// - mboxName: NSString value representing the name for location/mbox
/// - targetParameters: a TargetParameters object containing parameters for the location clicked
static func locationClicked(name _: String, targetParameters _: TargetParameters?) {
// TODO: need to verify input parameters
// TODO: need to convert "targetParameters" to [String:Any] array
let eventData = [TargetConstants.EventDataKeys.IS_LOCATION_DISPLAYED: true, TargetConstants.EventDataKeys.MBOX_NAMES: "", TargetConstants.EventDataKeys.MBOX_PARAMETERS: "", TargetConstants.EventDataKeys.ORDER_PARAMETERS: "", TargetConstants.EventDataKeys.PRODUCT_PARAMETERS: "", TargetConstants.EventDataKeys.PROFILE_PARAMETERS: ""] as [String: Any]
@objc(clickedLocation:withParameters:)
static func clickedLocation(mboxName: String, targetParameters: TargetParameters?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason as mentioned above

@objc(clickedLocation:withTargetParameters:)

and

static func clickedLocation(name: String, targetParameters: TargetParameters?)
or
static func clickedLocation(_ name: String, with targetParameters: TargetParameters?)

if mboxName.isEmpty {
Log.error(label: LOG_TAG, "Failed to send click notification, Mbox name must not be empty or nil.")
return
}

var eventData = [TargetConstants.EventDataKeys.MBOX_NAME: mboxName, TargetConstants.EventDataKeys.IS_LOCATION_CLICKED: true] as [String: Any]

if let targetParams = targetParameters {
eventData[TargetConstants.EventDataKeys.TARGET_PARAMETERS] = targetParams
}

let event = Event(name: TargetConstants.EventName.LOCATION_CLICKED, type: EventType.target, source: EventSource.requestContent, data: eventData)
MobileCore.dispatch(event: event)
}
Expand Down
Loading