diff --git a/.circleci/config.yml b/.circleci/config.yml index 1cc5203..212abc5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,9 +35,9 @@ jobs: # restore pods related caches - restore_cache: keys: - - cocoapods-cache-v1-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }} - - cocoapods-cache-v1-{{ arch }}-{{ .Branch }} - - cocoapods-cache-v1 + - cocoapods-cache-v5-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }} + - cocoapods-cache-v5-{{ arch }}-{{ .Branch }} + - cocoapods-cache-v5 # install CocoaPods - using default CocoaPods version, not the bundle - run: @@ -47,7 +47,7 @@ jobs: # save pods related files - save_cache: name: Saving CocoaPods Cache - key: cocoapods-cache-v1-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }} + key: cocoapods-cache-v5-{{ arch }}-{{ .Branch }}-{{ checksum "Podfile.lock" }} paths: - ./Pods - ~/.cocoapods @@ -76,4 +76,12 @@ jobs: # code coverage - run: name: Upload Code Coverage Report - command: bash <(curl -s https://codecov.io/bash) -v -X s3 -c -D "./build/out" -J "AEPTarget" + command: make codecov + + # verify XCFramework archive builds + - run: + name: Build XCFramework + command: | + if [ "${CIRCLE_BRANCH}" == "main" ]; then + make archive + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4dd47a7..a2469bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,12 @@ on: default: '3.0.0' action_tag: - description: 'create tag ("no" to skip)' + description: 'create tag? ("no" to skip)' + required: true + default: 'yes' + + release_AEPTarget: + description: 'release AEPTarget Pod? ("no" to skip)' required: true default: 'yes' @@ -24,6 +29,9 @@ jobs: - name: Install jq run: brew install jq + - name: Install cocoapods + run: gem install cocoapods + - name: Check version in Podspec run: | set -eo pipefail @@ -40,13 +48,16 @@ jobs: set -eo pipefail echo SPM integration test starts: make test-SPM-integration + - name: podspec file verification if: ${{ github.event.inputs.action_tag == 'yes' }} run: | set -eo pipefail echo podspec file verification starts: make test-podspec - - uses: release-drafter/release-drafter@v5 + +#https://github.com/release-drafter/release-drafter/commit/fe52e97d262833ae07d05efaf1a239df3f1b5cd4 + - uses: release-drafter/release-drafter@fe52e97d262833ae07d05efaf1a239df3f1b5cd4 if: ${{ github.event.inputs.action_tag == 'yes' }} with: name: v${{ github.event.inputs.tag }} @@ -56,11 +67,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish Pods + - name: Publish Pods - AEPTarget + if: ${{ github.event.inputs.release_AEPTarget == 'yes' }} run: | set -eo pipefail - gem install cocoapods - pod lib lint AEPTarget.podspec --allow-warnings --swift-version=5.1 pod trunk push AEPTarget.podspec --allow-warnings --swift-version=5.1 env: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} diff --git a/AEPTarget.podspec b/AEPTarget.podspec index dd6d5d0..8a9d944 100644 --- a/AEPTarget.podspec +++ b/AEPTarget.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPTarget" - s.version = "3.0.0" + s.version = "3.1.0" s.summary = "Experience Platform Target extension for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC The Experience Platform Target extension provides APIs that allow use of the Target product in the Adobe Experience Platform SDK. diff --git a/AEPTarget.xcodeproj/project.pbxproj b/AEPTarget.xcodeproj/project.pbxproj index 74d3f80..3d54442 100644 --- a/AEPTarget.xcodeproj/project.pbxproj +++ b/AEPTarget.xcodeproj/project.pbxproj @@ -1158,7 +1158,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.target; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1190,7 +1190,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.target; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/AEPTarget/Sources/Target+PublicAPI.swift b/AEPTarget/Sources/Target+PublicAPI.swift index 91123a3..b3804ed 100644 --- a/AEPTarget/Sources/Target+PublicAPI.swift +++ b/AEPTarget/Sources/Target+PublicAPI.swift @@ -16,7 +16,12 @@ import Foundation @objc public extension Target { /// true if the response content event listener is already registered, false otherwise - private static var isResponseListenerRegister: Bool = false + #if DEBUG + static var isResponseListenerRegister: Bool = false + #else + private static var isResponseListenerRegister: Bool = false + #endif + /// `Dictionary` to keep track of pending target request @nonobjc private static var pendingTargetRequest: ThreadSafeDictionary = ThreadSafeDictionary() @@ -96,6 +101,8 @@ import Foundation // If the callback is present call with default content if let callback = request.contentCallback { callback(request.defaultContent) + } else if let callback = request.contentWithDataCallback { + callback(request.defaultContent, nil) } Log.debug(label: Target.LOG_TAG, "TargetRequest removed because mboxName is empty.") continue @@ -290,7 +297,7 @@ import Foundation /// Handles the response event with event name as `TargetConstants.EventName.TARGET_REQUEST_RESPONSE` /// - Parameters: - /// - event: Response content event with content + /// - event: Response content event with content and optional data payload private static func handleResponseEvent(_ event: Event) { if event.name != TargetConstants.EventName.TARGET_REQUEST_RESPONSE { return @@ -311,11 +318,17 @@ import Foundation return } - guard let callback = targetRequest.contentCallback else { + if let callback = targetRequest.contentCallback { + let content = event.data?[TargetConstants.EventDataKeys.TARGET_CONTENT] as? String ?? targetRequest.defaultContent + callback(content) + } else if let callback = targetRequest.contentWithDataCallback { + let content = event.data?[TargetConstants.EventDataKeys.TARGET_CONTENT] as? String ?? targetRequest.defaultContent + let responsePayload = event.data?[TargetConstants.EventDataKeys.TARGET_DATA_PAYLOAD] as? [String: Any] + + callback(content, responsePayload) + } else { Log.warning(label: LOG_TAG, "Missing callback for target request with pair id the \(responsePairId)") return } - let content = event.data?[TargetConstants.EventDataKeys.TARGET_CONTENT] as? String ?? targetRequest.defaultContent - callback(content) } } diff --git a/AEPTarget/Sources/Target.swift b/AEPTarget/Sources/Target.swift index 4418f58..8e49b5c 100644 --- a/AEPTarget/Sources/Target.swift +++ b/AEPTarget/Sources/Target.swift @@ -325,7 +325,10 @@ public class Target: NSObject, Extension { continue } - dispatchAnalyticsForTargetRequest(payload: getAnalyticsForTargetPayload(mboxJson: mboxJson, sessionId: targetState.sessionId)) + // dispatch internal analytics for target event with analytics payload, if available + if let analyticsPayload = getAnalyticsForTargetPayload(json: mboxJson) { + dispatchAnalyticsForTargetRequest(payload: preprocessAnalyticsPayload(analyticsPayload, sessionId: targetState.sessionId)) + } } if targetState.notifications.isEmpty { @@ -375,27 +378,12 @@ public class Target: NSObject, Extension { return } - guard let metrics = mboxJson[TargetConstants.TargetJson.METRICS] as? [[String: Any?]?] else { + let metric = extractClickMetric(mboxJson: mboxJson) + guard metric.token != nil else { Log.warning(label: Target.LOG_TAG, "\(TargetError.ERROR_CLICK_NOTIFICATION_SEND_FAILED) \(TargetError.ERROR_NO_CLICK_METRICS) \(mboxName).") return } - var clickMetricFound = false - - for metricItem in metrics { - guard let metric = metricItem, 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 - } - - clickMetricFound = true - break - } - - if !clickMetricFound { - Log.warning(label: Target.LOG_TAG, "\(TargetError.ERROR_CLICK_NOTIFICATION_SEND_FAILED) \(TargetError.ERROR_NO_CLICK_METRIC_FOUND) \(mboxName).") - return - } - // bail out if the target configuration is not available or if the privacy is opted-out if let error = prepareForTargetRequest() { Log.warning(label: Target.LOG_TAG, TargetError.ERROR_CLICK_NOTIFICATION_NOT_SENT + error) @@ -413,6 +401,11 @@ public class Target: NSObject, Extension { return } + // dispatch internal analytics for target event with click-tracking analytics payload, if available + if let analyticsPayload = metric.analyticsPayload { + dispatchAnalyticsForTargetRequest(payload: preprocessAnalyticsPayload(analyticsPayload, sessionId: targetState.sessionId)) + } + let error = sendTargetRequest(event, targetParameters: event.targetParameters, lifecycleData: lifecycleSharedState, identityData: identitySharedState) { connection in self.processNotificationResponse(event: event, connection: connection) } @@ -509,13 +502,24 @@ public class Target: NSObject, Extension { } for targetRequest in batchRequests { - var content = "" - if let mboxJson = mboxesDictionary[targetRequest.name] { - content = extractMboxContent(mboxJson: mboxJson) ?? targetRequest.defaultContent - let payload = getAnalyticsForTargetPayload(mboxJson: mboxJson, sessionId: targetState.sessionId) - dispatchAnalyticsForTargetRequest(payload: payload) + guard let mboxJson = mboxesDictionary[targetRequest.name] else { + dispatchMboxContent(event: event, content: targetRequest.defaultContent, data: nil, responsePairId: targetRequest.responsePairId) + continue + } + + let (content, responseTokens) = extractMboxContentAndResponseTokens(mboxJson: mboxJson) + let analyticsPayload = getAnalyticsForTargetPayload(json: mboxJson) + + // dispatch internal analytics for target event with analytics payload, if available + if let payload = analyticsPayload { + dispatchAnalyticsForTargetRequest(payload: preprocessAnalyticsPayload(payload, sessionId: targetState.sessionId)) } - dispatchMboxContent(event: event, content: content, responsePairId: targetRequest.responsePairId) + let clickmetric = extractClickMetric(mboxJson: mboxJson) + + // package analytics payload and response tokens to be returned in request callback + let responsePayload = packageMboxResponsePayload(responseTokens: responseTokens, analyticsPayload: analyticsPayload, metricsAnalyticsPayload: clickmetric.analyticsPayload) + + dispatchMboxContent(event: event, content: content ?? targetRequest.defaultContent, data: responsePayload, responsePairId: targetRequest.responsePairId) } } @@ -631,8 +635,8 @@ public class Target: NSObject, Extension { targetParameters: TargetParameters? = nil, lifecycleData: [String: Any]? = nil, identityData: [String: Any]? = nil, - completionHandler: ((HttpConnection) -> Void)?) -> String? - { + completionHandler: ((HttpConnection) -> Void)?) + -> String? { let tntId = targetState.tntId let thirdPartyId = targetState.thirdPartyId let environmentId = Int64(targetState.environmentId) @@ -659,7 +663,9 @@ public class Target: NSObject, Extension { let request = NetworkRequest(url: url, httpMethod: .post, connectPayload: requestJson, httpHeaders: headers, connectTimeout: timeout, readTimeout: timeout) stopEvents() + Log.debug(label: Target.LOG_TAG, "Sending Target request with url: \(url.absoluteString) and body: \(requestJson).") networkService.connectAsync(networkRequest: request) { connection in + Log.debug(label: Target.LOG_TAG, "Target response is received with code: \(connection.responseCode ?? -1) and data: \(connection.responseString ?? "").") self.targetState.updateSessionTimestamp() if let completionHandler = completionHandler { completionHandler(connection) @@ -759,18 +765,27 @@ public class Target: NSObject, Extension { /// - batchRequests: `[TargetRequests]` to return the default content private func runDefaultCallbacks(event: Event, batchRequests: [TargetRequest]) { for request in batchRequests { - dispatchMboxContent(event: event, content: request.defaultContent, responsePairId: request.responsePairId) + dispatchMboxContent(event: event, content: request.defaultContent, data: nil, responsePairId: request.responsePairId) } } /// Dispatches the Target Response Content Event. /// - Parameters: - /// - content: the target content + /// - content: the target content. + /// - data: the target data payload containing one or more of response tokens, analytics payload and click tracking analytics payload. /// - pairId: the pairId of the associated target request content event. - private func dispatchMboxContent(event: Event, content: String, responsePairId: String) { + private func dispatchMboxContent(event: Event, content: String, data: [String: Any]?, responsePairId: String) { Log.trace(label: Target.LOG_TAG, "dispatchMboxContent - " + TargetError.ERROR_TARGET_EVENT_DISPATCH_MESSAGE) - let responseEvent = Event(name: TargetConstants.EventName.TARGET_REQUEST_RESPONSE, type: EventType.target, source: EventSource.responseContent, data: [TargetConstants.EventDataKeys.TARGET_CONTENT: content, TargetConstants.EventDataKeys.TARGET_RESPONSE_PAIR_ID: responsePairId, TargetConstants.EventDataKeys.TARGET_RESPONSE_EVENT_ID: event.id.uuidString]) + let responseEvent = Event(name: TargetConstants.EventName.TARGET_REQUEST_RESPONSE, + type: EventType.target, + source: EventSource.responseContent, + data: [ + TargetConstants.EventDataKeys.TARGET_CONTENT: content, + TargetConstants.EventDataKeys.TARGET_DATA_PAYLOAD: data as Any, + TargetConstants.EventDataKeys.TARGET_RESPONSE_PAIR_ID: responsePairId, + TargetConstants.EventDataKeys.TARGET_RESPONSE_EVENT_ID: event.id.uuidString, + ]) dispatch(event: responseEvent) } @@ -787,26 +802,35 @@ public class Target: NSObject, Extension { } Log.debug(label: Target.LOG_TAG, "processCachedTargetRequest - Cached mbox found for \(request.name) with data \(cachedMbox.description)") - let content = extractMboxContent(mboxJson: cachedMbox) ?? request.defaultContent - dispatchMboxContent(event: event, content: content, responsePairId: request.responsePairId) + let (content, responseTokens) = extractMboxContentAndResponseTokens(mboxJson: cachedMbox) + let analyticsPayload = getAnalyticsForTargetPayload(json: cachedMbox) + let metrics = extractClickMetric(mboxJson: cachedMbox) + + // package analytics payload and response tokens to be returned in request callback + let responsePayload = packageMboxResponsePayload(responseTokens: responseTokens, analyticsPayload: analyticsPayload, metricsAnalyticsPayload: metrics.analyticsPayload) + + dispatchMboxContent(event: event, content: content ?? request.defaultContent, data: responsePayload, responsePairId: request.responsePairId) } return requestsToSend } - /// Return Mbox content from mboxJson, if any + /// Return Mbox content and response tokens from mboxJson, if any. /// - Parameters: /// - mboxJson: `[String: Any]` target response dictionary - /// - Returns: `String` mbox content, if any otherwise returns nil - private func extractMboxContent(mboxJson: [String: Any]) -> String? { + /// - Returns: tuple containg `String` mbox content and `Dictionary` containing response tokens, if any. + private func extractMboxContentAndResponseTokens(mboxJson: [String: Any]) -> (content: String?, responseTokens: [String: String]?) { guard let optionsArray = mboxJson[TargetConstants.TargetJson.OPTIONS] as? [[String: Any?]?] else { Log.debug(label: Target.LOG_TAG, "extractMboxContent - unable to extract mbox contents, options array is nil") - return nil + return (nil, nil) } var contentBuilder: String = "" + var responseTokens: [String: String]? for option in optionsArray { + responseTokens = option?[TargetConstants.TargetJson.Option.RESPONSE_TOKENS] as? [String: String] + guard let content = option?[TargetConstants.TargetJson.Option.CONTENT] else { continue } @@ -826,7 +850,7 @@ public class Target: NSObject, Extension { } } - return contentBuilder + return (contentBuilder, responseTokens) } /// Dispatches an Analytics Event containing the Analytics for Target (A4T) payload @@ -845,15 +869,28 @@ public class Target: NSObject, Extension { MobileCore.dispatch(event: event) } - /// Grabs the a4t payload from the target response and convert the keys to correct format - /// Returns an empty `Dictionary` if there are no analytics payload that needs to be sent. + /// Preprocesses the analytics for target (a4t) payload received in the Target response for Analytics consumption. + /// - Returns: `Dictionary` containing a4t payload with keys in internal format. + private func preprocessAnalyticsPayload(_ payload: [String: String], sessionId: String) -> [String: String] { + var result: [String: String] = [:] + + for (k, v) in payload { + result["&&\(k)"] = v + } + + if !sessionId.isEmpty { + result[TargetConstants.TargetJson.SESSION_ID] = sessionId + } + + return result + } + + /// Grabs the analytics for target (a4t) payload from the target response if available. /// - Parameters: - /// - mboxJson: A prefetched mbox dictionary - /// - sessionId: session id - /// - Returns: `Dictionary` containing a4t payload - private func getAnalyticsForTargetPayload(mboxJson: [String: Any], sessionId: String) -> [String: String]? { - var payload: [String: String] = [:] - guard let analyticsJson = mboxJson[TargetConstants.TargetJson.ANALYTICS_PARAMETERS] as? [String: Any] else { + /// - json: dictionary containing a4t payload. + /// - Returns: `Dictionary` containing a4t payload or nil. + private func getAnalyticsForTargetPayload(json: [String: Any]) -> [String: String]? { + guard let analyticsJson = json[TargetConstants.TargetJson.ANALYTICS] as? [String: Any] else { return nil } @@ -861,14 +898,61 @@ public class Target: NSObject, Extension { return nil } - for (k, v) in payloadJson { - payload["&&\(k)"] = v + return payloadJson + } + + /// Extracts click metric info from the Target response if available. + /// - Parameters: + /// - mboxJson: Mbox dictionary. + /// - Returns: tuple containing `String` click token and `Dictionary` containing click tracking analytics payload. + private func extractClickMetric(mboxJson: [String: Any]) -> (token: String?, analyticsPayload: [String: String]?) { + guard let metrics = mboxJson[TargetConstants.TargetJson.METRICS] as? [[String: Any]?] else { + return (nil, nil) } - if !sessionId.isEmpty { - payload[TargetConstants.TargetJson.SESSION_ID] = sessionId + for metricItem in metrics { + guard + let metric = metricItem, + 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 + } + + let analyticsPayload = getAnalyticsForTargetPayload(json: metric) + return (token, analyticsPayload) + } + + return (nil, nil) + } + + /// Packages response tokens and analytics payload returned in Target response. + /// + /// If click tracking is enabled for the mbox, analytics payload returned inside metrics for click is also added. + /// - Parameters: + /// - responseTokens: dictionary containing response tokens. + /// - analyticsPayload: dictionary containing analytics for target (a4t) payload. + /// - metricsAnalyticsPayload: dictionary containing a4t payload for click metric. + /// - Returns: `Dictionary` containing Target payload or nil. + private func packageMboxResponsePayload(responseTokens: [String: String]?, + analyticsPayload: [String: String]?, + metricsAnalyticsPayload: [String: String]?) + -> [String: Any]? { + var responsePayload: [String: Any] = [:] + + if let responseTokens = responseTokens { + responsePayload[TargetConstants.TargetResponse.RESPONSE_TOKENS] = responseTokens + } + + if let analyticsPayload = analyticsPayload { + responsePayload[TargetConstants.TargetResponse.ANALYTICS_PAYLOAD] = analyticsPayload + } + + if let metricsAnalyticsPayload = metricsAnalyticsPayload { + responsePayload[TargetConstants.TargetResponse.CLICK_METRIC_ANALYTICS_PAYLOAD] = metricsAnalyticsPayload } - return payload + return responsePayload.isEmpty ? nil : responsePayload } } diff --git a/AEPTarget/Sources/TargetConstants.swift b/AEPTarget/Sources/TargetConstants.swift index db7f826..ebc33a2 100644 --- a/AEPTarget/Sources/TargetConstants.swift +++ b/AEPTarget/Sources/TargetConstants.swift @@ -15,7 +15,7 @@ import Foundation enum TargetConstants { static let EXTENSION_NAME = "com.adobe.module.target" static let FRIENDLY_NAME = "Target" - static let EXTENSION_VERSION = "3.0.0" + static let EXTENSION_VERSION = "3.1.0" static let DATASTORE_NAME = EXTENSION_NAME static let DEFAULT_SESSION_TIMEOUT: Int = 30 * 60 // 30 mins static let DELIVERY_API_URL_BASE = "https://%@/rest/v1/delivery/?client=%@&sessionId=%@" @@ -78,13 +78,19 @@ enum TargetConstants { static let IGNORED_SESSION_LENGTH = "a.ignoredSessionLength" } + enum TargetResponse { + static let RESPONSE_TOKENS = "responseTokens" + static let ANALYTICS_PAYLOAD = "analytics.payload" + static let CLICK_METRIC_ANALYTICS_PAYLOAD = "clickmetric.analytics.payload" + } + enum TargetJson { static let OPTIONS = "options" static let PARAMETERS = "parameters" static let METRICS = "metrics" static let HTML = "html" static let JSON = "json" - static let ANALYTICS_PARAMETERS = "analytics" + static let ANALYTICS = "analytics" static let ANALYTICS_PAYLOAD = "payload" /// For A4T requests event data. static let SESSION_ID = "a.target.sessionId" @@ -116,6 +122,7 @@ enum TargetConstants { enum Option { static let TYPE = "type" static let CONTENT = "content" + static let RESPONSE_TOKENS = "responseTokens" } } @@ -184,6 +191,7 @@ enum TargetConstants { static let PRODUCT_PARAMETERS = "productparameters" static let PROFILE_PARAMETERS = "profileparameters" static let TARGET_CONTENT = "content" + static let TARGET_DATA_PAYLOAD = "data" static let TARGET_RESPONSE_PAIR_ID = "responsePairId" static let TARGET_RESPONSE_EVENT_ID = "responseEventId" // shared sate diff --git a/AEPTarget/Sources/TargetDeliveryRequestBuilder.swift b/AEPTarget/Sources/TargetDeliveryRequestBuilder.swift index 0bcf632..ada6e3c 100644 --- a/AEPTarget/Sources/TargetDeliveryRequestBuilder.swift +++ b/AEPTarget/Sources/TargetDeliveryRequestBuilder.swift @@ -155,9 +155,10 @@ enum TargetDeliveryRequestBuilder { var customerIds = [CustomerID]() if let visitorIds = identitySharedState?[TargetConstants.Identity.SharedState.Keys.VISITOR_IDS_LIST] as? [[String: Any]] { for visitorId in visitorIds { - if let id = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_ID] as? String, - let code = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_TYPE] as? String, - let authenticatedState = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_AUTHENTICATION_STATE] as? Int + if + let id = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_ID] as? String, + let code = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_TYPE] as? String, + let authenticatedState = visitorId[TargetConstants.Identity.SharedState.Keys.VISITORID_AUTHENTICATION_STATE] as? Int { customerIds.append(CustomerID(id: id, integrationCode: code, authenticatedState: AuthenticatedState.from(state: authenticatedState))) } diff --git a/AEPTarget/Sources/TargetRequest.swift b/AEPTarget/Sources/TargetRequest.swift index 3d56afb..f533dcd 100644 --- a/AEPTarget/Sources/TargetRequest.swift +++ b/AEPTarget/Sources/TargetRequest.swift @@ -17,6 +17,7 @@ public class TargetRequest: NSObject, Codable { @objc public let targetParameters: TargetParameters? @objc let responsePairId: String @objc var contentCallback: ((String?) -> Void)? + @objc var contentWithDataCallback: ((String?, [String: Any]?) -> Void)? /// Instantiate a `TargetRequest` object /// - Parameters: @@ -29,6 +30,22 @@ public class TargetRequest: NSObject, Codable { self.defaultContent = defaultContent self.targetParameters = targetParameters self.contentCallback = contentCallback + contentWithDataCallback = nil + responsePairId = UUID().uuidString + } + + /// Instantiate a `TargetRequest` object + /// - Parameters: + /// - name: `String` mbox name for this request + /// - defaultContent: `String` default content for this request + /// - targetParameters: `TargetParameters` for this request + /// - contentWithDataCallback: which will get called with target mbox content, and an optional dictionary containing one or more of response tokens, analytics payload, and click metric analytics payload, if available. + @objc public init(mboxName: String, defaultContent: String, targetParameters: TargetParameters? = nil, contentWithDataCallback: ((String?, [String: Any]?) -> Void)? = nil) { + name = mboxName + self.defaultContent = defaultContent + self.targetParameters = targetParameters + self.contentWithDataCallback = contentWithDataCallback + contentCallback = nil responsePairId = UUID().uuidString } diff --git a/AEPTarget/Sources/TargetState.swift b/AEPTarget/Sources/TargetState.swift index 10a774e..2d27987 100644 --- a/AEPTarget/Sources/TargetState.swift +++ b/AEPTarget/Sources/TargetState.swift @@ -106,8 +106,9 @@ class TargetState { guard let configuration = configuration else { return } - if let newClientCode = configuration[TargetConstants.Configuration.SharedState.Keys.TARGET_CLIENT_CODE] as? String, - newClientCode != clientCode + if + let newClientCode = configuration[TargetConstants.Configuration.SharedState.Keys.TARGET_CLIENT_CODE] as? String, + newClientCode != clientCode { updateEdgeHost("") } @@ -189,9 +190,11 @@ class TargetState { let name = mbox.key var mboxNode = mbox.value if !name.isEmpty, prefetchedMboxJsonDicts[name] == nil { - // remove not accepted keys - for key in LOADED_MBOX_ACCEPTED_KEYS { - mboxNode.removeValue(forKey: key) + // Save click metrics, it will be used when sending notifications later. + for key in mboxNode.keys { + if !LOADED_MBOX_ACCEPTED_KEYS.contains(key) { + mboxNode.removeValue(forKey: key) + } } loadedMboxJsonDicts[name] = mboxNode } diff --git a/AEPTarget/Sources/URL+Target.swift b/AEPTarget/Sources/URL+Target.swift index 9f7728f..76af724 100644 --- a/AEPTarget/Sources/URL+Target.swift +++ b/AEPTarget/Sources/URL+Target.swift @@ -30,7 +30,7 @@ extension URL { else { return nil } - return queryItems.reduce([String: String]()) { (dict, queryItem) -> [String: String] in + return queryItems.reduce([String: String]()) { dict, queryItem -> [String: String] in var dict = dict dict[queryItem.name] = queryItem.value return dict diff --git a/AEPTarget/Tests/FunctionalTests/TargetClickedLocationFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetClickedLocationFunctionalTests.swift index 083e934..6be7087 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetClickedLocationFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetClickedLocationFunctionalTests.swift @@ -68,7 +68,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -124,7 +124,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { XCTAssertTrue(notificationsJson.contains("\"a.AppID\"")) XCTAssertTrue(notificationsJson.contains("\"a.locale\"")) - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } @@ -149,7 +149,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { XCTAssertEqual("DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", mockRuntime.createdSharedStates[0]?["tntid"] as? String) } - func testLocationClicked_afterRetrievedTheSameLocationContent() { + func testLocationClicked_afterRetrieveLocationContentNoMetrics() { // mocked network response let responseString = """ { @@ -171,8 +171,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { "content": { "key1": "value1" }, - "type": "json", - "eventToken": "uR0kIAPO+tZtIPW92S0NnWqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==" + "type": "json" } ], "analytics" : { @@ -185,7 +184,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { """ let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), ].map { $0.asDictionary() } @@ -212,7 +211,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -239,6 +238,145 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { eventListener(locationClickedEvent) } + func testLocationClicked_afterRetrieveLocationContent() { + // mocked network response + let responseString = """ + { + "status": 200, + "id": { + "tntId": "DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", + "marketingCloudVisitorId": "61055260263379929267175387965071996926" + }, + "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", + "client": "acopprod3", + "edgeHost": "mboxedge35.tt.omtrdc.net", + "execute": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01", + "options": [ + { + "content": { + "key1": "value1" + }, + "type": "json" + } + ], + "metrics": [ + { + "type":"click", + "eventToken":"ABPi/uih7s0vo6/8kqyxjA==" + } + ], + "analytics" : { + "payload" : {"pe" : "tnt", "tnta" : "33333:1:0|12121|1,38711:1:0|1|1"} + } + } + ] + } + } + """ + + let requestDataArray: [[String: Any]?] = [ + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), + ].map { + $0.asDictionary() + } + + let data: [String: Any] = [ + "request": requestDataArray, + "targetparams": TargetParameters(profileParameters: mockProfileParam).asDictionary() as Any, + ] + let loadRequestEvent = Event(name: "TargetLoadRequest", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.requestContent", + data: data) + + // creates a configuration's shared state + mockRuntime.simulateSharedState(extensionName: "com.adobe.module.configuration", event: loadRequestEvent, data: (value: mockConfigSharedState, status: .set)) + + // creates a lifecycle's shared state + mockRuntime.simulateSharedState(extensionName: "com.adobe.module.lifecycle", event: loadRequestEvent, data: (value: mockLifecycleData, status: .set)) + + // creates an identity's shared state + mockRuntime.simulateSharedState(extensionName: "com.adobe.module.identity", event: loadRequestEvent, data: (value: mockIdentityData, status: .set)) + + // registers the event listeners for Target extension + target.onRegistered() + + // override network service + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + return nil + } + + guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { + XCTFail() + return + } + + XCTAssertTrue(target.readyForEvent(loadRequestEvent)) + XCTAssertTrue(target.targetState.loadedMboxJsonDicts.isEmpty) + // handles retrieve location content event + eventListener(loadRequestEvent) + XCTAssertEqual(1, target.targetState.loadedMboxJsonDicts.count) + XCTAssertTrue(Set(target.targetState.loadedMboxJsonDicts.keys) == Set([ + "t_test_01", + ])) + + // Build the location data + let locationClickedEvent = Event(name: "TargetLocationClicked", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.requestContent", + data: [ + "name": "t_test_01", + TargetConstants.EventDataKeys.IS_LOCATION_CLICKED: true, + ]) + + mockNetworkService.resolvers.removeAll() + mockNetworkService.mock { request in + if !request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + XCTFail("Location clicked should trigger a notification request to Target.") + return nil + } + + guard let payloadDictionary = self.payloadAsDictionary(request.connectPayload) else { + XCTFail("Location clicked notification payload should be valid.") + return nil + } + + guard let notificationsArray = payloadDictionary["notifications"] as? [[String: Any]] else { + XCTFail("Location clicked notification should be present in the request payload.") + return nil + } + + XCTAssertEqual(1, notificationsArray.count) + let notification = notificationsArray[0] + + let notificationId = notification["id"] as? String + XCTAssertEqual(false, notificationId?.isEmpty) + + let tokens = notification["tokens"] as? [String] + XCTAssertEqual(1, tokens?.count) + XCTAssertEqual("ABPi/uih7s0vo6/8kqyxjA==", tokens?[0]) + XCTAssertEqual("click", notification["type"] as? String) + + let mbox = notification["mbox"] as? [String: Any] + XCTAssertEqual("t_test_01", mbox?["name"] as? String) + + return nil + } + + // simulate location clicked event + mockRuntime.simulateComingEvent(event: locationClickedEvent) + } + func testLocationClicked_withoutPrefetchedMbox() { // mocked network response let responseString = """ @@ -299,7 +437,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -379,7 +517,7 @@ class TargetClickedLocationFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } diff --git a/AEPTarget/Tests/FunctionalTests/TargetDisplayedLocationsFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetDisplayedLocationsFunctionalTests.swift index 9c5732e..6492d9a 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetDisplayedLocationsFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetDisplayedLocationsFunctionalTests.swift @@ -69,7 +69,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -127,7 +127,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { XCTAssertTrue(notificationsJson.contains("\"a.AppID\"")) XCTAssertTrue(notificationsJson.contains("\"a.locale\"")) - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -186,7 +186,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let errorResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) + let errorResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: errorResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -237,7 +237,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { """ let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), ].map { $0.asDictionary() } @@ -264,7 +264,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -351,7 +351,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -432,7 +432,7 @@ class TargetDisplayedLocationsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } diff --git a/AEPTarget/Tests/FunctionalTests/TargetFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetFunctionalTests.swift index 5fbd3c0..cbf1683 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetFunctionalTests.swift @@ -122,7 +122,7 @@ class TargetFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } @@ -159,7 +159,7 @@ class TargetFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) return nil } eventListener(prefetchEvent2) @@ -271,7 +271,10 @@ class TargetFunctionalTests: TargetFunctionalTestsBase { mockNetworkService.resolvers.removeAll() mockNetworkService.mock { request in XCTAssertNotNil(request) - let requestJson = JSON(parseJSON: request.connectPayload) + guard let requestJson = try? JSON(data: request.connectPayload) else { + XCTFail("Target response should be valid for prefetch request.") + return nil + } XCTAssertEqual("DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", requestJson["id"]["tntId"].stringValue) XCTAssertFalse(requestJson["id"]["thirdPartyId"].exists()) XCTAssertFalse(requestJson["id"]["customerIds"].exists()) @@ -453,7 +456,7 @@ class TargetFunctionalTests: TargetFunctionalTestsBase { XCTAssertNotNil(request) let queryMap = self.getQueryMap(url: request.url.absoluteString) XCTAssertEqual(queryMap["sessionId"] ?? "", storedSessionId) - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } @@ -488,7 +491,7 @@ class TargetFunctionalTests: TargetFunctionalTestsBase { XCTAssertNotNil(request) let queryMap = self.getQueryMap(url: request.url.absoluteString) XCTAssertEqual(queryMap["sessionId"] ?? "", storedSessionId) - XCTAssertTrue(request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) return nil } eventListener(prefetchEvent2) diff --git a/AEPTarget/Tests/FunctionalTests/TargetFunctionalTestsBase.swift b/AEPTarget/Tests/FunctionalTests/TargetFunctionalTestsBase.swift index a9bec52..2e4a3d2 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetFunctionalTestsBase.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetFunctionalTestsBase.swift @@ -30,7 +30,7 @@ class TargetFunctionalTestsBase: XCTestCase { override func setUp() { // Mock data - mockConfigSharedState = ["target.clientCode": "code_123", "global.privacy": "optedin"] + mockConfigSharedState = ["target.clientCode": "acopprod3", "global.privacy": "optedin"] mockLifecycleData = [ "lifecyclecontextdata": [ @@ -103,9 +103,8 @@ class TargetFunctionalTestsBase: XCTestCase { return prettyPrintedString } - func payloadAsDictionary(_ payload: String?) -> [String: Any]? { - if let payload = payload, let data = payload.data(using: .utf8), - let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] + func payloadAsDictionary(_ payload: Data) -> [String: Any]? { + if let dictionary = try? JSONSerialization.jsonObject(with: payload, options: .allowFragments) as? [String: Any] { return dictionary } diff --git a/AEPTarget/Tests/FunctionalTests/TargetLoadRequestsFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetLoadRequestsFunctionalTests.swift index 8b1c6ea..7634d43 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetLoadRequestsFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetLoadRequestsFunctionalTests.swift @@ -41,8 +41,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { "content": { "key1": "value1" }, - "type": "json", - "eventToken": "uR0kIAPO+tZtIPW92S0NnWqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==" + "type": "json" } ], "analytics" : { @@ -55,8 +54,8 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { """ let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), - TargetRequest(mboxName: "t_test_02", defaultContent: "default2", targetParameters: TargetParameters(parameters: ["mbox-parameter-key2": "mbox-parameter-value2"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(parameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), + TargetRequest(mboxName: "t_test_02", defaultContent: "default2", targetParameters: TargetParameters(parameters: ["mbox-parameter-key2": "mbox-parameter-value2"]), contentCallback: nil), ].map { $0.asDictionary() } @@ -89,7 +88,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -161,7 +160,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTAssertNotNil(executeJson["mboxes"][1]["parameters"]["a.OSVersion"].stringValue) XCTAssertNotNil(executeJson["mboxes"][1]["parameters"]["a.AppID"].stringValue) XCTAssertEqual(executeJson["mboxes"][1]["parameters"]["mbox-parameter-key2"].stringValue, "mbox-parameter-value2") - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -177,7 +176,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTAssertEqual("mboxedge35.tt.omtrdc.net", target.targetState.edgeHost) XCTAssertEqual(1, target.targetState.loadedMboxJsonDicts.count) let mboxJson = prettify(target.targetState.loadedMboxJsonDicts["t_test_01"]) - XCTAssertTrue(mboxJson.contains("\"eventToken\" : \"uR0kIAPO+tZtIPW92S0NnWqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==\"")) + XCTAssertTrue(mboxJson.contains("\"name\" : \"t_test_01\"")) // verifies the Target's shared state XCTAssertEqual(1, mockRuntime.createdSharedStates.count) @@ -186,7 +185,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { func testLoadRequestContent_withOrderParameters() { let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"], order: TargetOrder(id: "id", total: 12.34, purchasedProductIds: ["A", "B", "C"]))), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"], order: TargetOrder(id: "id", total: 12.34, purchasedProductIds: ["A", "B", "C"])), contentCallback: nil), ].map { $0.asDictionary() } @@ -219,7 +218,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -296,7 +295,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { func testLoadRequestContent_withProductParameters() { let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"], product: TargetProduct(productId: "764334", categoryId: "Online"))), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"], product: TargetProduct(productId: "764334", categoryId: "Online")), contentCallback: nil), ].map { $0.asDictionary() } @@ -329,7 +328,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -411,7 +410,8 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"], order: TargetOrder(id: "id", total: 12.34, purchasedProductIds: ["A", "B", "C"]), product: TargetProduct(productId: "764334", categoryId: "Online") - )), + ), + contentCallback: nil), ].map { $0.asDictionary() } @@ -444,7 +444,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -554,8 +554,8 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { } """ let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), - TargetRequest(mboxName: "t_test_02", defaultContent: "default2", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), + TargetRequest(mboxName: "t_test_02", defaultContent: "default2", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), ].map { $0.asDictionary() } @@ -582,7 +582,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 400, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } @@ -616,7 +616,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { } """ let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default_content_123"), + TargetRequest(mboxName: "t_test_01", defaultContent: "default_content_123", contentCallback: nil), ].map { $0.asDictionary() } @@ -642,8 +642,8 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { @@ -684,7 +684,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { // creates a configuration's shared state let configuration = [ - "target.clientCode": "code_123", + "target.clientCode": "acopprod3", "global.privacy": "optedin", ] mockRuntime.simulateSharedState(extensionName: "com.adobe.module.configuration", event: prefetchEvent, data: (value: configuration, status: .set)) @@ -698,7 +698,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let badResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 500, httpVersion: nil, headerFields: nil) + let badResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 500, httpVersion: nil, headerFields: nil) networkRequestExpectation.fulfill() return (data: responseString.data(using: .utf8), response: badResponse, error: nil) } @@ -713,7 +713,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTAssertEqual(1, mockRuntime.dispatchedEvents.count) mockRuntime.resetDispatchedEventAndCreatedSharedStates() let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default_content_123"), + TargetRequest(mboxName: "t_test_01", defaultContent: "default_content_123", contentCallback: nil), ].map { $0.asDictionary() } @@ -808,7 +808,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { // creates a configuration's shared state let configuration = [ - "target.clientCode": "code_123", + "target.clientCode": "acopprod3", "global.privacy": "optedin", ] mockRuntime.simulateSharedState(extensionName: "com.adobe.module.configuration", event: prefetchEvent, data: (value: configuration, status: .set)) @@ -822,7 +822,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let badResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let badResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) networkRequestExpectation.fulfill() return (data: responseString.data(using: .utf8), response: badResponse, error: nil) } @@ -837,9 +837,9 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { XCTAssertEqual(1, mockRuntime.dispatchedEvents.count) mockRuntime.resetDispatchedEventAndCreatedSharedStates() let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "Drink_1", defaultContent: "default_content"), - TargetRequest(mboxName: "Drink_2", defaultContent: "default_content"), - TargetRequest(mboxName: "Drink_3", defaultContent: "default_content_123"), + TargetRequest(mboxName: "Drink_1", defaultContent: "default_content", contentCallback: nil), + TargetRequest(mboxName: "Drink_2", defaultContent: "default_content", contentCallback: nil), + TargetRequest(mboxName: "Drink_3", defaultContent: "default_content_123", contentCallback: nil), ].map { $0.asDictionary() } @@ -933,7 +933,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { // creates a configuration's shared state let configuration = [ - "target.clientCode": "code_123", + "target.clientCode": "acopprod3", "global.privacy": "optedin", ] mockRuntime.simulateSharedState(extensionName: "com.adobe.module.configuration", event: prefetchEvent, data: (value: configuration, status: .set)) @@ -946,7 +946,7 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { _ in - let badResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let badResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) networkRequestExpectation.fulfill() return (data: responseString.data(using: .utf8), response: badResponse, error: nil) } @@ -963,8 +963,8 @@ class TargetLoadRequestsFunctionalTests: TargetFunctionalTestsBase { mockRuntime.resetDispatchedEventAndCreatedSharedStates() let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "Drink_1", defaultContent: "default_content"), - TargetRequest(mboxName: "Drink_2", defaultContent: "default_content"), + TargetRequest(mboxName: "Drink_1", defaultContent: "default_content", contentCallback: nil), + TargetRequest(mboxName: "Drink_2", defaultContent: "default_content", contentCallback: nil), ].map { $0.asDictionary() } diff --git a/AEPTarget/Tests/FunctionalTests/TargetPrefetchFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetPrefetchFunctionalTests.swift index 002420a..4be019b 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetPrefetchFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetPrefetchFunctionalTests.swift @@ -91,7 +91,7 @@ class TargetPrefetchFunctionalTests: TargetFunctionalTestsBase { XCTFail() return nil } - XCTAssertTrue(request.url.absoluteString.contains("https://code_123.tt.omtrdc.net/rest/v1/delivery/?client=code_123&sessionId=")) + XCTAssertTrue(request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=")) XCTAssertTrue(Set(payloadDictionary.keys) == Set([ "id", "experienceCloud", @@ -158,7 +158,7 @@ class TargetPrefetchFunctionalTests: TargetFunctionalTestsBase { XCTAssertNotNil(prefetchJson["mboxes"][0]["parameters"]["a.RunMode"].stringValue) XCTAssertNotNil(prefetchJson["mboxes"][0]["parameters"]["a.AppID"].stringValue) XCTAssertNotNil(prefetchJson["mboxes"][0]["parameters"]["a.locale"].stringValue) - let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } guard let eventListener: EventListener = mockRuntime.listeners["com.adobe.eventType.target-com.adobe.eventSource.requestContent"] else { diff --git a/AEPTarget/Tests/FunctionalTests/TargetThirdPartyIdFunctionalTests.swift b/AEPTarget/Tests/FunctionalTests/TargetThirdPartyIdFunctionalTests.swift index db1b8a4..2a19ecd 100644 --- a/AEPTarget/Tests/FunctionalTests/TargetThirdPartyIdFunctionalTests.swift +++ b/AEPTarget/Tests/FunctionalTests/TargetThirdPartyIdFunctionalTests.swift @@ -33,7 +33,7 @@ class TargetThirdPartyIdFunctionalTests: TargetFunctionalTestsBase { XCTAssertEqual(target.targetState.thirdPartyId, "mockId") let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), ].map { $0.asDictionary() } @@ -100,7 +100,7 @@ class TargetThirdPartyIdFunctionalTests: TargetFunctionalTestsBase { XCTAssertNil(target.targetState.thirdPartyId) let requestDataArray: [[String: Any]?] = [ - TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), + TargetRequest(mboxName: "t_test_01", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), ].map { $0.asDictionary() } diff --git a/AEPTarget/Tests/IntegrationTests/TargetIntegrationTests.swift b/AEPTarget/Tests/IntegrationTests/TargetIntegrationTests.swift index 5b74cb8..d9a22a8 100644 --- a/AEPTarget/Tests/IntegrationTests/TargetIntegrationTests.swift +++ b/AEPTarget/Tests/IntegrationTests/TargetIntegrationTests.swift @@ -29,7 +29,15 @@ class TargetIntegrationTests: XCTestCase { EventHub.reset() } - override func tearDown() {} + override func tearDown() { + Target.isResponseListenerRegister = false + let unregisterExpectation = XCTestExpectation(description: "Unregister extension.") + unregisterExpectation.expectedFulfillmentCount = 1 + MobileCore.unregisterExtension(Target.self) { + unregisterExpectation.fulfill() + } + wait(for: [unregisterExpectation], timeout: 1) + } private func waitForLatestSettledSharedState(_ extensionName: String, timeout: Double = 1, triggerEvent: Event? = nil) -> [String: Any]? { var sharedState: [String: Any]? @@ -136,7 +144,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", ]) guard let config = waitForLatestSettledSharedState("com.adobe.module.configuration", timeout: 2) else { XCTFail("failed to retrieve the latest configuration (.set)") @@ -182,9 +190,8 @@ class TargetIntegrationTests: XCTestCase { ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { request in Log.debug(label: self.T_LOG_TAG, "request url is: \(request.url.absoluteString)") - if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") { - if let data = request.connectPayload.data(using: .utf8), - let payloadDictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + if let payloadDictionary = try? JSONSerialization.jsonObject(with: request.connectPayload, options: .allowFragments) as? [String: Any] { Log.debug(label: self.T_LOG_TAG, "request payload is: \n \(self.prettify(payloadDictionary))") @@ -212,19 +219,19 @@ class TargetIntegrationTests: XCTestCase { // { // "id": "vid_id_1", // "integrationCode": "vid_type_1", -// "authenticatedState": "unknown" +// "authenticatedState": "authenticated" // }, // { // "id": "vid_id_2", // "integrationCode": "vid_type_2", -// "authenticatedState": "unknown" +// "authenticatedState": "authenticated" // } // ] // } let customerIdsJson = self.prettifyJsonArray(customerIds) XCTAssertTrue(customerIdsJson.contains("\"integrationCode\" : \"vid_type_1\"")) XCTAssertTrue(customerIdsJson.contains("\"id\" : \"vid_id_2\"")) - XCTAssertTrue(customerIdsJson.contains("\"authenticatedState\" : \"unknown\"")) + XCTAssertTrue(customerIdsJson.contains("\"authenticatedState\" : \"authenticated\"")) // verify payloadDictionary["context"] guard let contextDictionary = payloadDictionary["context"] as? [String: Any] else { @@ -332,7 +339,7 @@ class TargetIntegrationTests: XCTestCase { XCTFail("Failed to parse the request payload [\(request.connectPayload)] to JSON object") } networkRequestExpectation.fulfill() - } // end if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") + } // end if request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } @@ -362,8 +369,29 @@ class TargetIntegrationTests: XCTestCase { "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", "client": "acopprod3", "edgeHost": "mboxedge35.tt.omtrdc.net", - "prefetch": { - "mboxes": [] + "execute": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01", + "options": [ + { + "content": "someContent1", + "type": "html" + } + ] + }, + { + "index": 0, + "name": "t_test_02", + "options": [ + { + "content": "someContent2", + "type": "html" + } + ] + } + ] } } """ @@ -383,20 +411,425 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", ]) let targetRequestExpectation = XCTestExpectation(description: "monitor the target request") let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { request in - if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") { + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + return nil + } + let retrieveRequest1 = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content1") { content in + XCTAssertEqual("someContent1", content) + targetRequestExpectation.fulfill() + } + let retrieveRequest2 = TargetRequest(mboxName: "t_test_02", + defaultContent: "default_content2") { content, data in + XCTAssertEqual("someContent2", content) + XCTAssertNil(data) + targetRequestExpectation.fulfill() + } + Target.retrieveLocationContent([retrieveRequest1, retrieveRequest2]) + wait(for: [targetRequestExpectation], timeout: 1) + } + + func testRetrieveLocationContent_defaultContentWhenNoTargetResponseContent() { + let responseString = """ + { + "status": 200, + "id": { + "tntId": "DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", + "marketingCloudVisitorId": "61055260263379929267175387965071996926" + }, + "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", + "client": "acopprod3", + "edgeHost": "mboxedge35.tt.omtrdc.net", + "execute": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01" + }, + { + "index": 1, + "name": "t_test_02" + } + ] + } + } + """ + let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.server": "amsdk.tt.omtrdc.net", + "target.clientCode": "acopprod3", + ]) + + let targetRequestExpectation = XCTestExpectation(description: "Should return default content when no content is returned from Target.") + targetRequestExpectation.expectedFulfillmentCount = 2 + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://amsdk .tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") { + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + return nil + } + let retrieveRequest1 = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content1") { content in + XCTAssertEqual("default_content1", content) + targetRequestExpectation.fulfill() + } + let retrieveRequest2 = TargetRequest(mboxName: "t_test_02", + defaultContent: "default_content2") { content, data in + XCTAssertEqual("default_content2", content) + XCTAssertNil(data) + targetRequestExpectation.fulfill() + } + Target.retrieveLocationContent([retrieveRequest1, retrieveRequest2]) + wait(for: [targetRequestExpectation], timeout: 1) + } + + func testRetrieveLocationContent_defaultContentOnTargetServerError() { + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.server": "amsdk.tt.omtrdc.net", + "target.clientCode": "acopprod3", + ]) + + let targetRequestExpectation = XCTestExpectation(description: "retrieveLocationContent should return default content when response indicates server error.") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://amsdk .tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let response = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 500, httpVersion: nil, headerFields: nil) + return (data: nil, response: response, error: nil) + } + return nil + } + let retrieveRequest = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content1") { content, data in + XCTAssertEqual("default_content1", content) + XCTAssertNil(data) + targetRequestExpectation.fulfill() + } + + Target.retrieveLocationContent([retrieveRequest]) + wait(for: [targetRequestExpectation], timeout: 1) + } + + func testRetrieveLocationContent_afterPrefetch() { + let responseString = """ + { + "status": 200, + "id": { + "tntId": "DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", + "marketingCloudVisitorId": "61055260263379929267175387965071996926" + }, + "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", + "client": "acopprod3", + "edgeHost": "mboxedge35.tt.omtrdc.net", + "prefetch": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01", + "options": [ + { + "content": "someContent1", + "type": "html" + } + ], + "eventToken": "uR0kIAPO+tZtIPW92S0NnWqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==" + }, + { + "index": 1, + "name": "t_test_02", + "options": [ + { + "content": "someContent2", + "type": "html" + } + ], + "eventToken": "mKH481kPwvU9+su+8rbG4GqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==" + } + ] + } + } + """ + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.clientCode": "acopprod3", + ]) + + let prefetchExpectation = XCTestExpectation(description: "prefetchContent should prefetch content without error.") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + + if request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + XCTFail("retrieveLocationContant should not send a network request to Target if requested mboxes are already prefetched.") + return nil + } + return nil + } + + Target.prefetchContent([ + TargetPrefetch(name: "t_test_01", targetParameters: nil), + TargetPrefetch(name: "t_test_02", targetParameters: nil), + ]) { error in + if let error = error { + Log.error(label: self.T_LOG_TAG, "Target.prefetchContent - failed, error: \(String(describing: error))") + XCTFail("Target.prefetchContent - failed, error: \(String(describing: error))") + } + prefetchExpectation.fulfill() + } + wait(for: [prefetchExpectation], timeout: 1) + + let targetRequestExpectation = XCTestExpectation(description: "retrieveLocationContent should return prefetched content for the given mboxes.") + targetRequestExpectation.expectedFulfillmentCount = 2 + let retrieveRequest1 = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content1") { content in + XCTAssertEqual("someContent1", content) + targetRequestExpectation.fulfill() + } + let retrieveRequest2 = TargetRequest(mboxName: "t_test_02", + defaultContent: "default_content2") { content, data in + XCTAssertEqual("someContent2", content) + XCTAssertNil(data) + targetRequestExpectation.fulfill() + } + Target.retrieveLocationContent([retrieveRequest1, retrieveRequest2]) + wait(for: [targetRequestExpectation], timeout: 1) + } + + func testRetrieveLocationContent_withA4TAndResponseTokens() { + let responseString = """ + { + "status": 200, + "id": { + "tntId": "DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", + "marketingCloudVisitorId": "61055260263379929267175387965071996926" + }, + "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", + "client": "acopprod3", + "edgeHost": "mboxedge35.tt.omtrdc.net", + "execute": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01", + "options": [ + { + "content": "someContent", + "type": "html", + "responseTokens":{ + "activity.name":"My test activity" + } + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"331289:0:0|2|1,331289:0:0|32767|1" + } + } + } + ] + } + } + """ + let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.server": "amsdk.tt.omtrdc.net", + "target.clientCode": "acopprod3", + ]) + + let targetRequestExpectation = XCTestExpectation(description: "retrieveLocationContent should return A4T payload and response tokens.") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + return nil + } + + let retrieveRequest = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content") { content, data in + XCTAssertEqual("someContent", content) + + guard let data = data else { + XCTFail("Data containing A4T payload and response tokens should be valid.") + return + } + let analyticsPayload = data["analytics.payload"] as? [String: String] + XCTAssertEqual(2, analyticsPayload?.count) + XCTAssertEqual("tnt", analyticsPayload?["pe"]) + XCTAssertEqual("331289:0:0|2|1,331289:0:0|32767|1", analyticsPayload?["tnta"]) + + let responseTokens = data["responseTokens"] as? [String: String] + XCTAssertEqual(1, responseTokens?.count) + XCTAssertEqual("My test activity", responseTokens?["activity.name"]) + targetRequestExpectation.fulfill() + } + + Target.retrieveLocationContent([retrieveRequest]) + wait(for: [targetRequestExpectation], timeout: 1) + } + + func testRetrieveLocationContent_afterPrefetchwithA4TAndResponseTokens() { + let responseString = """ + { + "status": 200, + "id": { + "tntId": "DE03D4AD-1FFE-421F-B2F2-303BF26822C1.35_0", + "marketingCloudVisitorId": "61055260263379929267175387965071996926" + }, + "requestId": "01d4a408-6978-48f7-95c6-03f04160b257", + "client": "acopprod3", + "edgeHost": "mboxedge35.tt.omtrdc.net", + "prefetch": { + "mboxes": [ + { + "index": 0, + "name": "t_test_01", + "options": [ + { + "content": "someContent", + "type": "html", + "responseTokens":{ + "activity.name":"My test activity" + }, + "eventToken": "uR0kIAPO+tZtIPW92S0NnWqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==" + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"331289:0:0|2|1,331289:0:0|32767|1" + } + } + } + ] + } + } + """ + let validResponse = HTTPURLResponse(url: URL(string: "https://amsdk.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.server": "amsdk.tt.omtrdc.net", + "target.clientCode": "acopprod3", + ]) + + let prefetchExpectation = XCTestExpectation(description: "Should return A4T payload and response tokens") + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { return (data: responseString.data(using: .utf8), response: validResponse, error: nil) } return nil } - let retrieveRequest = TargetRequest(mboxName: "t_test_01", defaultContent: "default_content") { content in - XCTAssertEqual("default_content", content) + + Target.prefetchContent([ + TargetPrefetch(name: "t_test_01", targetParameters: nil), + ], + with: TargetParameters(profileParameters: ["name": "Smith"])) { error in + if let error = error { + Log.error(label: self.T_LOG_TAG, "Target.prefetchContent - failed, error: \(String(describing: error))") + XCTFail("Target.prefetchContent - failed, error: \(String(describing: error))") + } + prefetchExpectation.fulfill() + } + wait(for: [prefetchExpectation], timeout: 1) + + let targetRequestExpectation = XCTestExpectation(description: "retrieveLocationContent should return A4T payload and response tokens cached on prefetch.") + let retrieveRequest = TargetRequest(mboxName: "t_test_01", + defaultContent: "default_content") { content, data in + XCTAssertEqual("someContent", content) + + guard let data = data else { + XCTFail("Data containing A4T payload and response tokens should be valid.") + return + } + let analyticsPayload = data["analytics.payload"] as? [String: String] + XCTAssertEqual(2, analyticsPayload?.count) + XCTAssertEqual("tnt", analyticsPayload?["pe"]) + XCTAssertEqual("331289:0:0|2|1,331289:0:0|32767|1", analyticsPayload?["tnta"]) + + let responseTokens = data["responseTokens"] as? [String: String] + XCTAssertEqual(1, responseTokens?.count) + XCTAssertEqual("My test activity", responseTokens?["activity.name"]) targetRequestExpectation.fulfill() } Target.retrieveLocationContent([retrieveRequest]) @@ -418,7 +851,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", ]) let getErrorExpectation = XCTestExpectation(description: "init extensions") @@ -458,7 +891,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", ]) let getErrorExpectation = XCTestExpectation(description: "init extensions") @@ -487,7 +920,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", ]) Target.setThirdPartyId("third_party_id") @@ -592,7 +1025,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", "analytics.server": "test.analytics.net", "analytics.rsids": "abc", "analytics.batchLimit": 0, @@ -606,8 +1039,9 @@ class TargetIntegrationTests: XCTestCase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { request in - if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") { - if request.connectPayload.contains("ADCKKBC") { + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let connectPayloadString = String(decoding: request.connectPayload, as: UTF8.self) + if connectPayloadString.contains("ADCKKBC") { targetRequestExpectation.fulfill() return nil } else { @@ -666,7 +1100,6 @@ class TargetIntegrationTests: XCTestCase { "metrics": [ { "type": "click", - "selector": "#app > DIV:nth-of-type(1) > DIV:nth-of-type(2) > SECTION.section:eq(0) > DIV.container:eq(0) > FORM.col-md-4:eq(0) > DIV.form-group:eq(0) > BUTTON.btn:eq(0)", "eventToken": "QPaLjCeI9qKCBUylkRQKBg==" } ], @@ -718,7 +1151,7 @@ class TargetIntegrationTests: XCTestCase { "experienceCloud.server": "test.com", "global.privacy": "optedin", "target.server": "amsdk.tt.omtrdc.net", - "target.clientCode": "amsdk", + "target.clientCode": "acopprod3", "analytics.server": "test.analytics.net", "analytics.rsids": "abc", "analytics.batchLimit": 0, @@ -732,8 +1165,9 @@ class TargetIntegrationTests: XCTestCase { let mockNetworkService = TestableNetworkService() ServiceProvider.shared.networkService = mockNetworkService mockNetworkService.mock { request in - if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=amsdk&sessionId=") { - if request.connectPayload.contains("ADCKKBC") { + if request.url.absoluteString.contains("https://amsdk.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let connectPayloadString = String(decoding: request.connectPayload, as: UTF8.self) + if connectPayloadString.contains("ADCKKBC") { targetRequestExpectation.fulfill() return nil } else { @@ -758,4 +1192,342 @@ class TargetIntegrationTests: XCTestCase { ) wait(for: [targetRequestExpectation], timeout: 1) } + + func testClickedLocation_withA4TClickMetricForPrefetchedMbox() { + let responseString = """ + { + "status":200, + "requestId":"602c9986-ae9e-48e9-b35b-45b14d589703", + "client":"acopprod3", + "id":{ + "tntId":"55508C70-F530-4E33-AAD2-F09BB99C5C3E.35_0", + "marketingCloudVisitorId":"32943535451574954856183504879211787972" + }, + "edgeHost":"mboxedge35.tt.omtrdc.net", + "prefetch":{ + "mboxes":[ + { + "index":0, + "name":"mboxName1", + "state":"HGGFUlY2Hmffsj5VuZfIKvOoZVgDVZNvMSVRJkhvV+1BQzL9/VYMJF0oT8y0dzKFFVP/IVnGUuOesmpjkWvCWA==", + "options":[ + { + "content":"someContent1", + "type":"html", + "eventToken":"mKH481kPwvU9+su+8rbG4GqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==", + "sourceType":"target" + } + ], + "metrics":[ + { + "type":"click", + "eventToken":"ABPi/uih7s0vo6/8kqyxjA==", + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"409277:0:0|32767" + } + } + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"409277:0:0|2,409277:0:0|1" + } + } + }, + { + "index":1, + "name":"mboxName2", + "state":"HGGFUlY2Hmffsj5VuZfIKvOoZVgDVZNvMSVRJkhvV+1BQzL9/VYMJF0oT8y0dzKFFVP/IVnGUuOesmpjkWvCWA==", + "options":[ + { + "content":"someContent2", + "type":"html", + "eventToken":"/CB0Gnng3tuikitYzXjtYGqipfsIHvVzTQxHolz2IpSCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q==", + "sourceType":"target" + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"331289:0:0|2,331289:0:0|32767,331289:0:0|1" + } + } + } + ] + } + } + """ + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "Init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Analytics.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 1) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.clientCode": "acopprod3", + "target.server": "", + "analytics.server": "test.analytics.net", + "analytics.rsids": "abc", + "analytics.batchLimit": 0, + "analytics.aamForwardingEnabled": false, + "analytics.backdatePreviousSessionInfo": false, + "analytics.offlineEnabled": false, + "analytics.launchHitDelay": 0, + ]) + + Analytics.clearQueue() + sleep(2) + + let notificationExpectation = XCTestExpectation(description: "clickedLocation should send click notification to Target and hit containing a4t payload to Analytics.") + notificationExpectation.expectedFulfillmentCount = 2 + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + + if request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + notificationExpectation.fulfill() + return nil + } + + if request.url.absoluteString.contains("https://test.analytics.net/b/ss/abc/0") { + notificationExpectation.fulfill() + return (data: nil, + response: HTTPURLResponse(url: URL(string: "https://test.analytics.net/b/ss/abc/0")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil), + error: nil) + } + return nil + } + + let prefetchExpectation = XCTestExpectation(description: "prefetchContent should prefetch content without error.") + Target.prefetchContent([ + TargetPrefetch(name: "mboxName1", targetParameters: nil), + TargetPrefetch(name: "mboxName2", targetParameters: nil), + ]) { error in + if let error = error { + Log.error(label: self.T_LOG_TAG, "Target.prefetchContent - failed, error: \(String(describing: error))") + XCTFail("Target.prefetchContent - failed, error: \(String(describing: error))") + return + } + prefetchExpectation.fulfill() + } + wait(for: [prefetchExpectation], timeout: 1) + + // In real customer scenario, there will be a display location notification request before click. + + Target.clickedLocation( + "mboxName1", + targetParameters: TargetParameters( + parameters: nil, + profileParameters: nil, + order: TargetOrder(id: "ADCKKBC", total: 400.50, purchasedProductIds: ["34", "125"]), + product: TargetProduct(productId: "24D334", categoryId: "Stationary") + ) + ) + wait(for: [notificationExpectation], timeout: 2) + } + + func testClickedLocation_withA4TClickMetricForLoadedMbox() { + let responseString = """ + { + "status":200, + "requestId":"bce9dfed-5caf-41e7-9926-d84947c874fa", + "client":"acopprod3", + "id":{ + "tntId":"55508C70-F530-4E33-AAD2-F09BB99C5C3E.35_0", + "marketingCloudVisitorId":"32943535451574954856183504879211787972" + }, + "edgeHost":"mboxedge35.tt.omtrdc.net", + "execute":{ + "mboxes":[ + { + "index":0, + "name":"mboxName1", + "options":[ + { + "content":"someContent1", + "type":"html", + "sourceType":"target" + } + ], + "metrics":[ + { + "type":"click", + "eventToken":"ABPi/uih7s0vo6/8kqyxjA==", + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"409277:0:0|32767|1" + } + } + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"409277:0:0|2|1,409277:0:0|1|1" + } + } + }, + { + "index":1, + "name":"mboxName2", + "options":[ + { + "content":"someContent2", + "type":"html", + "sourceType":"target" + } + ], + "analytics":{ + "payload":{ + "pe":"tnt", + "tnta":"331289:0:0|2|1,331289:0:0|32767|1,331289:0:0|1|1" + } + } + } + ] + } + } + """ + + // init mobile SDK, register extensions + let initExpectation = XCTestExpectation(description: "init extensions") + MobileCore.setLogLevel(.trace) + MobileCore.registerExtensions([Target.self, Analytics.self, Identity.self, Lifecycle.self]) { + initExpectation.fulfill() + } + wait(for: [initExpectation], timeout: 2) + + // update the configuration's shared state + MobileCore.updateConfigurationWith(configDict: [ + "experienceCloud.org": "orgid", + "experienceCloud.server": "test.com", + "global.privacy": "optedin", + "target.server": "", + "target.clientCode": "acopprod3", + "analytics.server": "test.analytics.net", + "analytics.rsids": "abc", + "analytics.batchLimit": 0, + "analytics.aamForwardingEnabled": false, + "analytics.backdatePreviousSessionInfo": false, + "analytics.offlineEnabled": false, + "analytics.launchHitDelay": 0, + ]) + + Analytics.clearQueue() + sleep(2) + + let notificationExpectation = XCTestExpectation(description: "clickedLocation should send click notification to Target and hit containing a4t payload to Analytics.") + notificationExpectation.expectedFulfillmentCount = 2 + let mockNetworkService = TestableNetworkService() + ServiceProvider.shared.networkService = mockNetworkService + mockNetworkService.mock { request in + if request.url.absoluteString.contains("https://acopprod3.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + let validResponse = HTTPURLResponse(url: URL(string: "https://acopprod3.tt.omtrdc.net/rest/v1/delivery")!, statusCode: 200, httpVersion: nil, headerFields: nil) + + return (data: responseString.data(using: .utf8), response: validResponse, error: nil) + } + + if request.url.absoluteString.contains("https://mboxedge35.tt.omtrdc.net/rest/v1/delivery/?client=acopprod3&sessionId=") { + notificationExpectation.fulfill() + return nil + } + + if request.url.absoluteString.contains("https://test.analytics.net/b/ss/abc/0") { + notificationExpectation.fulfill() + + return (data: nil, + response: HTTPURLResponse(url: URL(string: "https://test.analytics.net/b/ss/abc/0")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil), + error: nil) + } + return nil + } + + let retrieveExpectation = XCTestExpectation(description: "retrieveLocationContent should return content and data from Target.") + retrieveExpectation.expectedFulfillmentCount = 2 + retrieveExpectation.assertForOverFulfill = true + + Target.retrieveLocationContent([ + TargetRequest(mboxName: "mboxName1", defaultContent: "DefaultContent1") { content, data in + XCTAssertEqual("someContent1", content) + + guard let data = data else { + XCTFail("Data containing A4T payload should be valid.") + return + } + XCTAssertEqual(2, data.count) + + guard let a4tPayload = data["analytics.payload"] as? [String: String] else { + XCTFail("Analytics payload should be present.") + return + } + XCTAssertEqual(2, a4tPayload.count) + XCTAssertEqual("tnt", a4tPayload["pe"]) + XCTAssertEqual("409277:0:0|2|1,409277:0:0|1|1", a4tPayload["tnta"]) + + guard let clickMetricA4tPayload = data["clickmetric.analytics.payload"] as? [String: String] else { + XCTFail("Click metric Analytics payload should be present.") + return + } + XCTAssertEqual(2, clickMetricA4tPayload.count) + XCTAssertEqual("tnt", clickMetricA4tPayload["pe"]) + XCTAssertEqual("409277:0:0|32767|1", clickMetricA4tPayload["tnta"]) + + retrieveExpectation.fulfill() + }, + TargetRequest(mboxName: "mboxName2", defaultContent: "DefaultContent2") { content, data in + XCTAssertEqual("someContent2", content) + + guard let data = data else { + XCTFail("Data containing A4T payload should be valid.") + return + } + XCTAssertEqual(1, data.count) + + guard let a4tPayload = data["analytics.payload"] as? [String: String] else { + XCTFail("Analytics payload should be present.") + return + } + XCTAssertEqual(2, a4tPayload.count) + XCTAssertEqual("tnt", a4tPayload["pe"]) + XCTAssertEqual("331289:0:0|2|1,331289:0:0|32767|1,331289:0:0|1|1", a4tPayload["tnta"]) + + retrieveExpectation.fulfill() + }, + ], + with: nil) + wait(for: [retrieveExpectation], timeout: 1) + + Target.clickedLocation( + "mboxName1", + targetParameters: TargetParameters( + parameters: nil, + profileParameters: nil, + order: TargetOrder(id: "ADCKKBC", total: 400.50, purchasedProductIds: ["34", "125"]), + product: TargetProduct(productId: "24D334", categoryId: "Stationary") + ) + ) + wait(for: [notificationExpectation], timeout: 2) + } } diff --git a/AEPTarget/Tests/TestHelpers/FileManager+clear.swift b/AEPTarget/Tests/TestHelpers/FileManager+clear.swift index 9b71d58..9d8394c 100644 --- a/AEPTarget/Tests/TestHelpers/FileManager+clear.swift +++ b/AEPTarget/Tests/TestHelpers/FileManager+clear.swift @@ -20,12 +20,6 @@ extension FileManager { print("ERROR DESCRIPTION: \(error)") } - do { - try removeItem(at: URL(fileURLWithPath: "Library/Caches/com.adobe.module.edge")) - } catch { - print("ERROR DESCRIPTION: \(error)") - } - do { try removeItem(at: URL(fileURLWithPath: "Library/Caches/com.adobe.module.identity")) } catch { diff --git a/AEPTarget/Tests/TestHelpers/MockUIService.swift b/AEPTarget/Tests/TestHelpers/MockUIService.swift index 8223a47..b41bf42 100644 --- a/AEPTarget/Tests/TestHelpers/MockUIService.swift +++ b/AEPTarget/Tests/TestHelpers/MockUIService.swift @@ -35,6 +35,16 @@ class MockFloatingButton: FloatingButtonPresentable { func dismiss() { dismissCalled = true } + + var setButtonImageCalled = false + func setButtonImage(imageData _: Data) { + setButtonImageCalled = true + } + + var setInitialCalled = false + func setInitial(position _: FloatingButtonPosition) { + setInitialCalled = true + } } class MockUIService: UIService { diff --git a/AEPTarget/Tests/UnitTests/DeliveryRequestBuilderTests.swift b/AEPTarget/Tests/UnitTests/DeliveryRequestBuilderTests.swift index 3ec171f..da356a0 100644 --- a/AEPTarget/Tests/UnitTests/DeliveryRequestBuilderTests.swift +++ b/AEPTarget/Tests/UnitTests/DeliveryRequestBuilderTests.swift @@ -312,8 +312,8 @@ class DeliveryRequestBuilderTests: XCTestCase { "a.DaysSinceLastUse": "0", "a.locale": "en-US", ], - targetRequestArray: [TargetRequest(mboxName: "Drink_1", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"])), - TargetRequest(mboxName: "Drink_2", defaultContent: "default2", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]))], + targetRequestArray: [TargetRequest(mboxName: "Drink_1", defaultContent: "default", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil), + TargetRequest(mboxName: "Drink_2", defaultContent: "default2", targetParameters: TargetParameters(profileParameters: ["mbox-parameter-key1": "mbox-parameter-value1"]), contentCallback: nil)], targetParameters: TargetParameters(profileParameters: ["name": "Smith"]) ) @@ -596,6 +596,10 @@ private class MockedSystemInfoService: SystemInfoService { "My iPhone" } + func getDeviceModelNumber() -> String { + "" + } + func getMobileCarrierName() -> String? { "" } diff --git a/AEPTarget/Tests/UnitTests/Event+TargetTests.swift b/AEPTarget/Tests/UnitTests/Event+TargetTests.swift index 0a8acaf..d1c7fe3 100644 --- a/AEPTarget/Tests/UnitTests/Event+TargetTests.swift +++ b/AEPTarget/Tests/UnitTests/Event+TargetTests.swift @@ -45,8 +45,8 @@ class TargetEventTests: XCTestCase { } func testBatchRequestObjectArray() throws { - let requestDict_1 = TargetRequest(mboxName: "request_1", defaultContent: mockDefaultContent_1, targetParameters: mockTargetParameter_1).asDictionary() - let requestDict_2 = TargetRequest(mboxName: "request_2", defaultContent: mockDefaultContent_2, targetParameters: mockTargetParameter_2).asDictionary() + let requestDict_1 = TargetRequest(mboxName: "request_1", defaultContent: mockDefaultContent_1, targetParameters: mockTargetParameter_1, contentCallback: nil).asDictionary() + let requestDict_2 = TargetRequest(mboxName: "request_2", defaultContent: mockDefaultContent_2, targetParameters: mockTargetParameter_2, contentCallback: nil).asDictionary() let eventData = [TargetConstants.EventDataKeys.LOAD_REQUESTS: [requestDict_1, requestDict_2]] let event = Event(name: TargetConstants.EventName.LOAD_REQUEST, type: EventType.target, source: EventSource.requestContent, data: eventData as [String: Any]) guard let array: [TargetRequest] = event.targetRequests else { diff --git a/AEPTarget/Tests/UnitTests/Target+PublicAPITests.swift b/AEPTarget/Tests/UnitTests/Target+PublicAPITests.swift index c3362a0..3f68581 100644 --- a/AEPTarget/Tests/UnitTests/Target+PublicAPITests.swift +++ b/AEPTarget/Tests/UnitTests/Target+PublicAPITests.swift @@ -16,9 +16,16 @@ import XCTest class TargetPublicAPITests: XCTestCase { override func setUp() { - MockExtension.reset() + // Put setup code here. This method is called before the invocation of each test method in the class. EventHub.shared.start() registerMockExtension(MockExtension.self) + Target.isResponseListenerRegister = false + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + MockExtension.reset() + EventHub.reset() } private func registerMockExtension(_ type: T.Type) { @@ -94,6 +101,7 @@ class TargetPublicAPITests: XCTestCase { XCTAssertEqual("unexpected error", error.description) expectation.fulfill() } + wait(for: [expectation], timeout: 1) } func testLocationDisplayed() throws { @@ -123,29 +131,30 @@ class TargetPublicAPITests: XCTestCase { } func testLocationDisplayed_withEmptyMboxName() throws { - var dispatchedEvent = false + let expectation = XCTestExpectation(description: "displayedLocations should not dispatch an event for empty mboxes array.") + expectation.isInverted = true + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() - EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in - if let eventData = event.data, let _ = eventData[TargetConstants.EventDataKeys.MBOX_NAMES] as? [String] { - dispatchedEvent = true - } + EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { _ in + + expectation.fulfill() } Target.displayedLocations([], targetParameters: nil) - XCTAssertFalse(dispatchedEvent) + wait(for: [expectation], timeout: 1) } func testLocationClicked_withEmptyMboxName() throws { - var dispatchedEvent = false + let expectation = XCTestExpectation(description: "clickedLocation should not dispatch an event for empty mbox name.") + expectation.isInverted = true + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() - EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in - if let eventData = event.data, let _ = eventData[TargetConstants.EventDataKeys.MBOX_NAME] as? [String] { - dispatchedEvent = true - } + EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { _ in + expectation.fulfill() } Target.clickedLocation("", targetParameters: nil) - XCTAssertFalse(dispatchedEvent) + wait(for: [expectation], timeout: 1) } func testLocationClicked() throws { @@ -292,85 +301,188 @@ class TargetPublicAPITests: XCTestCase { wait(for: [expectation], timeout: 1) } - func test_retrieveLocationContent_withEmptyArray() throws { - var dispatchedRetrieveEvent = false - EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in - if let eventData = event.data, let _ = eventData[TargetConstants.EventDataKeys.LOAD_REQUESTS] { - dispatchedRetrieveEvent = true - } + func testRetrieveLocationContent_withEmptyArray() throws { + let expectation = XCTestExpectation(description: "retrieveLocationContent should not dispatch an event for empty mboxes request array.") + expectation.isInverted = true + + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() + EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { _ in + expectation.fulfill() } Target.retrieveLocationContent([]) - XCTAssertFalse(dispatchedRetrieveEvent) + wait(for: [expectation], timeout: 1) } - func test_retrieveLocationContent_withEmptyMboxName() throws { - let expectation = XCTestExpectation(description: "retrieveLocationContent should return default content if the given mbox name is empty") - var dispatchedRetrieveEvent = false - let request = TargetRequest(mboxName: "", defaultContent: "DefaultValue", targetParameters: nil, contentCallback: { content in - XCTAssertTrue(content == "DefaultValue") + func testRetrieveLocationContent_withEmptyMboxName() throws { + let expectation = XCTestExpectation(description: "retrieveLocationContent should return default content if the given mbox name is empty.") + let retrieveExpectation = XCTestExpectation(description: "retrieveLocationContent should not dispatch an event for empty mbox name.") + retrieveExpectation.isInverted = true + + let request1 = TargetRequest(mboxName: "", defaultContent: "DefaultValue", targetParameters: nil, contentCallback: { content in + XCTAssertEqual("DefaultValue", content) expectation.fulfill() }) - EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in - if let eventData = event.data, let _ = eventData[TargetConstants.EventDataKeys.LOAD_REQUESTS] { - dispatchedRetrieveEvent = true - } - } + let request2 = TargetRequest(mboxName: "", defaultContent: "DefaultValue2", targetParameters: nil, contentWithDataCallback: { content, data in + XCTAssertEqual("DefaultValue2", content) + XCTAssertNil(data) + expectation.fulfill() + }) - Target.retrieveLocationContent([request]) + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() + EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { _ in + retrieveExpectation.fulfill() + } - wait(for: [expectation], timeout: 1) - XCTAssertFalse(dispatchedRetrieveEvent) + Target.retrieveLocationContent([request1, request2]) + wait(for: [expectation, retrieveExpectation], timeout: 1) } - func test_retrieveLocationContent() throws { - let expectation1 = XCTestExpectation(description: "retrieveLocationContent should dispatch an event with someContent") - let expectation2 = XCTestExpectation(description: "retrieveLocationContent should dispatch an event with someContent2") + func testRetrieveLocationContent() throws { + let expectation = XCTestExpectation(description: "retrieveLocationContent should invoke the request callbacks for the given mboxes.") + expectation.expectedFulfillmentCount = 2 + expectation.assertForOverFulfill = true // Mocks let tr1 = TargetRequest(mboxName: "Drink_1", defaultContent: "DefaultValue", targetParameters: nil, contentCallback: { content in - XCTAssertTrue(content == "someContent") - expectation1.fulfill() + XCTAssertEqual("someContent", content) + expectation.fulfill() }) let pairId1 = tr1.responsePairId + let tr2 = TargetRequest(mboxName: "Drink_2", defaultContent: "DefaultValue2", targetParameters: nil, contentCallback: { content in - XCTAssertTrue(content == "someContent2") - expectation2.fulfill() + XCTAssertEqual("someContent2", content) + expectation.fulfill() }) let pairId2 = tr2.responsePairId - expectation1.assertForOverFulfill = true - expectation2.assertForOverFulfill = true + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in - guard let eventData = event.data, let requests = TargetRequest.from(dictionaries: eventData[TargetConstants.EventDataKeys.LOAD_REQUESTS] as? [[String: Any]]), let parameters = TargetParameters.from(dictionary: eventData[TargetConstants.EventDataKeys.TARGET_PARAMETERS] as? [String: Any]) + guard + let eventData = event.data, + let requests = TargetRequest.from(dictionaries: eventData["request"] as? [[String: Any]]), + let parameters = TargetParameters.from(dictionary: eventData["targetparams"] as? [String: Any]) else { - XCTFail() - expectation1.fulfill() - expectation2.fulfill() + XCTFail("Event should have a valid target retrieve location content data.") + expectation.fulfill() + expectation.fulfill() return } XCTAssertEqual(2, requests.count) - XCTAssertTrue([requests[0].name, requests[1].name].contains("Drink_1")) - XCTAssertTrue([requests[0].name, requests[1].name].contains("Drink_2")) - XCTAssertTrue([requests[0].defaultContent, requests[1].defaultContent].contains("DefaultValue")) - XCTAssertTrue([requests[0].defaultContent, requests[1].defaultContent].contains("DefaultValue2")) - XCTAssertNotNil(requests[0].responsePairId) - XCTAssertNotNil(requests[1].responsePairId) + XCTAssertEqual("Drink_1", requests[0].name) + XCTAssertEqual("DefaultValue", requests[0].defaultContent) + XCTAssertEqual(pairId1, requests[0].responsePairId) + XCTAssertEqual("Drink_2", requests[1].name) + XCTAssertEqual("DefaultValue2", requests[1].defaultContent) + XCTAssertEqual(pairId2, requests[1].responsePairId) + + XCTAssertNotNil(parameters.profileParameters) + XCTAssertEqual(1, parameters.profileParameters?.count) XCTAssertEqual("Smith", parameters.profileParameters?["name"]) + XCTAssertNotNil(parameters.parameters) + XCTAssertEqual(1, parameters.parameters?.count) + XCTAssertEqual("mbox_parameter_value", parameters.parameters?["mbox_parameter_key"]) + + let event1 = Event(name: "TargetRequestResponse", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.responseContent", + data: [ + "content": "someContent", + "responsePairId": pairId1, + "responseEventId": event.id.uuidString, + ]) + EventHub.shared.dispatch(event: event1) + + let event2 = Event(name: "TargetRequestResponse", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.responseContent", + data: [ + "content": "someContent2", + "responsePairId": pairId2, + "responseEventId": event.id.uuidString, + ]) + EventHub.shared.dispatch(event: event2) + } - EventHub.shared.dispatch(event: event.createResponseEvent(name: TargetConstants.EventName.TARGET_REQUEST_RESPONSE, type: EventType.target, source: EventSource.responseContent, data: [TargetConstants.EventDataKeys.TARGET_CONTENT: "someContent", TargetConstants.EventDataKeys.TARGET_RESPONSE_PAIR_ID: pairId1])) + Target.retrieveLocationContent([tr1, tr2], with: TargetParameters(parameters: ["mbox_parameter_key": "mbox_parameter_value"], profileParameters: ["name": "Smith"])) + + wait(for: [expectation], timeout: 2) + } + + func testRetrieveLocationContent_contentWithDataCallback() throws { + let expectation = XCTestExpectation(description: "retrieveLocationContent should invoke the request callbacks for the given mboxes.") + expectation.expectedFulfillmentCount = 2 + expectation.assertForOverFulfill = true - EventHub.shared.dispatch(event: event.createResponseEvent(name: TargetConstants.EventName.TARGET_REQUEST_RESPONSE, type: EventType.target, source: EventSource.responseContent, data: [TargetConstants.EventDataKeys.TARGET_CONTENT: "someContent2", TargetConstants.EventDataKeys.TARGET_RESPONSE_PAIR_ID: pairId2])) - expectation1.fulfill() - expectation2.fulfill() + // Mocks + let tr1 = TargetRequest(mboxName: "Drink_1", defaultContent: "DefaultValue", targetParameters: nil, contentWithDataCallback: { content, data in + XCTAssertEqual("someContent", content) + XCTAssertNil(data) + expectation.fulfill() + }) + let pairId1 = tr1.responsePairId + let tr2 = TargetRequest(mboxName: "Drink_2", defaultContent: "DefaultValue2", targetParameters: nil) { content, data in + XCTAssertEqual("someContent2", content) + XCTAssertNil(data) + expectation.fulfill() + } + let pairId2 = tr2.responsePairId + + EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() + EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: EventType.target, source: EventSource.requestContent) { event in + guard + let eventData = event.data, + let requests = TargetRequest.from(dictionaries: eventData["request"] as? [[String: Any]]), + let parameters = TargetParameters.from(dictionary: eventData["targetparams"] as? [String: Any]) + else { + XCTFail("Event should have a valid target retrieve location content data.") + expectation.fulfill() + expectation.fulfill() + return + } + + XCTAssertEqual(2, requests.count) + XCTAssertEqual("Drink_1", requests[0].name) + XCTAssertEqual("DefaultValue", requests[0].defaultContent) + XCTAssertEqual(pairId1, requests[0].responsePairId) + XCTAssertEqual("Drink_2", requests[1].name) + XCTAssertEqual("DefaultValue2", requests[1].defaultContent) + XCTAssertEqual(pairId2, requests[1].responsePairId) + + XCTAssertNotNil(parameters.profileParameters) + XCTAssertEqual(1, parameters.profileParameters?.count) + XCTAssertEqual("Smith", parameters.profileParameters?["name"]) + XCTAssertNotNil(parameters.parameters) + XCTAssertEqual(1, parameters.parameters?.count) + XCTAssertEqual("mbox_parameter_value", parameters.parameters?["mbox_parameter_key"]) + + let event1 = Event(name: "TargetRequestResponse", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.responseContent", + data: [ + "content": "someContent", + "responsePairId": pairId1, + "responseEventId": event.id.uuidString, + ]) + EventHub.shared.dispatch(event: event1) + + let event2 = Event(name: "TargetRequestResponse", + type: "com.adobe.eventType.target", + source: "com.adobe.eventSource.responseContent", + data: [ + "content": "someContent2", + "responsePairId": pairId2, + "responseEventId": event.id.uuidString, + ]) + EventHub.shared.dispatch(event: event2) } Target.retrieveLocationContent([tr1, tr2], with: TargetParameters(parameters: ["mbox_parameter_key": "mbox_parameter_value"], profileParameters: ["name": "Smith"])) - wait(for: [expectation1, expectation2], timeout: 1) + wait(for: [expectation], timeout: 2) } - func test_clearPrefetchCache() { + func testClearPrefetchCache() { let expectation = XCTestExpectation(description: "Should dispatch a clearPrefetchCache event") EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: "com.adobe.eventType.target", source: "com.adobe.eventSource.requestReset") { event in @@ -387,7 +499,7 @@ class TargetPublicAPITests: XCTestCase { wait(for: [expectation], timeout: 1) } - func test_setPreviewRestartDeepLink() { + func testSetPreviewRestartDeepLink() { let expectation = XCTestExpectation(description: "Should dispatch a setPreviewRestartDeepLink event") EventHub.shared.getExtensionContainer(MockExtension.self)?.eventListeners.clear() EventHub.shared.getExtensionContainer(MockExtension.self)?.registerListener(type: "com.adobe.eventType.target", source: "com.adobe.eventSource.requestContent") { event in diff --git a/AEPTargetDemoApp/AppDelegate.swift b/AEPTargetDemoApp/AppDelegate.swift index ceca7e5..3e99ad9 100644 --- a/AEPTargetDemoApp/AppDelegate.swift +++ b/AEPTargetDemoApp/AppDelegate.swift @@ -10,7 +10,6 @@ governing permissions and limitations under the License. */ -import ACPCore import AEPAnalytics import AEPAssurance import AEPCore @@ -26,15 +25,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. MobileCore.setLogLevel(.trace) - MobileCore.registerExtensions([Lifecycle.self, Identity.self, Target.self, Signal.self, Analytics.self]) + MobileCore.registerExtensions([Lifecycle.self, Identity.self, Target.self, Signal.self, Analytics.self, Assurance.self]) MobileCore.configureWith(appId: TestConstants.LAUNCH_ID) MobileCore.updateConfigurationWith(configDict: ["target.previewEnabled": true]) - // register AEPAssurance - AEPAssurance.registerExtension() - // need to call `ACPCore.start` in order to get ACP* extensions registered to AEPCore - ACPCore.start {} - return true } diff --git a/AEPTargetDemoApp/ContentView.swift b/AEPTargetDemoApp/ContentView.swift index e050a27..ab907a9 100644 --- a/AEPTargetDemoApp/ContentView.swift +++ b/AEPTargetDemoApp/ContentView.swift @@ -24,37 +24,43 @@ struct ContentView: View { var body: some View { ScrollView { VStack(alignment: .center, spacing: nil, content: { - TextField("Griffon URL", text: $griffonUrl).multilineTextAlignment(.center) - Button("Connect to griffon") { - startGriffon() - }.padding(10) + Group { + TextField("Griffon URL", text: $griffonUrl).multilineTextAlignment(.center) + Button("Connect to griffon") { + startGriffon() + }.padding(10) - Button("Prefetch") { - prefetch() - }.padding(10) + Button("Prefetch") { + prefetch() + }.padding(10) - Button("GetLocations") { - getLocations() - }.padding(10) + Button("GetLocations (using contentCallback)") { + getLocations1() + }.padding(10) - Button("Locations displayed") { - locationDisplayed() - }.padding(10) + Button("GetLocations (using contentWithDataCallback)") { + getLocations2() + }.padding(10) - Button("Location clicked") { - locationClicked() - }.padding(10) + Button("Locations displayed") { + locationDisplayed() + }.padding(10) - Button("Reset Experience") { - resetExperience() - }.padding(10) + Button("Location clicked") { + locationClicked() + }.padding(10) - Text("Third Party ID - \(thirdPartyId)") - Button("Get Third Party Id") { - getThirdPartyId() - }.padding(10) + Button("Reset Experience") { + resetExperience() + }.padding(10) + } Group { + Text("Third Party ID - \(thirdPartyId)") + Button("Get Third Party Id") { + getThirdPartyId() + }.padding(10) + Text("Tnt id - \(tntId)") Button("Get Tnt Id") { getTntId() @@ -66,7 +72,7 @@ struct ContentView: View { }.padding(10) Button("Clear prefetch cache") { - setThirdPartyId() + clearPrefetchCache() }.padding(10) Button("Enter Preview") { @@ -79,7 +85,7 @@ struct ContentView: View { func startGriffon() { if let url = URL(string: griffonUrl) { - AEPAssurance.startSession(url) + Assurance.startSession(url: url) } } @@ -93,22 +99,54 @@ struct ContentView: View { ) } - func getLocations() { - Target.retrieveLocationContent([TargetRequest(mboxName: "aep-loc-1", defaultContent: "DefaultValue", targetParameters: nil, contentCallback: { content in + func getLocations1() { + Target.retrieveLocationContent([TargetRequest(mboxName: "aep-loc-1", defaultContent: "DefaultValue1", targetParameters: nil, contentCallback: { content in print("------") - print(content ?? "") + print("Content: \(content ?? "")") }), TargetRequest(mboxName: "aep-loc-2", defaultContent: "DefaultValue2", targetParameters: nil, contentCallback: { content in print("------") - print(content ?? "") + print("Content: \(content ?? "")") }), TargetRequest(mboxName: "aep-loc-x", defaultContent: "DefaultValuex", targetParameters: nil, contentCallback: { content in print("------") - print(content ?? "") + print("Content: \(content ?? "")") })], with: TargetParameters(parameters: ["mbox_parameter_key": "mbox_parameter_value"], profileParameters: ["name": "Smith"], order: TargetOrder(id: "id1", total: 1.0, purchasedProductIds: ["ppId1"]), product: TargetProduct(productId: "pId1", categoryId: "cId1"))) } + func getLocations2() { + Target.retrieveLocationContent([ + TargetRequest(mboxName: "aep-loc-1", defaultContent: "DefaultValue1", targetParameters: nil, contentWithDataCallback: { content, data in + print("------") + print("Content: \(content ?? "")") + + let responseTokens = data?["responseTokens"] as? [String: String] ?? [:] + print("Response tokens: \(responseTokens as AnyObject)") + + let analyticsPayload = data?["analytics.payload"] as? [String: String] ?? [:] + print("Analytics payload: \(analyticsPayload as AnyObject)") + + let clickAnalyticsPayload = data?["clickmetric.analytics.payload"] as? [String: String] ?? [:] + print("Metrics Analytics payload (click): \(clickAnalyticsPayload as AnyObject)") + }), + TargetRequest(mboxName: "aep-loc-2", defaultContent: "DefaultValue2", targetParameters: nil, contentWithDataCallback: { content, data in + print("------") + print("Content: \(content ?? "")") + + let responseTokens = data?["responseTokens"] as? [String: String] ?? [:] + print("Response tokens: \(responseTokens as AnyObject)") + + let analyticsPayload = data?["analytics.payload"] as? [String: String] ?? [:] + print("Analytics payload: \(analyticsPayload as AnyObject)") + + let clickAnalyticsPayload = data?["clickmetric.analytics.payload"] as? [String: String] ?? [:] + print("Metrics Analytics payload (click): \(clickAnalyticsPayload as AnyObject)") + }), + ], + with: TargetParameters(parameters: ["mbox_parameter_key": "mbox_parameter_value"], profileParameters: ["name": "Smith"], order: TargetOrder(id: "id1", total: 1.0, purchasedProductIds: ["ppId1"]), product: TargetProduct(productId: "pId1", categoryId: "cId1"))) + } + func locationDisplayed() { Target.displayedLocations(["aep-loc-1", "aep-loc-2"], targetParameters: TargetParameters(parameters: ["mbox_parameter_key": "mbox_parameter_value"], profileParameters: ["name": "Smith"], order: TargetOrder(id: "id1", total: 1.0, purchasedProductIds: ["ppId1"]), product: TargetProduct(productId: "pId1", categoryId: "cId1"))) } diff --git a/AEPTargetDemoObjCApp/AppDelegate.h b/AEPTargetDemoObjCApp/AppDelegate.h index e033a29..de4ca76 100644 --- a/AEPTargetDemoObjCApp/AppDelegate.h +++ b/AEPTargetDemoObjCApp/AppDelegate.h @@ -18,7 +18,6 @@ @import AEPServices; @import AEPTarget; @import AEPAssurance; -@import ACPCore; @import AEPAnalytics; @interface AppDelegate : UIResponder diff --git a/AEPTargetDemoObjCApp/AppDelegate.m b/AEPTargetDemoObjCApp/AppDelegate.m index 329c8f2..9591392 100644 --- a/AEPTargetDemoObjCApp/AppDelegate.m +++ b/AEPTargetDemoObjCApp/AppDelegate.m @@ -23,7 +23,7 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [AEPMobileCore setLogLevel: AEPLogLevelTrace]; - NSArray *extensionsToRegister = @[AEPMobileIdentity.class, AEPMobileLifecycle.class, AEPMobileTarget.class, AEPMobileAnalytics.class]; + NSArray *extensionsToRegister = @[AEPMobileIdentity.class, AEPMobileLifecycle.class, AEPMobileTarget.class, AEPMobileAnalytics.class, AEPMobileAssurance.class]; [AEPMobileCore registerExtensions:extensionsToRegister completion:^{ [AEPMobileCore lifecycleStart:@{@"contextDataKey": @"contextDataVal"}]; }]; @@ -31,11 +31,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Use the App id assigned to this application via Adobe Launch [AEPMobileCore configureWithAppId: @""]; [AEPMobileCore updateConfiguration:@{@"target.previewEnabled": @YES}]; - - [AEPAssurance registerExtension]; - [ACPCore start:^{ - - }]; return YES; } diff --git a/AEPTargetDemoObjCApp/Base.lproj/Main.storyboard b/AEPTargetDemoObjCApp/Base.lproj/Main.storyboard index 87795e3..5c57425 100644 --- a/AEPTargetDemoObjCApp/Base.lproj/Main.storyboard +++ b/AEPTargetDemoObjCApp/Base.lproj/Main.storyboard @@ -17,24 +17,49 @@ + + - + - - - + + - - - - + + - - - - + - - + + + + + + + + + + + + - - + + + + + + + + + + + - + + - - - + + + + + + diff --git a/AEPTargetDemoObjCApp/ViewController.m b/AEPTargetDemoObjCApp/ViewController.m index e9598bc..bf94283 100644 --- a/AEPTargetDemoObjCApp/ViewController.m +++ b/AEPTargetDemoObjCApp/ViewController.m @@ -36,7 +36,7 @@ - (IBAction)prefetchClicked:(id)sender { }]; } -- (IBAction)loadRequest:(id)sender { +- (IBAction)loadRequestWithContent:(id)sender { AEPTargetRequestObject *request1 = [[AEPTargetRequestObject alloc] initWithMboxName:@"aep-loc-1" defaultContent:@"defaultContent" targetParameters:nil contentCallback:^(NSString * _Nullable content) { NSLog(@"Content is >> %@", content ?: @"nope"); }]; @@ -46,6 +46,40 @@ - (IBAction)loadRequest:(id)sender { [AEPMobileTarget retrieveLocationContent:@[request1, request2] withParameters:nil]; } +- (IBAction)loadRequestWithContentAndData:(id)sender { + AEPTargetRequestObject *request1 = [[AEPTargetRequestObject alloc] initWithMboxName:@"aep-loc-1" defaultContent:@"defaultContent" targetParameters:nil contentWithDataCallback:^(NSString * _Nullable content, NSDictionary * _Nullable data) { + NSLog(@"Content is >> %@", content ?: @"nope"); + + if ([data objectForKey:@"responseTokens"]) { + NSLog(@"Response Tokens are >> %@", data[@"responseTokens"]); + } + + if ([data objectForKey:@"analytics.payload"]) { + NSLog(@"Analytics payload is >> %@", data[@"analytics.payload"]); + } + + if ([data objectForKey:@"clickmetric.analytics.payload"]) { + NSLog(@"Click tracking Analytics payload is >> %@", data[@"clickmetric.analytics.payload"]); + } + }]; + AEPTargetRequestObject *request2 = [[AEPTargetRequestObject alloc] initWithMboxName:@"aep-loc-2" defaultContent:@"defaultContent2" targetParameters:nil contentWithDataCallback:^(NSString * _Nullable content, NSDictionary * _Nullable data) { + NSLog(@"Content is >> %@", content ?: @"nope"); + + if ([data objectForKey:@"responseTokens"]) { + NSLog(@"Response Tokens are >> %@", data[@"responseTokens"]); + } + + if ([data objectForKey:@"analytics.payload"]) { + NSLog(@"Analytics payload is >> %@", data[@"analyticspayload"]); + } + + if ([data objectForKey:@"clickmetric.analytics.payload"]) { + NSLog(@"Click tracking Analytics payload is >> %@", data[@"clickmetric.analytics.payload"]); + } + }]; + [AEPMobileTarget retrieveLocationContent:@[request1, request2] withParameters:nil]; +} + - (IBAction)locationDisplayedClicked:(id)sender { AEPTargetOrder *order = [[AEPTargetOrder alloc] initWithId:@"id1" total:1.0 purchasedProductIds:@[@"ppId1"]]; AEPTargetProduct *product =[[AEPTargetProduct alloc] initWithProductId:@"pId1" categoryId:@"cId1"]; @@ -93,7 +127,7 @@ - (IBAction)setThirdPartyClicked:(id)sender { - (IBAction)startGriffon:(id)sender { if(![_griffonUrl.text isEqualToString:@""]) { - [AEPAssurance startSession:[NSURL URLWithString:_griffonUrl.text]]; + [AEPMobileAssurance startSessionWithUrl:[NSURL URLWithString:_griffonUrl.text]]; } } diff --git a/Documentation/AEPTarget.md b/Documentation/AEPTarget.md index abc2815..3008b2d 100644 --- a/Documentation/AEPTarget.md +++ b/Documentation/AEPTarget.md @@ -191,6 +191,12 @@ This API sends a batch request to the configured Target server for multiple mbox For mbox locations in the Target requests list that are not already prefetched, this API sends a batch request to the configured Target server. The content for the mbox locations that have been prefetched in a previous request are returned from the SDK, and no additional network request is made. Each Target request object in the list contains a callback function, which is invoked when content is available for its given mbox location. +When using `contentWithData` callback to instantiate TargetRequest object, the following keys can be used to read response tokens and Analytics for Target (A4T) info from the data payload if available in the Target response. + + - responseTokens (Response tokens) + - analytics.payload (A4T payload) + - clickmetric.analytics.payload (Click tracking A4T payload) + ### Swift #### Syntax @@ -226,11 +232,25 @@ static func retrieveLocationContent(_ requestArray: [TargetRequest], with target product: TargetProduct(productId: "24D334", categoryId: "Stationary") ) - let request1 = TargetRequest(mboxName: "logo", defaultContent: "BlueWhale", targetParameters: TargetParameters1) { _ in - // do something with the received content + let request1 = TargetRequest(mboxName: "logo", defaultContent: "BlueWhale", targetParameters: TargetParameters1) { content in + if let content = content { + // do something with the target content. + } } - let request2 = TargetRequest(mboxName: "logo", defaultContent: "red", targetParameters: TargetParameters2) { _ in - // do something with the received content + let request2 = TargetRequest(mboxName: "logo", defaultContent: "red", targetParameters: TargetParameters2) { content, data in + if let content = content { + // do something with the target content. + } + + // Read the data dictionary containing one or more of response tokens, analytics payload and click-tracking analytics payload, if available. + if let data = data { + let responseTokens = data["responseTokens"] as? [String: String] ?? [:] + + let analyticsPayload = data["analytics.payload"] as? [String: String] ?? [:] + + let clickMetricAnalyticsPayload = data["clickmetric.analytics.payload"] as? [String: String] ?? [:] + ... + } } Target.retrieveLocationContent([request1, request2], with: globalTargetParameters) ``` @@ -263,11 +283,29 @@ static func retrieveLocationContent(_ requestArray: [TargetRequest], with target AEPTargetParameters *targetParameters2 = [[AEPTargetParameters alloc] initWithParameters:mboxParameters2 profileParameters:nil order:order2 product:product2 ]; AEPTargetRequestObject *request1 = [[AEPTargetRequestObject alloc] initWithMboxName: @"logo" defaultContent: @"BlueWhale" targetParameters: targetParameters1 contentCallback:^(NSString * _Nullable content) { - // do something with the received content + // do something with the target content. + NSString *targetContent = content ?: @""; }]; - AEPTargetRequestObject *request2 = [[AEPTargetRequestObject alloc] initWithMboxName: @"logo" defaultContent: @"red" targetParameters: targetParameters2 contentCallback:^(NSString * _Nullable content) { - // do something with the received content + AEPTargetRequestObject *request2 = [[AEPTargetRequestObject alloc] initWithMboxName: @"logo" defaultContent: @"red" targetParameters: targetParameters2 contentWithDataCallback:^(NSString * _Nullable content, NSDictionary * _Nullable data) { + // do something with the target content. + NSString *targetContent = content ?: @""; + + // Read the data dictionary containing one or more of response tokens, analytics payload and click-tracking analytics payload, if available. + if ([data count] > 0) { + if ([data objectForKey:@"responseTokens"]) { + // read response tokens + } + + if ([data objectForKey:@"analytics.payload"]) { + // read analytics payload + } + + if ([data objectForKey:@"clickmetric.analytics.payload"]) { + // read click-tracking analytics payload + } + } }]; + // Create request object array NSArray *requestArray = @[request1,request2]; diff --git a/Makefile b/Makefile index 0f89d1e..3182bae 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ SIMULATOR_ARCHIVE_PATH = ./build/ios_simulator.xcarchive/Products/Library/Framew IOS_ARCHIVE_PATH = ./build/ios.xcarchive/Products/Library/Frameworks/ lint-autocorrect: - swiftlint autocorrect + swiftlint autocorrect --format lint: swiftlint lint @@ -16,7 +16,7 @@ check-format: swiftformat --lint AEPTarget/Sources --swiftversion 5.1 format: - swiftformat . + swiftformat AEPTarget/Sources --swiftversion 5.1 pod-install: (pod install --repo-update) @@ -71,6 +71,10 @@ check-version: test-SPM-integration: (sh ./script/test-SPM.sh) +codecov: + (./script/codecov_downloader.sh) + (bash codecov -v -X s3 -c -D "./build/out" -J "AEPTarget") + test-podspec: (sh ./script/test-podspec.sh) diff --git a/Package.swift b/Package.swift index d66ef4f..f0f800d 100644 --- a/Package.swift +++ b/Package.swift @@ -20,11 +20,11 @@ let package = Package( .library(name: "AEPTarget", targets: ["AEPTarget"]), ], dependencies: [ - .package(url: "https://github.com/adobe/aepsdk-core-ios.git", .upToNextMajor(from: "3.1.0")) + .package(url: "https://github.com/adobe/aepsdk-core-ios.git", .upToNextMajor(from: "3.1.0")), ], targets: [ .target(name: "AEPTarget", - dependencies: ["AEPCore", .product(name: "AEPServices", package: "AEPCore")], + dependencies: ["AEPCore"], path: "AEPTarget/Sources"), ] ) diff --git a/Podfile b/Podfile index d71a5b3..218b07d 100644 --- a/Podfile +++ b/Podfile @@ -8,38 +8,30 @@ workspace 'AEPTarget' project 'AEPTarget.xcodeproj' target 'AEPTarget' do - pod 'AEPCore', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' + pod 'AEPCore' end target 'AEPTargetDemoApp' do - pod 'AEPCore', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPServices', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPRulesEngine', :git => 'https://github.com/adobe/aepsdk-rulesengine-ios.git', :branch => 'main' - pod 'AEPIdentity', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPLifecycle', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPSignal', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'ACPCore', :git => 'https://github.com/adobe/aepsdk-compatibility-ios.git', :branch => 'main' + pod 'AEPCore' + pod 'AEPIdentity' + pod 'AEPLifecycle' + pod 'AEPSignal' pod 'AEPAssurance' pod 'AEPAnalytics' end target 'AEPTargetDemoObjCApp' do - pod 'AEPCore', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPServices', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPRulesEngine', :git => 'https://github.com/adobe/aepsdk-rulesengine-ios.git', :branch => 'main' - pod 'AEPIdentity', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPLifecycle', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'ACPCore', :git => 'https://github.com/adobe/aepsdk-compatibility-ios.git', :branch => 'main' + pod 'AEPCore' + pod 'AEPIdentity' + pod 'AEPLifecycle' pod 'AEPAssurance' pod 'AEPAnalytics' end target 'AEPTargetTests' do - pod 'AEPCore', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPServices', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPRulesEngine', :git => 'https://github.com/adobe/aepsdk-rulesengine-ios.git', :branch => 'main' - pod 'AEPIdentity', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' - pod 'AEPLifecycle', :git => 'https://github.com/adobe/aepsdk-core-ios.git', :branch => 'main' + pod 'AEPCore' + pod 'AEPIdentity' + pod 'AEPLifecycle' pod 'AEPAnalytics' pod 'SwiftyJSON', '~> 4.0' end diff --git a/Podfile.lock b/Podfile.lock index 11f29d3..0581a43 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,108 +1,55 @@ PODS: - - ACPCore (2.9.9): - - AEPCore - - AEPIdentity - - AEPLifecycle - - AEPRulesEngine - - AEPServices - - AEPSignal - AEPAnalytics (3.0.1): - AEPCore - AEPServices - - AEPAssurance (1.1.0): - - ACPCore (>= 2.9.0) - - AEPAssurance/xcframeworks (= 1.1.0) - - AEPAssurance/xcframeworks (1.1.0): - - ACPCore (>= 2.9.0) - - AEPCore (3.1.0): - - AEPRulesEngine (= 1.0.0) - - AEPServices (= 3.1.0) - - AEPIdentity (3.1.0): - - AEPCore (= 3.1.0) - - AEPLifecycle (3.1.0): - - AEPCore (= 3.1.0) - - AEPRulesEngine (1.0.0) - - AEPServices (3.1.0) - - AEPSignal (3.1.0): - - AEPCore (= 3.1.0) + - AEPAssurance (3.0.0): + - AEPCore (>= 3.1.0) + - AEPServices (>= 3.1.0) + - AEPCore (3.2.3): + - AEPRulesEngine (= 1.0.1) + - AEPServices (= 3.2.3) + - AEPIdentity (3.2.3): + - AEPCore (= 3.2.3) + - AEPLifecycle (3.2.3): + - AEPCore (= 3.2.3) + - AEPRulesEngine (1.0.1) + - AEPServices (3.2.3) + - AEPSignal (3.2.3): + - AEPCore (= 3.2.3) - SwiftyJSON (4.3.0) DEPENDENCIES: - - ACPCore (from `https://github.com/adobe/aepsdk-compatibility-ios.git`, branch `main`) - AEPAnalytics - AEPAssurance - - AEPCore (from `https://github.com/adobe/aepsdk-core-ios.git`, branch `main`) - - AEPIdentity (from `https://github.com/adobe/aepsdk-core-ios.git`, branch `main`) - - AEPLifecycle (from `https://github.com/adobe/aepsdk-core-ios.git`, branch `main`) - - AEPRulesEngine (from `https://github.com/adobe/aepsdk-rulesengine-ios.git`, branch `main`) - - AEPServices (from `https://github.com/adobe/aepsdk-core-ios.git`, branch `main`) - - AEPSignal (from `https://github.com/adobe/aepsdk-core-ios.git`, branch `main`) + - AEPCore + - AEPIdentity + - AEPLifecycle + - AEPSignal - SwiftyJSON (~> 4.0) SPEC REPOS: trunk: - AEPAnalytics - AEPAssurance + - AEPCore + - AEPIdentity + - AEPLifecycle + - AEPRulesEngine + - AEPServices + - AEPSignal - SwiftyJSON -EXTERNAL SOURCES: - ACPCore: - :branch: main - :git: https://github.com/adobe/aepsdk-compatibility-ios.git - AEPCore: - :branch: main - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPIdentity: - :branch: main - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPLifecycle: - :branch: main - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPRulesEngine: - :branch: main - :git: https://github.com/adobe/aepsdk-rulesengine-ios.git - AEPServices: - :branch: main - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPSignal: - :branch: main - :git: https://github.com/adobe/aepsdk-core-ios.git - -CHECKOUT OPTIONS: - ACPCore: - :commit: deec3800bce117f7f760eb6644af4559489c2aae - :git: https://github.com/adobe/aepsdk-compatibility-ios.git - AEPCore: - :commit: 6e8c14693c78fe0ce858e5edfa483649e560e62a - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPIdentity: - :commit: 6e8c14693c78fe0ce858e5edfa483649e560e62a - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPLifecycle: - :commit: 6e8c14693c78fe0ce858e5edfa483649e560e62a - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPRulesEngine: - :commit: 1e270654b330b9f07a6b1ec374e686f2e631e039 - :git: https://github.com/adobe/aepsdk-rulesengine-ios.git - AEPServices: - :commit: 6e8c14693c78fe0ce858e5edfa483649e560e62a - :git: https://github.com/adobe/aepsdk-core-ios.git - AEPSignal: - :commit: 6e8c14693c78fe0ce858e5edfa483649e560e62a - :git: https://github.com/adobe/aepsdk-core-ios.git - SPEC CHECKSUMS: - ACPCore: 67e4b855bf16cfb6a63024a4ddb73d15c0fd30e5 AEPAnalytics: 13353b3ea333be0af7cfc4845830d87d360726e2 - AEPAssurance: c9c21b7e57bf993db210766631db2ef3732d4fa4 - AEPCore: 026568b04d8c302ad801a6b4ed6ec3c160817ef6 - AEPIdentity: 2ca611ed7f2bf081657b311cbffb210900558fa8 - AEPLifecycle: 59b2de8c96a93647a258a982f1d5b2e16a425360 - AEPRulesEngine: 8bbc694fce6900cd33b2496c7d4b33ee2eeffc46 - AEPServices: cc1ec2987185ab01d62f6d41be0bb94a1b892efb - AEPSignal: 68dce891ed648057d0282b137040a5766a95c963 + AEPAssurance: 18068627111e366a851dc2166239f22b665101bd + AEPCore: eaa2a725705c17d2a63c131aa682a21429688ee6 + AEPIdentity: 8e28eeda440dbbaac977e6e499feeb5a2bf3f2a8 + AEPLifecycle: e492629ac51c0e123e02b81238cebefaa3e7cb82 + AEPRulesEngine: 5075ed294026a12e37bd26fe260f74604d205354 + AEPServices: 29df53822d7708b602ab5789b668734e3b24b6c7 + AEPSignal: 1bcaabf874da22a31cb777d6ff5908b63142b378 SwiftyJSON: 6faa0040f8b59dead0ee07436cbf76b73c08fd08 -PODFILE CHECKSUM: 12b5d538eb4160448ca4a83c9cbf68182fe9d5c4 +PODFILE CHECKSUM: 17c3d3356f61bc53119b5121c21dd73706f2d9dd -COCOAPODS: 1.10.1 +COCOAPODS: 1.10.0 diff --git a/README.md b/README.md index c1974e1..d46019f 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,13 @@ To add the AEPTarget Package to your application, from the Xcode menu select: Enter the URL for the AEPTarget package repository: `https://github.com/adobe/aepsdk-target-ios.git`. +When prompted, specify the Version rule using a specific version range or an exact version. + Alternatively, if your project has a `Package.swift` file, you can add AEPTarget directly to your dependencies: ``` dependencies: [ - .package(url: "https://github.com/adobe/aepsdk-target-ios.git", .upToNextMajor(from: "3.0.0")) + .package(url: "https://github.com/adobe/aepsdk-target-ios.git", .upToNextMajor(from: "3.1.0")), ], targets: [ .target(name: "YourTarget", diff --git a/Script/codecov_downloader.sh b/Script/codecov_downloader.sh new file mode 100755 index 0000000..d70d81a --- /dev/null +++ b/Script/codecov_downloader.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# 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. + +curl -s https://codecov.io/bash >codecov +VERSION=$(grep 'VERSION=\"[0-9\.]*\"' codecov | cut -d'"' -f2); +for i in 1 256 512; do + shasum -a $i -c --ignore-missing <(curl -s "https://raw.githubusercontent.com/codecov/codecov-bash/${VERSION}/SHA${i}SUM") || + shasum -a $i -c <(curl -s "https://raw.githubusercontent.com/codecov/codecov-bash/${VERSION}/SHA${i}SUM" | grep -w "codecov") +done diff --git a/Script/test-SPM.sh b/Script/test-SPM.sh index e1b6e4c..4e61c05 100755 --- a/Script/test-SPM.sh +++ b/Script/test-SPM.sh @@ -23,12 +23,11 @@ mkdir -p $PROJECT_NAME && cd $PROJECT_NAME swift package init # Create the Package.swift. -echo "// swift-tools-version:5.3 +echo "// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: \"TestProject\", - defaultLocalization: \"en-US\", platforms: [ .iOS(.v10) ], @@ -39,8 +38,8 @@ let package = Package( ) ], dependencies: [ - .package(name: \"AEPCore\", url: \"https://github.com/adobe/aepsdk-core-ios.git\", .branch(\"main\")), - .package(name: \"AEPTarget\", path: \"../\") + .package(url: \"https://github.com/adobe/aepsdk-core-ios.git\", .upToNextMajor(from:\"3.1.0\")), + .package(path: \"../\") ], targets: [ .target( @@ -49,6 +48,7 @@ let package = Package( .product(name: \"AEPCore\", package: \"AEPCore\"), .product(name: \"AEPIdentity\", package: \"AEPCore\"), .product(name: \"AEPLifecycle\", package: \"AEPCore\"), + .product(name: \"AEPSignal\", package: \"AEPCore\"), .product(name: \"AEPServices\", package: \"AEPCore\"), .product(name: \"AEPTarget\", package: \"AEPTarget\"), ]) diff --git a/Script/test-podspec.sh b/Script/test-podspec.sh index cb3d512..2a817e9 100644 --- a/Script/test-podspec.sh +++ b/Script/test-podspec.sh @@ -28,12 +28,7 @@ echo " platform :ios, '10.0' target '$PROJECT_NAME' do use_frameworks! - pod 'AEPCore', '>= 3.1.0' - pod 'AEPIdentity', '>= 3.1.0' - pod 'AEPLifecycle', '>= 3.1.0' - pod 'AEPServices', '>= 3.1.0' - pod 'AEPRulesEngine', '~> 1.0.0' - pod 'AEPTarget', :path => '../' + pod 'AEPTarget', :path => '../AEPTarget.podspec' end " >>Podfile