Build a Trip Experience

This section describes how to build a trip experience for the user, and how to receive the user's driving behavior from Zendrive SDK. You must cache the data locally, as described in the TripRepository documentation.

This section includes the following topics:

Build a Trip Experience

Build a Trip Experience

Build a Trip Experience

Build a Trip Experience

Build a Trip Experience

Build a Trip Experience

1.0. Fetch User Trips

This section describes how to use the driverTripsAPI to fetch user trip statistics.

1.1. Integrate tripList API To Fetch User Trips

Create a FetchIQLTripsJob class as described in the following code snippet. Parse the data received and cache it locally. Refer to the driverTripsAPI endpoint for more information.

class FetchIQLTripsJob: Job {
    func run() {
        DDLogDebug("Start: Fetch iql trips")
        let core = IQLRef.sharedInstance
        guard let driverId = core.loginRepository.currentUser?.driverId else {
            fatalError("User id doesn't exist")
        }
        core.api.getTrips(driverId: driverId, cursor: nil,
                          onSuccess: { data in
            for tripData in data.trips! {
                let trip =  core.tripRepository.tripDetails(tripInfo: tripData)
                core.tripRepository.insert(trip: trip)
            }
            ResponseMetaUtils.sharedInstance.setResponseMeta(data: data.responseMetadata)

        }, onFailure: { failure in
            DDLogDebug("Failure - \(failure)")
            DDLogDebug("End: trip list fetch failed")
        }) { error in
            DDLogDebug("Error - \(error)")
            DDLogDebug("End: trip fetch Error")
        }
    }
}
class API {
    ...
    ...
    func getTrips(driverId: String,
                  cursor: String?,
                  onSuccess: ((_ trips: TripListResponse) -> Void)?,
                  onFailure: ((_ failureResponse: FailureResponse) -> Void)?,
                  onError: ((_ error: APIError) -> Void)? ) {
        if let cursor = cursor {
            executeGetRequest(
                endpoint: .getTrips,
                pathParams: ["driver_id": driverId],
                queryParams: ["cursor": cursor],
                onSuccess: onSuccess, onFailure: onFailure, onError: onError)
        } else {
            executeGetRequest(
                endpoint: .getTrips,
                pathParams: ["driver_id": driverId],
                onSuccess: onSuccess, onFailure: onFailure, onError: onError)
        }
    }
    ...
    ...
}

1.2. Parse and Cache Trip Data

This section explains how to parse the trip data and cache it locally.

class TripRepository {
    func tripDetails(tripInfo: TripData) -> Trip {
        let events = tripInfo.drivingBehavior.eventRating
        let driveCategory = TripDriveCategory.carDriver
        let tripScore = TripScore.init(zendriveScore: Int32(tripInfo.drivingBehavior.score.zendriveScore))

        var driveRatings = TripEventRatings.init(aggressiveAccelerationRating: .notAvailable , hardBrakeRating: .notAvailable , speedingRating: .notAvailable, hardTurnRating: .notAvailable, phoneHandlingRating: .notAvailable)

        if let rapidAcceleration = TripStarRating.init(rawValue: "\(events.rapidAcceleration)") {
            driveRatings.aggressiveAccelerationRating = rapidAcceleration
        }
        if let hardBrake = TripStarRating.init(rawValue: "\(events.hardBrake)") {
            driveRatings.hardBrakeRating = hardBrake
        }
        if let overSpeeding = TripStarRating.init(rawValue: "\(events.overSpeeding)") {
            driveRatings.speedingRating = overSpeeding
        }
        if let hardTurn = TripStarRating.init(rawValue: "\(events.hardTurn)") {
            driveRatings.hardTurnRating = hardTurn
        }
        if let phoneUse = TripStarRating.init(rawValue:"\(events.phoneUse)") {
            driveRatings.phoneHandlingRating = phoneUse
        }

        let tripStartLocation = TripLocationPointWithTimestamp.init(timestampMillis: tripInfo.tripId, location: tripInfo.startLocation)
        let tripEndLocation = TripLocationPointWithTimestamp.init(timestampMillis: tripInfo.tripId, location: tripInfo.endLocation)

        let waypoint = [tripStartLocation, tripEndLocation]

        return Trip(driveCategory: driveCategory,
                    meta: nil,
                    createdTimeMillis: tripInfo.tripId,
                    updatedTimeMillis: 0,
                    timezone: "",
                    driveId: String(tripInfo.tripId),
                    startTimeMillis: tripInfo.tripId,
                    endTimeMillis: DateUtils.sharedInstance.getTimeStamp(inputDate: tripInfo.endTime),
                    driveType: .driving,
                    userMode: nil,
                    distanceMeters: tripInfo.distance,
                    averageSpeed: 0.0,
                    maxSpeed: 0.0,
                    phonePosition: .unknown,
                    waypoints: waypoint,
                    events: nil,
                    score: tripScore,
                    eventRatings: driveRatings,
                    warnings: nil,
                    tags: [],
                    isDriveCategoryManuallyUpdated: false,
                    vehicleId: nil,
                    isMockTrip: false)
    }
}
class TripRepository {
    ...
    ...

    func insert(trip: Trip) {
        let trips = allTrips()
        if !trips.contains(where: { $0.driveId == trip.driveId }) {
            self.tripDao.insert(record: trip)
            self.inserTripsMeta(trip: trip)
            if !loginRepo.isMockMode {
                updateProgramStatus()
            }
        }
    }

    func inserTripsMeta(trip: Trip) {
        IQLRef.sharedInstance.jobManager.enqueue(job: GenerateTripMapJob(trip: trip))
        DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
            self.updateTripMetaData(trip: trip)
        }
    }

    //Reverse geocode start and end coordinates to fetch the trip start and end addresses
    func updateTripMetaData(trip: Trip) {
        let geocoder = Geocoder.sharedInstance
        var tripMeta = TripMeta.init(image: trip.meta?.image, startAddress: trip.meta?.startAddress, endAddress: trip.meta?.endAddress)

        if tripMeta.startAddress == nil || tripMeta.endAddress == nil {
            DispatchQueue.global().async {
                if let firstPoint = trip.waypoints.first, let lastPoint = trip.waypoints.last {
                    if let startAddressRes = try? awaitPromise(geocoder.reverseGeocode(lat: firstPoint.location.latitude, lng: firstPoint.location.longitude)) {
                        tripMeta.startAddress = startAddressRes.address
                        if let endAddressRes = try? awaitPromise(geocoder.reverseGeocode(lat: lastPoint.location.latitude, lng: lastPoint.location.longitude)) {
                            tripMeta.endAddress = endAddressRes.address
                            self.updateTripMeta(driveId: String(trip.driveId), meta: tripMeta)
                        }
                    }
                }
            }
        }
    }
    
    ...
    ...
}

1.3. Implement Pagination To Fetch Additional Trips

Fetching all user trips at once is not considered best practice. We therefore recommend that you fetch 30 trips via each API call and implement accurate pagination in the trip list. To do so, use the key nextCursor which allows you to fetch the next set of trips by passing it via an API call.

extension TripsViewController {




    func getTripsWithTimeStamp(timeStamp: String) {

        guard let driverId = self.core.loginRepository.currentUser?.driverId else {
            fatalError("User id doesn't exist")
        }

        core.api.getTrips(driverId: driverId, cursor: timeStamp,
                          onSuccess: { data in
            ResponseMetaUtils.sharedInstance.resetCursor()
            for tripData in data.trips! {
                self.activityIndicator.stopAnimating()
                let trip =  self.core.tripRepository.tripDetails(tripInfo: tripData)
                self.core.tripRepository.insert(trip: trip)
            }
            ResponseMetaUtils.sharedInstance.setResponseMeta(data: data.responseMetadata)
            self.onTripAnalyzed()

        }, onFailure: { failure in
            self.activityIndicator.stopAnimating()
            DDLogDebug("Failure - \(failure)")
            DDLogDebug("End: trip list pagination failed")
        }) { error in
            self.activityIndicator.stopAnimating()
            DDLogDebug("Error - \(error)")
            DDLogDebug("End: trip list pagination Error")
        }



    }


}
class API {
    ...
    ...
    func getTrips(driverId: String,
                  cursor: String?,
                  onSuccess: ((_ trips: TripListResponse) -> Void)?,
                  onFailure: ((_ failureResponse: FailureResponse) -> Void)?,
                  onError: ((_ error: APIError) -> Void)? ) {
        if let cursor = cursor {
            executeGetRequest(
                endpoint: .getTrips,
                pathParams: ["driver_id": driverId],
                queryParams: ["cursor": cursor],
                onSuccess: onSuccess, onFailure: onFailure, onError: onError)
        } else {
            executeGetRequest(
                endpoint: .getTrips,
                pathParams: ["driver_id": driverId],
                onSuccess: onSuccess, onFailure: onFailure, onError: onError)
        }
    }
    ...
    ...
}w

2.0. Build Trip List Experience

Use the directions provided in the reference code implementation link below to build the trip list experience and the user experience (UX) for obtaining trip feedback from users.

3.0. Build Trip Detail Experience

Use the directions provided in the developer instructions below to build the user flow for obtaining trip details from IQL program users.

3.1. Fetch Data Using tripDetails API

Fetch trip details using the tripDetails API for individual trips before navigating to the Trip Details screen. Refer to tripDetails API for more information. Refer to the driverTrips API endpoint for more information.

extension TripsViewController:UICollectionViewDataSource {




    func collectionView(_ collectionView: UICollectionView, cell I’ve ForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let tripIndex = indexPath.row
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! TripViewCell
        let trip = self.trips[tripIndex]
        cell.configure(trip: trip) {
            self.onTapTrip(trip: trip)
        } 
    }





func onTapTrip(trip: Trip) {
        let updatedTrip = IQLRef.sharedInstance.tripRepository.findTrip(driveId: trip.driveId)
        if updatedTrip!.isTripUpdated { //Check if trip data is already updated
            let controller = getAppNavController()?.navigateTo(nav: .tripDetail ) as? TripDetailViewController
            controller?.trip = updatedTrip
        } else {
            self.fetchTripDetail(trip: trip)
        }
    }




func fetchTripDetail(trip: Trip) {
        self.activityIndicator.startAnimating()
        DDLogDebug("Start: Fetch trip detail")
        let core = IQLRef.sharedInstance
        guard let driverId = core.loginRepository.currentUser?.driverId else {
            fatalError("User id doesn't exist")
        }
        core.api.getTripDetails(driverId: driverId, tripId: trip.driveId,
                                onSuccess: { data in
            if data.simplePath != nil {
                self.core.tripRepository.updateTripInfoData(driveId: trip.driveId, data: data)
                let updatedTrip = IQLRef.sharedInstance.tripRepository.findTrip(driveId: trip.driveId)
                let controller = self.getAppNavController()?.navigateTo(nav: .tripDetail ) as? TripDetailViewController
                controller?.trip = updatedTrip
                self.activityIndicator.stopAnimating()
            }

        }, onFailure: { failure in
            self.activityIndicator.stopAnimating()
            DDLogDebug("Failure - \(failure)")
            DDLogDebug("End: fetch trip detail Failed")
        }) { error in
            self.activityIndicator.stopAnimating()
            DDLogDebug("Error - \(error)")
            DDLogDebug("End: fetch trip detail Error")
        }
    }



}
class API {
    ...
    ...
    func getTripDetails(driverId: String, tripId: String, onSuccess: ((_ trips: TripDetail) -> Void)?, onFailure: ((_ failureResponse: FailureResponse) -> Void)?, onError: ((_ error: APIError) -> Void)? ) {
        executeGetRequest(
            endpoint: .getTripDetail,
            pathParams: ["driver_id": driverId, "trip_id": tripId],
            onSuccess: onSuccess, onFailure: onFailure, onError: onError)
    }
    ...
    ...
}

3.2. Parse and Cache Trip Details Data

Use the developer instructions below to parse and cache trip details data retrieved by the tripDetails API.

class TripRepository {



func updateTripInfoData(driveId: String, data: TripDetail) {
        var waypointArr: [TripLocationPointWithTimestamp] = []
        var eventData: [TripEvent] = []

        if let waypoint = data.simplePath {
            for waypoint in waypoint {
                let location = TripLocationPoint.init(latitude: waypoint.latitude, longitude: waypoint.longitude)
                let wayPoint =  TripLocationPointWithTimestamp.init(timestampMillis: Int64(waypoint.time_millis), location: location)
                waypointArr.append(wayPoint)
            }
            if let tripInfo = data.tripInfo {
                let usermode = TripUserMode.init(rawValue: tripInfo.userMode.lowercased())
                let vehicleType = TripDriveCategory.init(rawValue: tripInfo.vehicleType!.lowercased())
                let driveType = TripDriveType.init(rawValue: tripInfo.driveType.lowercased())
                tripDao.updateTripDetails(driveId: driveId, waypoints: waypointArr, userMode: usermode!, driveCategory: vehicleType!, driveType: driveType!)
            }
        }

        if let eventArray = data.events {
            for event in eventArray {
                let eventType = TripEventType.init(rawValue: event.eventTypeName.lowercased())!
                let startTimeStamp = DateUtils.sharedInstance.getTimeStamp(inputDate: event.startTime)
                let endTimeStamp = DateUtils.sharedInstance.getTimeStamp(inputDate: event.endTime)
                let startLocation = TripLocationPoint.init(latitude: event.latitudeStart, longitude: event.longitudeStart)
                let endLocation = TripLocationPoint.init(latitude: event.latitudeEnd, longitude: event.longitudeEnd)
                let speedingInfo = TripSpeedingInfo.init(speedLimit: event.postedSpeedLimitKmph, maxSpeed: event.maxDriverSpeedKmph, avgSpeed: event.averageDriverSpeedKmph)
                let tripEvent = TripEvent.init(eventType: eventType, startTimestampMillis: startTimeStamp, endTimestampMillis: endTimeStamp, startLocation: startLocation, endLocation: endLocation, severity: nil, turnDirection: nil, speedingInfo: speedingInfo)
                eventData.append(tripEvent)
            }
            if !eventData.isEmpty {
                tripDao.updateTripEvents(driveId: driveId, events: eventData)
            }
        }
    }



}

3.3. Build a Trip Detail Page

Use the code provided in the reference implementation link below to build a trip detail page.

4.0. Implement Trip Deletion

Use this section to enable users to delete trip and trip information.

4.1. Enable Users To Delete Invalid and Non-driving Trips

Enable users to delete a trip in case the trip turns out to be invalid or a non-driving trip, using the instructions provided in this section. Refer to the tripFeedback EndpointAPI endpoint for more information.

4.2. Allow Trip Deletion To Trips Less Than 7 Days Old

Use the following code snippet to enable users to delete trips that are less than 7 days old

class TripViewCell: UICollectionViewCell {





func configure(trip: Trip,
                   onTripTap: @escaping TapAction,
                   onTapDelete: @escaping TapAction,
                   onTapRetry: @escaping TapAction,
                   onTapCancel: @escaping TapAction) {



        if DateUtils.sharedInstance.isTripOlderThanSevenDays(tripDate: startTimeDate) {
            tripFeedbackView.isHidden = true
            tripFeedbackView.height(10)
        } else {
            tripFeedbackView.height(48)
            tripFeedbackView.isHidden = false
        }



    }



}

class DateUtils {




func isTripOlderThanSevenDays(tripDate: Date) -> Bool {
        let currentDate = Date()
        let timeDifference = Int(currentDate.timeIntervalSince1970 - tripDate.timeIntervalSince1970)
        let hours = timeDifference / 3600
        let days = hours/24
        
        if days <= 7 {
            return true
        }
        return false
    }



}

4.3. Enable Trip Deletion Only for Non-qualified Users

Once a user qualifies for the IQL program, he or she should not be allowed to delete trips, as all trip data is required while preparing an offer. Use the following code snippet to disable trip deletion for qualified users:

func configure(trip: Trip,
                   onTripTap: @escaping TapAction,
                   onTapDelete: @escaping TapAction,
                   onTapRetry: @escaping TapAction,
                   onTapCancel: @escaping TapAction) {



        if loginRepo.state == .Qualified {
            tripFeedbackView.isHidden = true
            tripFeedbackView.height(10)
        } else {
            tripFeedbackView.height(48)
            tripFeedbackView.isHidden = false
        }



    }



}

5.0. IQL Reference Application UX Design

Here is the overall design implementation for the IQL Reference Application:

6.0. Publisher Integration Testing Checklist

Follow the testing checklist given below while testing the Build a Trip Experience process:

  • Trips Page: Ensure that on the Trips page, all the user's recent trips are displayed accurately.

  • Pagination: If the Trips page contains pagination, add additional trips accurately, as needed from time to time.

  • Trip Feedback: Ensure that the user can delete the trips where the user was not the driver. When a trip is deleted, it should be removed immediately from the Trips page. If trip deletion does not occur owing to internet issues, provide a retry option to the user.

  • Trip Deletion Logic: Ensure that the user can only delete trips that are less than 7 days old. Additionally, ensure that the delete option is unavailable once the user receives an offer.

  • Trip Detail: When the user taps on a particular trip, enable the Trips page to accurately display all the trip details.