diff --git a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj index 49a9a529..84688edc 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj +++ b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 469F7B20266012CB0038E773 /* FlurryDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469F7B1F266012CB0038E773 /* FlurryDestination.swift */; }; 469F7B23266013100038E773 /* Adjust in Frameworks */ = {isa = PBXBuildFile; productRef = 469F7B22266013100038E773 /* Adjust */; }; 469F7B25266013320038E773 /* AdjustDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469F7B24266013320038E773 /* AdjustDestination.swift */; }; + 96469A9B270279A600AC5772 /* IntercomDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96469A9A270279A600AC5772 /* IntercomDestination.swift */; }; + 96469A9E2702862100AC5772 /* Intercom in Frameworks */ = {isa = PBXBuildFile; productRef = 96469A9D2702862100AC5772 /* Intercom */; }; 965DC0FA2668077400DDF9C7 /* MixpanelDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965DC0F82668077400DDF9C7 /* MixpanelDestination.swift */; }; 965DC0FB2668077400DDF9C7 /* AmplitudeSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965DC0F92668077400DDF9C7 /* AmplitudeSession.swift */; }; 965DC0FE2668079400DDF9C7 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 965DC0FD2668079400DDF9C7 /* Mixpanel */; }; @@ -46,6 +48,7 @@ 469F7B152660116A0038E773 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 469F7B1F266012CB0038E773 /* FlurryDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlurryDestination.swift; sourceTree = ""; }; 469F7B24266013320038E773 /* AdjustDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustDestination.swift; sourceTree = ""; }; + 96469A9A270279A600AC5772 /* IntercomDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntercomDestination.swift; sourceTree = ""; }; 965DC0F82668077400DDF9C7 /* MixpanelDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelDestination.swift; sourceTree = ""; }; 965DC0F92668077400DDF9C7 /* AmplitudeSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeSession.swift; sourceTree = ""; }; 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseDestination.swift; sourceTree = ""; }; @@ -69,6 +72,7 @@ 965DC1212669942800DDF9C7 /* FirebaseAnalytics in Frameworks */, 469F7B1D266011D70038E773 /* Segment in Frameworks */, 965DC0FE2668079400DDF9C7 /* Mixpanel in Frameworks */, + 96469A9E2702862100AC5772 /* Intercom in Frameworks */, 469F7B23266013100038E773 /* Adjust in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -129,6 +133,7 @@ 96D8F16E26EFFA09007F8B28 /* ExampleDestination.swift */, 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */, 469F7B1F266012CB0038E773 /* FlurryDestination.swift */, + 96469A9A270279A600AC5772 /* IntercomDestination.swift */, 965DC0F82668077400DDF9C7 /* MixpanelDestination.swift */, ); name = destination_plugins; @@ -168,6 +173,7 @@ 965DC1202669942800DDF9C7 /* FirebaseAnalytics */, BA384C9726824F3700AFEA1B /* AppsFlyerLib */, 96DBF37F26FA984A00724B0B /* ComScore */, + 96469A9D2702862100AC5772 /* Intercom */, ); productName = DestinationsExample; productReference = 469F7B04266011690038E773 /* DestinationsExample.app */; @@ -203,6 +209,7 @@ 965DC11F2669942800DDF9C7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, BA384C9626824F3700AFEA1B /* XCRemoteSwiftPackageReference "AppsFlyerFramework" */, 96DBF37E26FA984900724B0B /* XCRemoteSwiftPackageReference "Comscore-Swift-Package-Manager" */, + 96469A9C2702862100AC5772 /* XCRemoteSwiftPackageReference "intercom-ios" */, ); productRefGroup = 469F7B05266011690038E773 /* Products */; projectDirPath = ""; @@ -236,6 +243,7 @@ BA384C9A2682973300AFEA1B /* AppsFlyerDestination.swift in Sources */, 469F7B20266012CB0038E773 /* FlurryDestination.swift in Sources */, 469F7B0C266011690038E773 /* ViewController.swift in Sources */, + 96469A9B270279A600AC5772 /* IntercomDestination.swift in Sources */, 96D8F16F26EFFA09007F8B28 /* ExampleDestination.swift in Sources */, 965DC0FA2668077400DDF9C7 /* MixpanelDestination.swift in Sources */, 96DBF37D26FA943300724B0B /* ComscoreDestination.swift in Sources */, @@ -471,6 +479,14 @@ version = 4.29.6; }; }; + 96469A9C2702862100AC5772 /* XCRemoteSwiftPackageReference "intercom-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "git@github.com:intercom/intercom-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; + }; + }; 965DC0FC2668079400DDF9C7 /* XCRemoteSwiftPackageReference "mixpanel-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:mixpanel/mixpanel-swift.git"; @@ -520,6 +536,11 @@ package = 469F7B21266013100038E773 /* XCRemoteSwiftPackageReference "ios_sdk" */; productName = Adjust; }; + 96469A9D2702862100AC5772 /* Intercom */ = { + isa = XCSwiftPackageProductDependency; + package = 96469A9C2702862100AC5772 /* XCRemoteSwiftPackageReference "intercom-ios" */; + productName = Intercom; + }; 965DC0FD2668079400DDF9C7 /* Mixpanel */ = { isa = XCSwiftPackageProductDependency; package = 965DC0FC2668079400DDF9C7 /* XCRemoteSwiftPackageReference "mixpanel-swift" */; diff --git a/Examples/destination_plugins/ComscoreDestination.swift b/Examples/destination_plugins/ComscoreDestination.swift index f78a8297..c726d6c4 100644 --- a/Examples/destination_plugins/ComscoreDestination.swift +++ b/Examples/destination_plugins/ComscoreDestination.swift @@ -263,6 +263,7 @@ private extension ComscoreDestination { // MARK: - Playback methods + // MARK: - func videoPlaybackStarted(event: TrackEvent, properties: JSON) { streamAnalytics = SCORStreamingAnalytics() diff --git a/Examples/destination_plugins/IntercomDestination.swift b/Examples/destination_plugins/IntercomDestination.swift new file mode 100644 index 00000000..61cf1e43 --- /dev/null +++ b/Examples/destination_plugins/IntercomDestination.swift @@ -0,0 +1,234 @@ +// +// ComscoreDestination.swift +// ComscoreDestination +// +// Created by Cody Garvin on 9/21/21. +// + +import Segment +import Intercom +import CoreMedia + +/** + An implementation of the Comscore Analytics device mode destination as a plugin. + */ + +class IntercomDestination: DestinationPlugin { + let timeline = Timeline() + let type = PluginType.destination + let key = "Intercom" + var analytics: Analytics? = nil + + private var intercomSettings: IntercomSettings? + private var configurationLabels = [String: Any]() + + func update(settings: Settings, type: UpdateType) { + // Skip if you have a singleton and don't want to keep updating via settings. + guard type == .initial else { return } + + // Grab the settings and assign them for potential later usage. + // Note: Since integrationSettings is generic, strongly type the variable. + guard let tempSettings: IntercomSettings = settings.integrationSettings(forPlugin: self) else { return } + intercomSettings = tempSettings + Intercom.setApiKey(tempSettings.mobileApiKey, forAppId: tempSettings.appId) + analytics?.log(message: "Intercolm.setApiKey(\(tempSettings.mobileApiKey), forApId:\(tempSettings.appId))", kind: .debug) + } + + func identify(event: IdentifyEvent) -> IdentifyEvent? { + + if let userId = event.userId { + Intercom.registerUser(withUserId: userId) + analytics?.log(message: "Intercom.registerUser(withUserId: \(userId)", kind: .debug) + } else if let _ = event.anonymousId { + Intercom.registerUnidentifiedUser() + analytics?.log(message: "Intercom.registerUnidentifiedUser()", kind: .debug) + } + + if let integration = event.integrations?.dictionaryValue?["Intercom"] as? [AnyHashable: Any], + let userHash = integration["user_hash"] as? String { + Intercom.setUserHash(userHash) + } + + if let traits = event.traits?.dictionaryValue { + // Set user attributes + setUserAttributes(traits, event: event) + } + + return event + } + + func track(event: TrackEvent) -> TrackEvent? { + + // Properties can not be empty + guard let properties = event.properties?.dictionaryValue else { + + Intercom.logEvent(withName: event.event) + analytics?.log(message: "Intercom.logEvent(withName: \(event.event))", kind: .debug) + return event + } + + var output = [String: Any]() + var price = [String: Any]() + var isAmountSet = false + + for (key, value) in properties { + output[key] = value + + if let dataValue = value as? Double, + key == "revenue" || key == "total" && !isAmountSet { + let amountInCents = dataValue * 100 + price["amount"] = amountInCents + output.removeValue(forKey: key) + isAmountSet = true + } + + if key == "currency" { + price["currency"] = value + output.removeValue(forKey: "currency") + } + + if price.count > 0 { + output["price"] = price + } + + if value is [String: Any] || value is [Any] { + output.removeValue(forKey: key) + } + } + + Intercom.logEvent(withName: event.event, metaData: output) + analytics?.log(message: "Intercom.logEvent(withName: \(event.event), metaData: \(output))", kind: .debug) + + return event + } + + func group(event: GroupEvent) -> GroupEvent? { + + // id is required field for adding or modifying a company + guard let traits = event.traits?.dictionaryValue, + let groupId = event.groupId else { return event } + + let company = setCompanyAttributes(traits) + company.companyId = groupId + + let userAttributes = ICMUserAttributes() + userAttributes.companies = [company] + + Intercom.updateUser(userAttributes) + analytics?.log(message: "Intercom.updateUser(\(userAttributes))", kind: .debug) + + return event + } + + func reset() { + Intercom.logout() + analytics?.log(message: "Intercom.logout()", kind: .debug) + } +} + +// Example of what settings may look like. +private struct IntercomSettings: Codable { + let appId: String + let mobileApiKey: String +} + +private extension IntercomDestination { + + func setUserAttributes(_ traits: [String: Any], event: RawEvent?) { + let userAttributes = ICMUserAttributes() + var customAttributes = traits + + if let email = traits["email"] as? String { + userAttributes.email = email + customAttributes.removeValue(forKey: "email") + } + + if let userId = traits["user_id"] as? String { + userAttributes.userId = userId + customAttributes.removeValue(forKey: "user_id") + } + + if let name = traits["name"] as? String { + userAttributes.name = name + customAttributes.removeValue(forKey: "name") + } + + if let phone = traits["phone"] as? String { + userAttributes.phone = phone + customAttributes.removeValue(forKey: "phone") + } + + if let createdAt = traits["created_at"] as? Double { + let date = Date(timeIntervalSince1970: createdAt) + userAttributes.signedUpAt = date + customAttributes.removeValue(forKey: "created_at") + } + + if let integration = event?.integrations?.dictionaryValue?["Intercom"] as? [AnyHashable: Any] { + if let languageOverride = integration["language_override"] as? String { + userAttributes.languageOverride = languageOverride + } + + if let unsubscribed = integration["unsubscribed"] as? Bool { + userAttributes.unsubscribedFromEmails = unsubscribed + } + } + + if let company = traits["company"] as? [String: Any] { + let companyData = setCompanyAttributes(company) + userAttributes.companies = [companyData] + } + + for (key, value) in traits { + if !(value is String) && + !(value is Int) && + !(value is Double) && + !(value is Bool) { + customAttributes.removeValue(forKey: key) + } + } + + userAttributes.customAttributes = customAttributes + Intercom.updateUser(userAttributes) + analytics?.log(message: "Intercom.updateUser(\(userAttributes)", kind: .debug) + } + + func setCompanyAttributes(_ company: [String: Any]) -> ICMCompany { + let companyData = ICMCompany() + var customTraits = company + + if let companyId = company["id"] as? String { + companyData.companyId = companyId + customTraits.removeValue(forKey: "id") + } + + if let monthlySpending = company["monthly_spend"] as? Double { + companyData.monthlySpend = NSNumber(value: monthlySpending) + customTraits.removeValue(forKey: "monthly_spend") + } + + if let plan = company["plan"] as? String { + companyData.plan = plan + customTraits.removeValue(forKey: "plan") + } + + if let createdAt = company["created_at"] as? Double { + let date = Date(timeIntervalSince1970: createdAt) + companyData.createdAt = date + customTraits.removeValue(forKey: "created_at") + } + + for (key, value) in company { + if !(value is String) && + !(value is Int) && + !(value is Double) && + !(value is Bool) { + customTraits.removeValue(forKey: key) + } + } + + companyData.customAttributes = customTraits + + return companyData + } +}