diff --git a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj index 192eab8d..9c0cac1c 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj +++ b/Examples/apps/DestinationsExample/DestinationsExample.xcodeproj/project.pbxproj @@ -22,6 +22,10 @@ 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 */; }; + 965DC1212669942800DDF9C7 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 965DC1202669942800DDF9C7 /* FirebaseAnalytics */; }; + 965DC1232669947F00DDF9C7 /* FirebaseDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */; }; + 965DC1262671656C00DDF9C7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 965DC1252671656C00DDF9C7 /* GoogleService-Info.plist */; }; + 9697C1F52679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png in Resources */ = {isa = PBXBuildFile; fileRef = 9697C1F42679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -39,6 +43,9 @@ 469F7B24266013320038E773 /* AdjustDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustDestination.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 = ""; }; + 965DC1252671656C00DDF9C7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 9697C1F42679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Segment_Logo_Avatar_Grey-1024.png"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -48,6 +55,7 @@ files = ( 469EC8E0266828860068F9E3 /* FlurryAnalyticsSPM in Frameworks */, 469EC8D0266066130068F9E3 /* SystemConfiguration.framework in Frameworks */, + 965DC1212669942800DDF9C7 /* FirebaseAnalytics in Frameworks */, 469F7B1D266011D70038E773 /* Segment in Frameworks */, 965DC0FE2668079400DDF9C7 /* Mixpanel in Frameworks */, 469F7B23266013100038E773 /* Adjust in Frameworks */, @@ -86,6 +94,7 @@ 469F7B102660116A0038E773 /* Assets.xcassets */, 469F7B122660116A0038E773 /* LaunchScreen.storyboard */, 469F7B152660116A0038E773 /* Info.plist */, + 965DC1242671655700DDF9C7 /* Resources */, ); path = DestinationsExample; sourceTree = ""; @@ -103,6 +112,7 @@ children = ( 469F7B24266013320038E773 /* AdjustDestination.swift */, 965DC0F92668077400DDF9C7 /* AmplitudeSession.swift */, + 965DC1222669947F00DDF9C7 /* FirebaseDestination.swift */, 469F7B1F266012CB0038E773 /* FlurryDestination.swift */, 965DC0F82668077400DDF9C7 /* MixpanelDestination.swift */, ); @@ -110,6 +120,15 @@ path = ../../../destination_plugins; sourceTree = ""; }; + 965DC1242671655700DDF9C7 /* Resources */ = { + isa = PBXGroup; + children = ( + 9697C1F42679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png */, + 965DC1252671656C00DDF9C7 /* GoogleService-Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -131,6 +150,7 @@ 469F7B22266013100038E773 /* Adjust */, 965DC0FD2668079400DDF9C7 /* Mixpanel */, 469EC8DF266828860068F9E3 /* FlurryAnalyticsSPM */, + 965DC1202669942800DDF9C7 /* FirebaseAnalytics */, ); productName = DestinationsExample; productReference = 469F7B04266011690038E773 /* DestinationsExample.app */; @@ -163,6 +183,7 @@ 469F7B21266013100038E773 /* XCRemoteSwiftPackageReference "ios_sdk" */, 965DC0FC2668079400DDF9C7 /* XCRemoteSwiftPackageReference "mixpanel-swift" */, 469EC8DE266828860068F9E3 /* XCRemoteSwiftPackageReference "FlurrySwiftPackage" */, + 965DC11F2669942800DDF9C7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); productRefGroup = 469F7B05266011690038E773 /* Products */; projectDirPath = ""; @@ -179,8 +200,10 @@ buildActionMask = 2147483647; files = ( 469F7B142660116A0038E773 /* LaunchScreen.storyboard in Resources */, + 965DC1262671656C00DDF9C7 /* GoogleService-Info.plist in Resources */, 469F7B112660116A0038E773 /* Assets.xcassets in Resources */, 469F7B0F266011690038E773 /* Main.storyboard in Resources */, + 9697C1F52679156C00B87EC1 /* Segment_Logo_Avatar_Grey-1024.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -198,6 +221,7 @@ 469F7B08266011690038E773 /* AppDelegate.swift in Sources */, 469F7B25266013320038E773 /* AdjustDestination.swift in Sources */, 469F7B0A266011690038E773 /* SceneDelegate.swift in Sources */, + 965DC1232669947F00DDF9C7 /* FirebaseDestination.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -425,6 +449,14 @@ minimumVersion = 2.9.3; }; }; + 965DC11F2669942800DDF9C7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -447,6 +479,11 @@ package = 965DC0FC2668079400DDF9C7 /* XCRemoteSwiftPackageReference "mixpanel-swift" */; productName = Mixpanel; }; + 965DC1202669942800DDF9C7 /* FirebaseAnalytics */ = { + isa = XCSwiftPackageProductDependency; + package = 965DC11F2669942800DDF9C7 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalytics; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 469F7AFC266011690038E773 /* Project object */; diff --git a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift index 0748fce4..79667c4c 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift +++ b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift @@ -33,6 +33,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Add Flurry destination plugin analytics?.add(plugin: FlurryDestination(name: "Flurry")) + + // Add the Firebase destination plugin + analytics?.add(plugin: FirebaseDestination(name: "Firebase")) return true } diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9bb..2c19b3d7 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,41 +1,49 @@ { "images" : [ { + "filename" : "Segment_Logo_40-2.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Segment_Logo_60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "Segment_Logo_58-1.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Segment_Logo_87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "Segment_Logo_80-1.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Segment_Logo_120-1.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "Segment_Logo_120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "Segment_Logo_180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" @@ -46,6 +54,7 @@ "size" : "20x20" }, { + "filename" : "Segment_Logo_40.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" @@ -56,36 +65,43 @@ "size" : "29x29" }, { + "filename" : "Segment_Logo_58.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Segment_Logo_40-1.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "Segment_Logo_80.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Segment_Logo_76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "Segment_Logo_152.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "Segment_Logo_167.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "Segment_Logo_1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_1024.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_1024.png new file mode 100644 index 00000000..f64862fc Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_1024.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120-1.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120-1.png new file mode 100644 index 00000000..65fcefb2 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120-1.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120.png new file mode 100644 index 00000000..65fcefb2 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_120.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_152.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_152.png new file mode 100644 index 00000000..aac94896 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_152.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_167.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_167.png new file mode 100644 index 00000000..7e377f64 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_167.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_180.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_180.png new file mode 100644 index 00000000..6d34392d Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_180.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-1.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-1.png new file mode 100644 index 00000000..0724ff95 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-1.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-2.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-2.png new file mode 100644 index 00000000..0724ff95 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40-2.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40.png new file mode 100644 index 00000000..0724ff95 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_40.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58-1.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58-1.png new file mode 100644 index 00000000..8c914361 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58-1.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58.png new file mode 100644 index 00000000..8c914361 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_58.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_60.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_60.png new file mode 100644 index 00000000..24452f59 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_60.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_76.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_76.png new file mode 100644 index 00000000..42e151d3 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_76.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80-1.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80-1.png new file mode 100644 index 00000000..ae20de7a Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80-1.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80.png new file mode 100644 index 00000000..ae20de7a Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_80.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_87.png b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_87.png new file mode 100644 index 00000000..fe4e038b Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Assets.xcassets/AppIcon.appiconset/Segment_Logo_87.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/LaunchScreen.storyboard b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/LaunchScreen.storyboard index 865e9329..4f9d767c 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/LaunchScreen.storyboard +++ b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,8 @@ - - + + + - + @@ -11,15 +12,31 @@ - + - + + + + + + + + + + + + + + - + + + + diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard index 55b776ab..6e61b13c 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard +++ b/Examples/apps/DestinationsExample/DestinationsExample/Base.lproj/Main.storyboard @@ -16,22 +16,112 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Resources/GoogleService-Info.plist b/Examples/apps/DestinationsExample/DestinationsExample/Resources/GoogleService-Info.plist new file mode 100644 index 00000000..8fbab3a1 --- /dev/null +++ b/Examples/apps/DestinationsExample/DestinationsExample/Resources/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + 896388530950-bevk6oulirofbnc87iahub4vth7salm2.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.896388530950-bevk6oulirofbnc87iahub4vth7salm2 + API_KEY + AIzaSyBwNNqiITj54xNP6QFAFS7cdxJdDT81PWU + GCM_SENDER_ID + 896388530950 + PLIST_VERSION + 1 + BUNDLE_ID + com.segment.DestinationsExample + PROJECT_ID + codyfire-aec37 + STORAGE_BUCKET + codyfire-aec37.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:896388530950:ios:05c2c9171a83fd109b1487 + + \ No newline at end of file diff --git a/Examples/apps/DestinationsExample/DestinationsExample/Resources/Segment_Logo_Avatar_Grey-1024.png b/Examples/apps/DestinationsExample/DestinationsExample/Resources/Segment_Logo_Avatar_Grey-1024.png new file mode 100644 index 00000000..2197ad03 Binary files /dev/null and b/Examples/apps/DestinationsExample/DestinationsExample/Resources/Segment_Logo_Avatar_Grey-1024.png differ diff --git a/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift b/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift index 3e3f95c0..5c48112c 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift +++ b/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift @@ -8,17 +8,224 @@ import UIKit import Segment +enum SpecEvent: Int { + case track + case screen + case group + case identify + case alias + + func eventTypeName() -> String { + switch self { + case .track: + return "name" + case .identify: + return "user id" + case .screen: + return "screen title" + case .group: + return "group id" + case .alias: + return "new id" + } + } +} + class ViewController: UIViewController { + + @IBOutlet weak var propertiesStack: UIStackView? + @IBOutlet weak var eventSegment: UISegmentedControl? + @IBOutlet weak var propertiesLabel: UILabel? + @IBOutlet weak var propertiesStepper: UIStepper? + @IBOutlet weak var eventField: UITextField? + var analytics = UIApplication.shared.delegate?.analytics + private var keysFields = [UITextField]() + private var propertiesFields = [UITextField]() + override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. + title = "Spec Events" + propertiesStepper?.autorepeat = false + propertiesStepper?.maximumValue = 6 + propertiesStepper?.addTarget(self, action: #selector(stepperChanged(_:)), for: .valueChanged) + + eventSegment?.addTarget(self, action: #selector(eventChoiceChanged(_:)), for: .valueChanged) + + propertiesStack?.spacing = 4.0 } - @IBAction func trackAction(_ sender: Any) { - analytics?.track(name: "test event", properties: ["testValue": 42]) + @IBAction func eventAction(_ sender: Any) { + + view.endEditing(true) + + // Determine the current segment + guard let selectedIndex = eventSegment?.selectedSegmentIndex else { + return + } + + let specChosen = SpecEvent(rawValue: selectedIndex) + + switch specChosen { + case .track: + trackEvent() + case .screen: + screenEvent() + case .group: + groupEvent() + case .identify: + identifyEvent() + case .alias: + aliasEvent() + case .none: + print("Failed to establish event type") + } + + clearAll() + } + + func trackEvent() { + guard let eventFieldText = eventField?.text else { return } + analytics?.track(name: eventFieldText, properties: valuesEntered()) + } + + func screenEvent() { + guard let eventFieldText = eventField?.text else { return } + analytics?.screen(screenTitle: eventFieldText, properties: valuesEntered()) + } + + func groupEvent() { + guard let eventFieldText = eventField?.text else { return } + analytics?.group(groupId: eventFieldText, traits: valuesEntered()) + } + + func identifyEvent() { + guard let eventFieldText = eventField?.text else { return } + analytics?.identify(userId: eventFieldText, traits: valuesEntered()) + } + + func aliasEvent() { + guard let eventFieldText = eventField?.text else { return } + analytics?.alias(newId: eventFieldText) } } +extension ViewController { + + @objc + func stepperChanged(_ stepper: UIStepper) { + + let currentStepCount = Int(stepper.value) + if propertiesFields.count < currentStepCount { + + let captureView = UIView() + captureView.tag = propertiesFields.count + 100 + captureView.alpha = 0.0 + + let nextKeyField = UITextField() + nextKeyField.placeholder = "Key..." + nextKeyField.borderStyle = .roundedRect + keysFields.append(nextKeyField) + + let nextField = UITextField() + nextField.placeholder = "Value..." + nextField.borderStyle = .roundedRect + propertiesFields.append(nextField) + + captureView.addSubview(nextKeyField) + captureView.addSubview(nextField) + + nextKeyField.translatesAutoresizingMaskIntoConstraints = false + nextKeyField.heightAnchor.constraint(equalToConstant: 36.0).isActive = true + nextKeyField.leadingAnchor.constraint(equalTo: captureView.leadingAnchor).isActive = true + + nextField.translatesAutoresizingMaskIntoConstraints = false + nextField.widthAnchor.constraint(equalTo: nextKeyField.widthAnchor).isActive = true + nextField.centerYAnchor.constraint(equalTo: nextKeyField.centerYAnchor).isActive = true + nextField.heightAnchor.constraint(equalToConstant: 36.0).isActive = true + nextField.trailingAnchor.constraint(equalTo: captureView.trailingAnchor).isActive = true + nextField.leadingAnchor.constraint(equalTo: nextKeyField.trailingAnchor, constant: 8.0).isActive = true + nextField.topAnchor.constraint(equalTo: captureView.topAnchor).isActive = true + nextField.bottomAnchor.constraint(equalTo: captureView.bottomAnchor).isActive = true + + propertiesStack?.addArrangedSubview(captureView) + + UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut, animations: { + captureView.alpha = 1.0 + captureView.layoutIfNeeded() + }, completion: nil) + + } else { + if propertiesFields.last != nil, + keysFields.last != nil, + let captureView = propertiesStack?.viewWithTag(currentStepCount + 100) { + + propertiesFields.removeLast() + keysFields.removeLast() + captureView.removeFromSuperview() + } + } + } + + @objc + func eventChoiceChanged(_ segment: UISegmentedControl) { + + // Dismiss the KB + view.endEditing(true) + + switch segment.selectedSegmentIndex { + case 0, // Track + 1: // Screen + propertiesLabel?.text = "Properties" + propertiesStepper?.isEnabled = true + case 2, // Group + 3: // Identify + propertiesLabel?.text = "Traits" + propertiesStepper?.isEnabled = true + case 4: // Alias + propertiesLabel?.text = "" + propertiesStepper?.isEnabled = false + default: + propertiesLabel?.text = "" + propertiesStepper?.isEnabled = false + } + + let specEvent = SpecEvent(rawValue: segment.selectedSegmentIndex) + eventField?.placeholder = specEvent?.eventTypeName() + } + + private func valuesEntered() -> [String: AnyHashable]? { + var keyValues = [String: AnyHashable]() + for (index, keyField) in keysFields.enumerated() { + let valueField = propertiesFields[index] + if let keyFieldText = keyField.text, + let valueFieldText = valueField.text, + !keyFieldText.isEmpty, !valueFieldText.isEmpty { + keyValues[keyFieldText] = valueFieldText + } + } + + if keyValues.isEmpty { + return nil + } else { + return keyValues + } + } + + private func clearAll() { + eventField?.text = nil + while propertiesFields.last != nil { + let currentStepCount = propertiesFields.count - 1 + if let captureView = propertiesStack?.viewWithTag(currentStepCount + 100) { + + propertiesFields.removeLast() + keysFields.removeLast() + captureView.removeFromSuperview() + } + } + propertiesStepper?.value = 0 + } +} diff --git a/Examples/destination_plugins/FirebaseDestination.swift b/Examples/destination_plugins/FirebaseDestination.swift new file mode 100644 index 00000000..bdf3e0f4 --- /dev/null +++ b/Examples/destination_plugins/FirebaseDestination.swift @@ -0,0 +1,219 @@ +// +// FirebaseDestination.swift +// DestinationsExample +// +// Created by Cody Garvin on 6/3/21. +// + +// NOTE: You can see this plugin in use in the DestinationsExample application. +// +// This plugin is NOT SUPPORTED by Segment. It is here merely as an example, +// and for your convenience should you find it useful. +// +// Firebase SPM package can be found here: https://github.com/firebase/firebase-ios-sdk + +// MIT License +// +// Copyright (c) 2021 Segment +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import Segment +import FirebaseCore +import FirebaseAnalytics + +/** + An implmentation of the Firebase Analytics device mode destination as a plugin. + */ + +class FirebaseDestination: DestinationPlugin { + let timeline: Timeline = Timeline() + let type: PluginType = .destination + let name: String + var analytics: Segment.Analytics? = nil + + required init(name: String) { + self.name = name + } + + func update(settings: Settings) { + guard let firebaseSettings = settings.integrationSettings(for: "Firebase") else { return } + if let deepLinkURLScheme = firebaseSettings["deepLinkURLScheme"] as? String { + FirebaseOptions.defaultOptions()?.deepLinkURLScheme = deepLinkURLScheme + analytics?.log(message: "Added deepLinkURLScheme: \(deepLinkURLScheme)") + } + + // First check if firebase has been set up from a previous settings call + if (FirebaseApp.app() != nil) { + analytics?.log(message: "Firebase already configured, skipping") + } else { + FirebaseApp.configure() + } + } + + func identify(event: IdentifyEvent) -> IdentifyEvent? { + + if let userId = event.userId { + FirebaseAnalytics.Analytics.setUserID(userId) + analytics?.log(message: "Firebase setUserId(\(userId))") + } + + // Check the user properties for type + if let traits = event.traits, + let mapDictionary = traits.dictionaryValue { + // Send off to identify + mapToStrings(mapDictionary) { key, data in + FirebaseAnalytics.Analytics.setUserProperty(data, forName: key) + analytics?.log(message: "Firebase setUserPropertyString \(data) for key \(key)") + } + } + + return event + } + + func track(event: TrackEvent) -> TrackEvent? { + + let name = formatFirebaseEventNames(event.event) + var parameters: [String: Any]? = nil + if let properties = event.properties?.dictionaryValue { + parameters = returnMappedFirebaseParameters(properties) + } + + FirebaseAnalytics.Analytics.logEvent(name, parameters: parameters) + analytics?.log(message: "Firebase logEventWithName \(name) parameters \(String(describing: parameters))") + return event + } + + func screen(event: ScreenEvent) -> ScreenEvent? { + + if let eventName = event.name { + FirebaseAnalytics.Analytics.logEvent(FirebaseAnalytics.AnalyticsEventScreenView, + parameters: [FirebaseAnalytics.AnalyticsParameterScreenName: eventName]) + analytics?.log(message: "Firebase setScreenName \(eventName)") + } + + + return event + } +} + +// MARK: - Support methods + +extension FirebaseDestination { + + // Maps Segment spec to Firebase constant + func formatFirebaseEventNames(_ eventName: String) -> String { + + let mappedValues = ["Product Clicked": FirebaseAnalytics.AnalyticsEventSelectContent, + "Product Viewed": FirebaseAnalytics.AnalyticsEventViewItem, + "Product Added": FirebaseAnalytics.AnalyticsEventAddToCart, + "Product Removed": FirebaseAnalytics.AnalyticsEventRemoveFromCart, + "Checkout Started": FirebaseAnalytics.AnalyticsEventBeginCheckout, + "Promotion Viewed": FirebaseAnalytics.AnalyticsEventPresentOffer, + "Payment Info Entered": FirebaseAnalytics.AnalyticsEventAddPaymentInfo, + "Order Completed": FirebaseAnalytics.AnalyticsEventPurchase, + "Order Refunded": FirebaseAnalytics.AnalyticsEventRefund, + "Product List Viewed": FirebaseAnalytics.AnalyticsEventViewItemList, + "Product Added to Wishlist": FirebaseAnalytics.AnalyticsEventAddToWishlist, + "Product Shared": FirebaseAnalytics.AnalyticsEventShare, + "Cart Shared": FirebaseAnalytics.AnalyticsEventShare, + "Products Searched": FirebaseAnalytics.AnalyticsEventSearch] + if let mappedEvent = mappedValues[eventName] { + return mappedEvent + } else { + return (try? formatFirebaseName(eventName)) ?? eventName + } + } + + func formatFirebaseName(_ eventName: String) throws -> String { + let trimmed = eventName.trimmingCharacters(in: .whitespaces) + do { + let regex = try NSRegularExpression(pattern: "([^a-zA-Z0-9_])", options: .caseInsensitive) + let formattedString = regex.stringByReplacingMatches(in: trimmed, options: .reportProgress, range: NSMakeRange(0, trimmed.count), withTemplate: "_") + + // Resize the string to maximum 40 characters if needed + let range = NSRange(location: 0, length: min(formattedString.count, 40)) + return NSString(string: formattedString).substring(with: range) + } catch { + analytics?.log(message: "Could not parse event name for Firebase.") + throw(error) + } + } + + func returnMappedFirebaseParameters(_ properties: [String: Any]) -> [String: Any] { + + let mappedKeys = ["products": FirebaseAnalytics.AnalyticsParameterItems, + "category": FirebaseAnalytics.AnalyticsParameterItemCategory, + "product_id": FirebaseAnalytics.AnalyticsParameterItemID, + "name": FirebaseAnalytics.AnalyticsParameterItemName, + "brand": FirebaseAnalytics.AnalyticsParameterItemBrand, + "price": FirebaseAnalytics.AnalyticsParameterPrice, + "quantity": FirebaseAnalytics.AnalyticsParameterQuantity, + "query": FirebaseAnalytics.AnalyticsParameterSearchTerm, + "shipping": FirebaseAnalytics.AnalyticsParameterShipping, + "tax": FirebaseAnalytics.AnalyticsParameterTax, + "total": FirebaseAnalytics.AnalyticsParameterValue, + "revenue": FirebaseAnalytics.AnalyticsParameterValue, + "order_id": FirebaseAnalytics.AnalyticsParameterTransactionID, + "currency": FirebaseAnalytics.AnalyticsParameterCurrency] + + var mappedValues = properties + + for (key, firebaseKey) in mappedKeys { + if var data = properties[key] { + + mappedValues.removeValue(forKey: key) + + if let castData = data as? [String: Any] { + data = returnMappedFirebaseParameters(castData) + } else if let castArray = data as? [Any] { + var updatedArray = [Any]() + for item in castArray { + if let castDictionary = item as? [String: Any] { + updatedArray.append(returnMappedFirebaseParameters(castDictionary)) + } else { + updatedArray.append(item) + } + } + data = updatedArray + } + + // Check key name for proper format + if let updatedFirebaseKey = try? formatFirebaseName(firebaseKey) { + mappedValues[updatedFirebaseKey] = data + } + } + } + + return mappedValues + } + + // Makes sure all traits are string based for Firebase API + func mapToStrings(_ mapDictionary: [String: Any], finalize: (String, String) -> Void) { + + for (key, data) in mapDictionary { + var dataString = data as? String ?? "\(data)" + let keyString = key.replacingOccurrences(of: " ", with: "_") + dataString = dataString.trimmingCharacters(in: .whitespacesAndNewlines) + finalize(keyString, dataString) + } + } +} + diff --git a/Sources/Segment/Events.swift b/Sources/Segment/Events.swift index 010478a9..e03d0989 100644 --- a/Sources/Segment/Events.swift +++ b/Sources/Segment/Events.swift @@ -31,19 +31,6 @@ extension Analytics { } } - public func track(name: String, properties: [String: Any]?) { - var props: JSON? = nil - if let properties = properties { - do { - props = try JSON(properties) - } catch { - exceptionFailure("\(error)") - } - } - let event = TrackEvent(event: name, properties: props) - process(incomingEvent: event) - } - public func track(name: String) { track(name: name, properties: nil as TrackEvent?) } @@ -61,12 +48,17 @@ extension Analytics { /// but want to record traits, you should pass nil. For more information on how we /// generate the UUID and Apple's policies on IDs, see https://segment.io/libraries/ios#ids /// - traits: A dictionary of traits you know about the user. Things like: email, name, plan, etc. - public func identify(userId: String, traits: T) { + public func identify(userId: String, traits: T?) { do { - let jsonTraits = try JSON(with: traits) - store.dispatch(action: UserInfo.SetUserIdAndTraitsAction(userId: userId, traits: jsonTraits)) - let event = IdentifyEvent(userId: userId, traits: jsonTraits) - process(incomingEvent: event) + if let traits = traits { + let jsonTraits = try JSON(with: traits) + store.dispatch(action: UserInfo.SetUserIdAndTraitsAction(userId: userId, traits: jsonTraits)) + let event = IdentifyEvent(userId: userId, traits: jsonTraits) + process(incomingEvent: event) + } else { + let event = IdentifyEvent(userId: userId, traits: nil) + process(incomingEvent: event) + } } catch { exceptionFailure("\(error)") } @@ -91,6 +83,7 @@ extension Analytics { /// - userId: A database ID (or email address) for this user. If you don't have a userId /// but want to record traits, you should pass nil. For more information on how we /// generate the UUID and Apple's policies on IDs, see https://segment.io/libraries/ios#ids + @objc public func identify(userId: String) { let event = IdentifyEvent(userId: userId, traits: nil) store.dispatch(action: UserInfo.SetUserIdAction(userId: userId)) @@ -141,19 +134,18 @@ extension Analytics { } } + @objc public func group(groupId: String) { group(groupId: groupId, traits: nil as GroupEvent?) } // MARK: - Alias - /* will possibly deprecate this ... TBD */ - /* + @objc public func alias(newId: String) { - let userInfo: UserInfo? = store.currentState() +// let userInfo: UserInfo? = store.currentState() let event = AliasEvent(newId: newId) store.dispatch(action: UserInfo.SetUserIdAction(userId: newId)) process(incomingEvent: event) } - */ } diff --git a/Sources/Segment/ObjC/EventsObjC.swift b/Sources/Segment/ObjC/EventsObjC.swift new file mode 100644 index 00000000..09a6a1d1 --- /dev/null +++ b/Sources/Segment/ObjC/EventsObjC.swift @@ -0,0 +1,118 @@ +// +// File.swift +// +// +// Created by Cody Garvin on 6/10/21. +// + +import Foundation + +// MARK: - Objective-C friendly methods +extension Analytics { + + // MARK: - Objective-C Track + + /// Associate a user with their unique ID and record traits about them. + /// - Parameters: + /// - userId: A database ID (or email address) for this user. If you don't have a userId + /// but want to record traits, you should pass nil. For more information on how we + /// generate the UUID and Apple's policies on IDs, see https://segment.io/libraries/ios#ids + /// - properties: A dictionary of traits you know about the user. Things like: email, name, plan, etc. + @objc + public func track(name: String, properties: [String: Any]?) { + var props: JSON? = nil + if let properties = properties { + do { + props = try JSON(properties) + } catch { + exceptionFailure("\(error)") + } + } + let event = TrackEvent(event: name, properties: props) + process(incomingEvent: event) + } + + + // MARK: - Objective-C Identify + + /// Associate a user with their unique ID and record traits about them. + /// - Parameters: + /// - userId: A database ID (or email address) for this user. If you don't have a userId + /// but want to record traits, you should pass nil. For more information on how we + /// generate the UUID and Apple's policies on IDs, see https://segment.io/libraries/ios#ids + /// - traits: A dictionary of traits you know about the user. Things like: email, name, plan, etc. + @objc + public func identify(userId: String, traits: [String: AnyHashable]?) { + do { + if let traits = traits { + let traits = try JSON(traits as Any) + let event = IdentifyEvent(userId: userId, traits: traits) + process(incomingEvent: event) + } else { + let event = IdentifyEvent(userId: userId, traits: nil) + process(incomingEvent: event) + } + + } catch { + exceptionFailure("Could not parse traits.") + } + } + + /// Associate a user with traits about them. + /// - Parameters: + /// - traits: A dictionary of traits you know about the user. Things like: email, name, plan, etc. + @objc + public func identify(traits: [String: AnyHashable]) { + do { + let traits = try JSON(traits as Any) + let event = IdentifyEvent(userId: nil, traits: traits) + process(event: event) + } catch { + exceptionFailure("Could not parse traits.") + } + } + + + // MARK: - Objective-C Screen + + /// Track a screen change with a title, category and other properties. + /// - Parameters: + /// - screenTitle: The title of the screen being tracked. + /// - category: A category to the type of screen if it applies. + /// - properties: Any extra metadata associated with the screen. e.g. method of access, size, etc. + @objc + public func screen(screenTitle: String, category: String? = nil, properties: [String: AnyHashable]?) { + var event = ScreenEvent(screenTitle: screenTitle, category: category, properties: nil) + if let properties = properties { + do { + let jsonProperties = try JSON(properties) + event = ScreenEvent(screenTitle: screenTitle, category: category, properties: jsonProperties) + } catch { + exceptionFailure("Could not parse properties.") + } + } + process(event: event) + } + + + // MARK: - Objective-C Group + + + /// Associate a user with a group such as a company, organization, project, etc. + /// - Parameters: + /// - groupId: A unique identifier for the group identification in your system. + /// - traits: Traits of the group you may be interested in such as email, phone or name. + @objc + public func group(groupId: String, traits: [String: AnyHashable]?) { + var event = GroupEvent(groupId: groupId) + if let traits = traits { + do { + let jsonTraits = try JSON(traits) + event = GroupEvent(groupId: groupId, traits: jsonTraits) + } catch { + exceptionFailure("Could not parse traits.") + } + } + process(event: event) + } +}