'use strict'; // Autocomplete setup var setupInputAutocompleteEventHandler = function (elementName, MapSvc, addressCallback, countryRestriction) { var input = (document.getElementById(elementName)); var autocomplete = new google.maps.places.Autocomplete(input); // biases results to map bounds - equivalent to calling setBounds or passing bounds in with AutocompleteOptions autocomplete.bindTo('bounds', MapSvc.map); if (countryRestriction) { var componentRestriction = { 'country': countryRestriction }; autocomplete.setComponentRestrictions(componentRestriction); } google.maps.event.addListener(autocomplete, 'place_changed', function () { var place = autocomplete.getPlace(); addressCallback(place, input); }); }; var getBackLink = function ($location, defaultLink) { var searchParms = $location.search(); if (searchParms['back']) { return searchParms['back']; } return defaultLink; }; var dismissCurrentOpenModal = function () { $('.modal-content > .ng-scope').each(function () { try { $(this).scope().$dismiss(); } catch (_) { } }); }; /* Controllers */ var controllers = angular.module('afroApp.controllers', []); /************************************/ /**** HELLO CONTROLLER **************/ /************************************/ controllers.controller('HelloCtrl', ['$scope', function ($scope) { console.log('Hello controller is booting...'); $scope.message = "Hello angular"; $scope.init = function () { if (!$scope.isPhoneGap) { var parentElement = document.getElementById("deviceready"); var listeningElement = parentElement.querySelector('.listening'); var receivedElement = parentElement.querySelector('.received-web'); listeningElement.setAttribute('style', 'display:none;'); receivedElement.setAttribute('style', 'display:block;'); } }; $scope.init(); }]); /************************************/ /**** PICKUP CONTROLLER *************/ /************************************/ controllers.controller('PickupCtrl', ['$scope', '$timeout', '$location', 'TripSvc', 'MapSvc', function ($scope, $timeout, $location, TripSvc, MapSvc) { $scope.tripSvc = TripSvc; $scope.setPickupLocationButtonDisabled = true; var fromConfirmationTemplate = '
Set pickup location
'; $scope.detectUserLocation = function () { $scope.getLocationMarkerLoading = true; MapSvc.getLocationFromBrowser(reverseGeocodeCallback).catch( function (error) { alert(error); } ).then(function (result) { $scope.getLocationMarkerLoading = false; }); }; $scope.clearFromAddress = function () { // It's really hard to clear an input that is bound to places autocomplete // http://stackoverflow.com/questions/14384004/how-to-clear-the-input-bound-to-the-google-places-autocomplete // Blur did nothing for us - this results in the most acceptable behaviour TripSvc.fromAddress = null; var fromInput = document.getElementById('from-input'); angular.element(fromInput).val(null); }; $scope.setFromLocation = function () { var mapCenter = MapSvc.map.getCenter(); TripSvc.fromLatLng = mapCenter; if (!TripSvc.fromAddress) { MapSvc.reverseGeocode(mapCenter, emptyFromAddressReverseGeocodeCallback); return; } MapSvc.mapOptions.center = TripSvc.fromLatLng; MapSvc.mapOptions.zoom = MapSvc.map.getZoom(); $location.path(destinationRoute); if (ga) { ga('send', 'event', 'Set Pickup Location', 'click'); } }; $scope.enableSetPickupLocationButton = function () { $scope.setPickupLocationButtonDisabled = false; $scope.$apply(); }; $scope.disableSetPickupLocationButton = function () { $scope.setPickupLocationButtonDisabled = true; $scope.$apply(); }; var reverseGeocodeCallback = function (geocoderResult) { TripSvc.fromLatLng = MapSvc.map.getCenter(); TripSvc.fromAddress = geocoderResult.formatted_address; TripSvc.fromAddressComponents = geocoderResult.address_components; $scope.enableSetPickupLocationButton(); $scope.tripSvc.getBranchInfo(); // var addressHandler = new AddressHandler(); // console.log("street number: '" + addressHandler.getStreetNumber(geocoderResult.address_components) + "'"); // console.log("street name: '" + addressHandler.getStreetName(geocoderResult.address_components) + "'"); // console.log("sublocality: '" + addressHandler.getSublocality(geocoderResult.address_components) + "'"); // console.log("locality: '" + addressHandler.getLocality(geocoderResult.address_components) + "'"); // console.log("suburb: '" + addressHandler.getSuburb(geocoderResult.address_components) + "'"); }; var emptyFromAddressReverseGeocodeCallback = function (geocoderResult) { TripSvc.fromLatLng = MapSvc.map.getCenter(); TripSvc.fromAddress = geocoderResult.formatted_address; TripSvc.fromAddressComponents = geocoderResult.address_components; $scope.setFromLocation(); }; var autocompleteCallback = function (place, input) { clearZoomChangedMapEventHandler(); if (!place.geometry) { return; } // If the place has a geometry, then present it on a map. if (place.geometry.viewport) { MapSvc.map.fitBounds(place.geometry.viewport); } else { MapSvc.map.setCenter(place.geometry.location); MapSvc.map.setZoom(17); // Why 17? Because it looks good. } TripSvc.fromLatLng = place.geometry.location; // this is not always useful (CT airport changed to just Cape Town) // and it's probably surprising anyway to have the input suddenly change to something other than what you selected // TripSvc.fromAddress = place.formatted_address; TripSvc.fromAddress = input.value; // binding issue with autocomplete TripSvc.fromAddressComponents = place.address_components; $scope.enableSetPickupLocationButton(); setupZoomChangedMapEventHandler(reverseGeocodeCallback); $scope.tripSvc.getBranchInfo(); }; var setupMapEventHandlers = function (callback) { // These ensure that reverse geocoding is started whenever the user interacts with the map google.maps.event.addListener(MapSvc.map, 'dragend', function () { var latLng = MapSvc.map.getCenter(); MapSvc.reverseGeocode(latLng, callback); }); google.maps.event.addListener(MapSvc.map, 'dragstart', function () { $scope.disableSetPickupLocationButton(); }); setupZoomChangedMapEventHandler(callback); }; var clearZoomChangedMapEventHandler = function () { google.maps.event.clearListeners(MapSvc.map, 'zoom_changed'); }; var setupZoomChangedMapEventHandler = function (callback) { google.maps.event.addListener(MapSvc.map, 'zoom_changed', function () { $scope.disableSetPickupLocationButton(); var latLng = MapSvc.map.getCenter(); MapSvc.reverseGeocode(latLng, callback); }); }; $scope.taxiTypeChanged = function (taxiType) { MapSvc.filterVisibleDriverMarkers(TripSvc.mappedTaxiType(taxiType)); }; $scope.setAvailableTaxiTypes = function () { var taxiTypes = $scope.tripSvc.taxiTypeList(); $('.slider-dot-wrap label').remove(); // need the timeout as marginLeftAdjustment isn't being applied correctly on first load of app $timeout(function () { for (var i = 0; i < taxiTypes.length; i++) { // Create a new element and position it with percentages var el = $('').css('left', (i / (taxiTypes.length - 1) * 98) + '%'); // Add the element inside #slider $(".slider-dot-wrap").append(el); // console.log(el.width()); var elWidth = el.width(); var marginLeftAdjustment = elWidth * 0.405; el.css({'margin-left': -marginLeftAdjustment}); } }, 0); //TODO: switching between branches makes the range slider go funny... not sure how to fix this yet // specifically: switching from a branch with 3 taxi types, having selected the middle taxi type // and going to a branch with 2 taxi types you'd expect maybe the slider to then be set on the rightmost taxi type // but it stays in the middle of the range controller even though theres no option there... // need the timeout because apparently an apply is already happening but doesn't seem to be // affecting the range slider binding. //$timeout(function(){ // $scope.$apply(function(){ // TripSvc.myBid = myBid; // }); //}, 0); $scope.tripSvc.taxiType = String(Math.max(0, Math.min(taxiTypes.length - 1, parseInt($scope.tripSvc.taxiType)))); }; $scope.$watch('tripSvc.branch', function (newValue, oldValue, scope) { if (angular.equals(newValue, oldValue)) { return; } scope.setAvailableTaxiTypes(); }, true); $scope.checkPreconditions = $scope.checkPreconditions || function () { return true; }; $scope.init = function () { if (TripSvc.fromLatLng) { MapSvc.mapOptions.center = TripSvc.fromLatLng; } if (!angular.equals(TripSvc.branch, {})) { $scope.setAvailableTaxiTypes(); } MapSvc.init().then(function (result) { TripSvc.startGetActiveTaxisPolling(); }); setupInputAutocompleteEventHandler('from-input', MapSvc, autocompleteCallback); setupMapEventHandlers(reverseGeocodeCallback); MapSvc.associateExistingDriverMarkersWithMap(); MapSvc.filterVisibleDriverMarkers(TripSvc.mappedTaxiType(TripSvc.taxiType)); var defaultLatLng = MapSvc.mapOptions.center; MapSvc.reverseGeocode(defaultLatLng, reverseGeocodeCallback); // not using $scope.detectUserLocation on purpose, as we don't want to handle errors (bug the user) in this case MapSvc.getLocationFromBrowser(reverseGeocodeCallback); }; if (!$scope.checkPreconditions()) { console.debug('Preconditions not met'); return; } $scope.init(); }]); /************************************/ /**** DESTINATION CONTROLLER ********/ /************************************/ controllers.controller('DestinationCtrl', ['$scope', '$modal', '$timeout', '$location', 'TripSvc', 'MapSvc', function ($scope, $modal, $timeout, $location, TripSvc, MapSvc) { $scope.tripSvc = TripSvc; $scope.showLoader = false; var destinationNotSetModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'destinationnotsetmodal.html', controller: 'TripModalCtrl as modal' }); }; $scope.backToPickup = function () { $location.path(pickupRoute); }; $scope.setDestinationLocation = function () { if (!TripSvc.destinationLatLng) { destinationNotSetModal(); return; } $scope.showLoader = true; MapSvc.addDestinationLocationMarker(TripSvc.destinationLatLng); MapSvc.mapOptions.center = TripSvc.destinationLatLng; MapSvc.mapOptions.zoom = MapSvc.map.getZoom(); $location.path(fareRoute); if (ga) { ga('send', 'event', 'Set Destination', 'click'); } }; var autocompleteCallback = function (place, input) { $scope.$apply(function () { TripSvc.destinationLatLng = place.geometry.location; // this is not always useful (CT airport changed to just Cape Town) // and it's probably suprising anyway to have the input suddenly change to something other than what you selected //TripSvc.destinationAddress = place.formatted_address; TripSvc.destinationAddress = input.value; // binding issue with autocomplete TripSvc.destinationAddressComponents = place.address_components; $scope.setDestinationLocation(); }); }; $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.fromAddress) { console.log("DestinationCtrl PreCheck: don't have a from address"); return false; } if (!TripSvc.fromLatLng) { console.log("DestinationCtrl PreCheck: don't have a from latlng"); return false; } if (!TripSvc.fromAddressComponents || TripSvc.fromAddressComponents.length == 0) { console.log("DestinationCtrl PreCheck: don't have a from address components"); return false; } if (!TripSvc.taxiType) { console.log("DestinationCtrl PreCheck: don't have a taxiType"); return false; } return true; }; $scope.init = function () { MapSvc.mapOptions.center = TripSvc.fromLatLng; MapSvc.init().then(function (result) { TripSvc.startGetActiveTaxisPolling(); }); MapSvc.addFromLocationMarker(TripSvc.fromLatLng); if (TripSvc.destinationLatLng) { MapSvc.addDestinationLocationMarker(TripSvc.destinationLatLng); } // possible issue in the Congo between Kinshasa (DRC) and Brazzaville (Congo)? across borders. var countryRestriction = null; if (TripSvc.fromAddressComponents) { var addressHandler = new AddressHandler(); countryRestriction = addressHandler.getCountryShortName(TripSvc.fromAddressComponents); } setupInputAutocompleteEventHandler('destination-input', MapSvc, autocompleteCallback, countryRestriction); MapSvc.associateExistingDriverMarkersWithMap(); }; if (!$scope.checkPreconditions()) { $location.path(pickupRoute); return; } $scope.init(); }]); /************************************/ /**** FARE CONTROLLER ***************/ /************************************/ controllers.controller('FareCtrl', ['$scope', '$modal', '$timeout', '$interval', '$location', '$q', 'TripSvc', 'UserSvc', 'MapSvc', function ($scope, $modal, $timeout, $interval, $location, $q, TripSvc, UserSvc, MapSvc) { $scope.userSvc = UserSvc; $scope.tripSvc = TripSvc; var touchBidPromise; $scope.creditCardPaymentsEnabled = featureFlags.creditCardPaymentsEnabled; $scope.cashPaymentSelected = TripSvc.paymentMethod == PaymentMethods.CASH; $scope.creditCardPaymentSelected = TripSvc.paymentMethod == PaymentMethods.CREDIT_CARD; $scope.voucherPaymentSelected = TripSvc.paymentMethod == PaymentMethods.VOUCHER; $scope.getPassengerVouchersPromise = null; $scope.getStripePaymentMethodsPromise = null; $scope.getCabButtonDisabled = false; $scope.editFareSelected = false; $scope.checkFareInput = function (isFormValid) { var isBidValid = TripSvc.myBid >= TripSvc.absoluteMinimumFare && TripSvc.myBid <= TripSvc.absoluteMaximumFare; if (isFormValid && isBidValid) { // toggle ui state back to not editing $scope.editFareSelected = false } }; $scope.enableGetCabButton = function () { $scope.getCabButtonDisabled = false; }; $scope.disableGetCabButton = function () { $scope.getCabButtonDisabled = true; }; $scope.backToDestination = function () { $location.path(destinationRoute); }; $scope.backToPickup = function () { $location.path(pickupRoute); }; var runChecksAndChangeBid = function (deltaBid) { var newBid = TripSvc.myBid + deltaBid; if (TripSvc.myBid < TripSvc.absoluteMinimumFare) { TripSvc.myBid = newBid; return; } if (TripSvc.myBid > TripSvc.absoluteMaximumFare) { TripSvc.myBid = newBid; return; } if (newBid < TripSvc.absoluteMinimumFare) { return; } if (newBid > TripSvc.absoluteMaximumFare) { return; } if (TripSvc.paymentMethod != PaymentMethods.VOUCHER) { TripSvc.myBid = newBid; return; } checkVoucher(newBid).then(function (result) { if (result) { TripSvc.myBid = newBid; if (UserSvc.vouchers[0].amount < TripSvc.myBid) { TripSvc.updateProcessingFee([PaymentMethods.VOUCHER, PaymentMethods.CREDIT_CARD]); } else { TripSvc.updateProcessingFee(); } } }); }; $scope.incrementBid = function (isFormValid) { runChecksAndChangeBid(TripSvc.fareStep); $scope.checkFareInput(isFormValid); }; $scope.decrementBid = function (isFormValid) { runChecksAndChangeBid(-TripSvc.fareStep); $scope.checkFareInput(isFormValid); }; $scope.touchIncrementBid = function (isFormValid) { touchBidPromise = $interval(function () { runChecksAndChangeBid(TripSvc.fareStep); $scope.checkFareInput(isFormValid); }, 100) }; $scope.touchDecrementBid = function (isFormValid) { touchBidPromise = $interval(function () { runChecksAndChangeBid(-TripSvc.fareStep); $scope.checkFareInput(isFormValid); }, 100) }; $scope.endTouchBid = function () { $interval.cancel(touchBidPromise); }; //$scope.manualFareEntry = function(){ // var minFare = TripSvc.getAbsoluteMinimumFare(); // var maxFare = TripSvc.getAbsoluteMaximumFare(); // // var promptString = String.format("Please enter your fare (min: {0} max: {1})", minFare, maxFare); // // var fareString = prompt(promptString, TripSvc.myBid); // // try{ // var fare = parseInt(fareString); // if(isNaN(fare)){ // fare = TripSvc.myBid; // } // // fare = Math.max(minFare, fare); // fare = Math.min(maxFare, fare); // // TripSvc.myBid = fare; // } // catch(err){ // console.log(err); // } //}; var openCreditCardRequiredModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'creditcardrequiredmodal.html', controller: 'TripModalCtrl as modal' }); }; var openCreditCardRequiredAndExpiredModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'creditcardrequiredandexpiredmodal.html', controller: 'TripModalCtrl as modal' }); }; var openCreditCardExpiredModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'creditcardexpiredmodal.html', controller: 'TripModalCtrl as modal' }); }; var openVoucherInsufficientModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'voucherinsufficientmodal.html', controller: 'TripModalCtrl as modal' }); }; var openVoucherInsufficientCardChargeModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'voucherinsufficientcardchargemodal.html', controller: 'TripModalCtrl as modal' }); }; var openVoucherCurrencyIncorrectModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'vouchercurrencyincorrectmodal.html', controller: 'TripModalCtrl as modal' }); }; $scope.setCashPaymentMethod = function () { TripSvc.paymentMethod = PaymentMethods.CASH; TripSvc.updateProcessingFee(); $scope.cashPaymentSelected = true; $scope.creditCardPaymentSelected = false; $scope.voucherPaymentSelected = false; }; var checkCreditCard = function () { return $scope.getStripePaymentMethodsPromise.then(function (result) { var defaultCreditCard = UserSvc.getDefaultCreditCard(); if (defaultCreditCard == null) { $location.search({back: fareRoute}); $location.path(passengerPaymentMethodsRoute); return false; } if (defaultCreditCard.expired) { openCreditCardExpiredModal(); $location.search({back: fareRoute}); $location.path(passengerPaymentMethodsRoute); return false; } return true; }); }; $scope.setCreditCardPaymentMethod = function () { checkCreditCard().then(function (result) { if (result) { TripSvc.paymentMethod = PaymentMethods.CREDIT_CARD; TripSvc.updateProcessingFee(); $scope.cashPaymentSelected = false; $scope.creditCardPaymentSelected = true; $scope.voucherPaymentSelected = false; } }); }; var checkVoucher = function (bidAmount) { var deferred = $q.defer(); $scope.getPassengerVouchersPromise.then(function (data) { if (UserSvc.vouchers.length < 1) { $location.search({back: fareRoute}); $location.path(passengerVoucherRoute); deferred.resolve(false); return; } if (UserSvc.vouchers[0].currency != TripSvc.currencyCode) { openVoucherCurrencyIncorrectModal(); //TODO: decide whether to take the user to the Vouchers screen //$location.search({back: fareRoute}); //$location.path(passengerVoucherRoute); deferred.resolve(false); return; } $scope.getStripePaymentMethodsPromise.then(function (result) { var defaultCreditCard = UserSvc.getDefaultCreditCard(); if (UserSvc.vouchers[0].credit_card_required) { if (defaultCreditCard == null) { openCreditCardRequiredModal(); $location.search({back: fareRoute, newPaymentMethod: PaymentMethods.VOUCHER}); $location.path(passengerPaymentMethodsRoute); deferred.resolve(false); return; } if (defaultCreditCard.expired) { openCreditCardRequiredAndExpiredModal(); $location.search({back: fareRoute, newPaymentMethod: PaymentMethods.VOUCHER}); $location.path(passengerPaymentMethodsRoute); deferred.resolve(false); return; } } if (UserSvc.vouchers[0].amount < bidAmount) { if (defaultCreditCard != null && !defaultCreditCard.expired) { openVoucherInsufficientCardChargeModal(); deferred.resolve(true); return; } openVoucherInsufficientModal(); deferred.resolve(false); return; } deferred.resolve(true); }); }); return deferred.promise; }; $scope.setVoucherPaymentMethod = function () { checkVoucher(TripSvc.myBid).then(function (result) { if (result) { TripSvc.paymentMethod = PaymentMethods.VOUCHER; if (UserSvc.vouchers[0].amount < TripSvc.myBid) { TripSvc.updateProcessingFee([PaymentMethods.VOUCHER, PaymentMethods.CREDIT_CARD]); } else { TripSvc.updateProcessingFee(); } $scope.cashPaymentSelected = false; $scope.creditCardPaymentSelected = false; $scope.voucherPaymentSelected = true; } }); }; $scope.getCab = function () { $scope.disableGetCabButton(); TripSvc.createJob().then( function (response) { $location.path(waitingAcceptJobRoute); }, function (response) { $scope.enableGetCabButton(); $scope.$apply(); } ); }; $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.destinationAddress) { console.log("FareCtrl PreCheck: don't have a destination address"); return false; } if (!TripSvc.destinationLatLng) { console.log("FareCtrl PreCheck: don't have a destination latlng"); return false; } if (!TripSvc.destinationAddressComponents || TripSvc.destinationAddressComponents.length == 0) { console.log("FareCtrl PreCheck: don't have a destination address components"); return false; } if (!TripSvc.taxiType) { console.log("FareCtrl PreCheck: don't have a taxiType"); return false; } return true; }; $scope.init = function () { $scope.getPassengerVouchersPromise = UserSvc.getPassengerVouchers(); // TODO: refactor this as UserSvc.getDefaultCreditCardToken makes its own call to getStripePaymentMethods and if there are no cards we'll hit the server // twice. Watch out for check functions above which also depend on $scope.getStripePaymentMethodsPromise $scope.getStripePaymentMethodsPromise = UserSvc.getStripePaymentMethods() .then(function (result) { UserSvc.getDefaultCreditCardToken() .then(function (defaultCreditCardToken) { if (defaultCreditCardToken != null) { TripSvc.creditCardToken = defaultCreditCardToken; } }); }); TripSvc.setDistanceDrawRouteAndSetEstimatedFare().then(function (data) { // re-run checks if user has navigated back // TODO: consider reverting to cash if checks fail, otherwise user might get taken to the cc/voucher screen to fix the problem, not fix it, come back // here and it all starts over again if ($scope.creditCardPaymentSelected) { checkCreditCard().then(function (result) { if (result) { TripSvc.updateProcessingFee(); } }); } if ($scope.voucherPaymentSelected) { checkVoucher(TripSvc.myBid).then(function (result) { if (result) { if (UserSvc.vouchers[0].amount < TripSvc.myBid) { TripSvc.updateProcessingFee([PaymentMethods.VOUCHER, PaymentMethods.CREDIT_CARD]); } else { TripSvc.updateProcessingFee(); } } }); } }); TripSvc.updateProcessingFee(); }; if (!$scope.checkPreconditions()) { console.debug('Preconditions not met'); $location.path(pickupRoute); return; } $scope.init(); }]); /************************************/ /**** WAITING ACCEPT JOB CONTROLLER */ /************************************/ controllers.controller('WaitingAcceptJobCtrl', ['$scope', '$modal', '$timeout', '$location', 'TripSvc', 'MapSvc', function ($scope, $modal, $timeout, $location, TripSvc, MapSvc) { $scope.tripSvc = TripSvc; $scope.mapSvc = MapSvc; $scope.legitimateLocationChange = false; var DriverEnRoute = function () { dismissCurrentOpenModal(); $timeout(function () { $('.modal-content > .ng-scope').scope().$dismiss(); }, 10000); $modal.open({ templateUrl: 'driverenroutemodal.html', controller: 'TripModalCtrl as modal' }); }; var noResponseFromDrivers = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'nodriversmodal.html', controller: 'TripModalCtrl as modal' }); }; var driversRejected = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'driversrejectmodal.html', controller: 'TripModalCtrl as modal' }); }; var noResponseTimer = $timeout(function () { TripSvc.cancelJob(); $scope.legitimateLocationChange = true; if (TripSvc.jobDriversRejected > 0) { driversRejected(); $location.path(fareRoute); } else { noResponseFromDrivers(); $location.path(fareRoute); } }, 90000); $scope.cancelJobModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'canceljobmodal.html', scope: $scope }); }; $scope.cancelJob = function () { TripSvc.cancelJob(); $scope.legitimateLocationChange = true; $timeout.cancel(noResponseTimer); $location.path(fareRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler if (ga) { ga('send', 'event', 'Job Cancelled', 'click'); } }; // this event is broadcast by the message handler onto the rootScope $scope.$on(jobAcceptedEvent, function (event, driver_device_id) { // TODO: rather show popover modal on the next screen instead of pausing on this one to show this dialogue DriverEnRoute(); $scope.legitimateLocationChange = true; $location.path(waitingDriverEnrouteRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler $timeout.cancel(noResponseTimer); }); $scope.$on("$locationChangeStart", function (event, next, current) { //TODO: conflict with loginErrorHttpInterceptor (confirm modal pops up) if ($scope.legitimateLocationChange) { $timeout.cancel(noResponseTimer); return; } $scope.cancelJobModal(); event.preventDefault(); }); $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.jobId) { console.log("WaitingAcceptJobCtrl PreCheck: don't have a jobId"); return false; } return true; }; $scope.init = function () { // go back to the pickup location so we can see cabs around us while waiting for job MapSvc.mapOptions.center = TripSvc.fromLatLng; MapSvc.init().then(function (result) { TripSvc.startGetActiveTaxisPolling(); }); MapSvc.addFromLocationMarker(TripSvc.fromLatLng); MapSvc.addDestinationLocationMarker(TripSvc.destinationLatLng); MapSvc.associateExistingDriverMarkersWithMap(); noResponseTimer; }; if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; $location.path(pickupRoute); return; } if (ga) { ga('send', 'event', 'Waiting for Accept', 'click'); } $scope.init(); }]); /*************************************/ /* WAITING DRIVER ENROUTE CONTROLLER */ /*************************************/ controllers.controller('WaitingDriverEnrouteCtrl', ['$scope', '$timeout', '$location', 'TripSvc', 'MapSvc', '$modal', function ($scope, $timeout, $location, TripSvc, MapSvc, $modal) { $scope.tripSvc = TripSvc; $scope.mapSvc = MapSvc; $scope.passengerAlertedAboutDriver = false; $scope.legitimateLocationChange = false; $scope.passengerTripControlsEnabled = featureFlags.passengerTripControlsEnabled; $scope.abandonJobModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'abandonjobmodal.html', scope: $scope }); }; var DriverAbandonedModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'driverabandonedmodal.html', controller: 'TripModalCtrl as modal' }); }; var DriverNoShowModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'drivernoshowmodal.html', controller: 'TripModalCtrl as modal' }); }; $scope.driverCloseModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'driverclosemodal.html', scope: $scope }); }; $scope.abandonJob = function () { TripSvc.abandonJob(); $scope.legitimateLocationChange = true; $location.path(fareRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler if (ga) { ga('send', 'event', 'Passenger Abandoned', 'click'); } }; $scope.collectJob = function () { TripSvc.collectJob(); $scope.legitimateLocationChange = true; $location.path(inTransitRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler if (ga) { ga('send', 'event', 'Passenger Collected', 'click'); } }; $scope.tripDetails = function () { $scope.legitimateLocationChange = true; $location.path(tripDetailsRoute); }; // this event is broadcast by the message handler onto the rootScope $scope.$on(jobNoShowEvent, function (event, job_id) { DriverNoShowModal(); $scope.legitimateLocationChange = true; $location.path(fareRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler }); // this event is broadcast by the message handler onto the rootScope $scope.$on(driverAbandonedEvent, function (event, job_id) { DriverAbandonedModal(); $scope.legitimateLocationChange = true; $location.path(fareRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler }); // this event is broadcast by the message handler onto the rootScope $scope.$on(driverCollectedEvent, function (event, job_id) { $scope.legitimateLocationChange = true; $location.path(inTransitRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler }); $scope.$on("$locationChangeStart", function (event, next, current) { //TODO: conflict with loginErrorHttpInterceptor (confirm modal pops up) if ($scope.legitimateLocationChange) { return; } $scope.abandonJobModal(); event.preventDefault(); }); $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.jobId) { console.log("WaitingDriverEnrouteCtrl PreCheck: don't have a jobId"); return false; } return true; }; $scope.init = function () { // go back to the pickup location so we can see cabs around us while waiting for driver to collect us MapSvc.mapOptions.center = TripSvc.fromLatLng; MapSvc.init().then(function (result) { TripSvc.startGetFlaggedTaxiPolling(); }); MapSvc.addFromLocationMarker(TripSvc.fromLatLng); MapSvc.addDestinationLocationMarker(TripSvc.destinationLatLng); var enRouteDriverMarker = MapSvc.findDriverMarker(TripSvc.driverDeviceId); if (enRouteDriverMarker) { MapSvc.addOrUpdateDriverMarker(enRouteDriverMarker.driver, true); } $scope.$watch('tripSvc.driverDistanceFromPickup', function (newValue, oldValue, scope) { if (angular.equals(newValue, oldValue)) { return; } if (newValue < 0.2 && !scope.passengerAlertedAboutDriver) { scope.passengerAlertedAboutDriver = true; console.log('Driver is arriving...'); multiVibrate(4, 200, 200); scope.driverCloseModal(); } }, true); }; if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; $location.path(pickupRoute); return; } if (ga) { ga('send', 'event', 'Driver Enroute', 'click'); } $scope.init(); }]); /*************************************/ /***** IN TRANSIT CONTROLLER *********/ /*************************************/ controllers.controller('InTransitCtrl', ['$scope', '$modal', '$timeout', '$location', 'TripSvc', 'MapSvc', function ($scope, $modal, $timeout, $location, TripSvc, MapSvc) { $scope.tripSvc = TripSvc; $scope.mapSvc = MapSvc; $scope.legitimateLocationChange = false; $scope.passengerTripControlsEnabled = featureFlags.passengerTripControlsEnabled; var setMapOptionsCenterToFlaggedDriverLocation = function () { var flaggedDriver = MapSvc.findDriverMarker(TripSvc.driverDeviceId).driver; var driverLatLng = new google.maps.LatLng(flaggedDriver.lat, flaggedDriver.lng); MapSvc.mapOptions.center = driverLatLng; }; var CantNavigateBackModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'cantnavigateback.html', controller: 'TripModalCtrl as modal' }); }; $scope.tripDetails = function () { $location.search({back: inTransitRoute}); $scope.legitimateLocationChange = true; $location.path(tripDetailsRoute); }; $scope.completeJob = function () { setMapOptionsCenterToFlaggedDriverLocation(); TripSvc.completeJob(); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler $scope.legitimateLocationChange = true; $location.path(ratingRoute); if (ga) { ga('send', 'event', 'Job Completed', 'click'); } }; // this event is broadcast by the message handler onto the rootScope $scope.$on(driverCompleteJobEvent, function (event, job_id) { setMapOptionsCenterToFlaggedDriverLocation(); $scope.legitimateLocationChange = true; $location.path(ratingRoute); //TODO: be consistent what happens in success callback on TripSvc vs having event fire by messageHandler }); $scope.$on(flaggedDriverUpdatedEvent, function (event, flaggedDriver) { var driverLatLng = new google.maps.LatLng(flaggedDriver.lat, flaggedDriver.lng); MapSvc.map.setCenter(driverLatLng); }); $scope.$on("$locationChangeStart", function (event, next, current) { if ($scope.legitimateLocationChange) { return; } CantNavigateBackModal(); event.preventDefault(); }); $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.jobId) { console.log("InTransitCtrl PreCheck: don't have a jobId"); return false; } return true; }; $scope.init = function () { MapSvc.init().then(function (result) { TripSvc.startGetFlaggedTaxiPolling(); }); MapSvc.addDestinationLocationMarker(TripSvc.destinationLatLng); }; if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; $location.path(pickupRoute); return; } $scope.init(); }]); /*************************************/ /***** RATING CONTROLLER *************/ /*************************************/ controllers.controller('RatingCtrl', ['$scope', '$modal', '$timeout', '$location', 'TripSvc', 'MapSvc', function ($scope, $modal, $timeout, $location, TripSvc, MapSvc) { $scope.tripSvc = TripSvc; $scope.mapSvc = MapSvc; $scope.rating = 0; $scope.legitimateLocationChange = false; var RateJobModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'thankyoumodal.html', controller: 'TripModalCtrl as modal' }); }; var CantNavigateBackModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'cantnavigateback.html', controller: 'TripModalCtrl as modal' }); }; $scope.rateJob = function () { TripSvc.rateDriver($scope.rating).then(function (response) { TripSvc.finishJobAndClearState(); if (ga) { ga('send', 'event', 'Rate Job', 'click'); } }); RateJobModal(); $timeout(function () { $(dismissCurrentOpenModal()).scope().$dismiss(); }, 10000); $scope.legitimateLocationChange = true; $location.path(pickupRoute); }; $scope.finish = function () { TripSvc.finishJobAndClearState(); $scope.legitimateLocationChange = true; $location.path(pickupRoute); }; $scope.ratingStates = [ {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'} ]; $scope.$on("$locationChangeStart", function (event, next, current) { if ($scope.legitimateLocationChange) { return; } CantNavigateBackModal(); event.preventDefault(); }); $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!TripSvc.jobId) { console.log("RatingCtrl PreCheck: don't have a jobId"); return false; } return true; }; $scope.init = function () { MapSvc.init().then(function (result) { TripSvc.startGetActiveTaxisPolling(); // should we just stop all polling at this stage? }); }; if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; $location.path(pickupRoute); return; } $scope.init(); }]); /************************************/ /**** ON-BOARD CONTROLLER ***********/ /************************************/ controllers.controller('OnBoardCtrl', ['$scope', '$location', '$translate', 'UserSvc', function ($scope, $location, $translate, UserSvc) { $scope.userSvc = UserSvc; $scope.multiLanguage = featureFlags.multiLanguage; $scope.lang = null; $scope.supportedLanguages = [{ lang: 'en', label: 'English' }, { lang: 'fr', label: 'Fran�ais' }]; for (var i = 0; i < $scope.supportedLanguages.length; i++) { if ($scope.supportedLanguages[i].lang == $translate.use()) { $scope.lang = $scope.supportedLanguages[i]; break; } } $scope.updateLanguage = function () { $translate.use($scope.lang.lang); }; $scope.takeTour = function () { $location.search({back: onBoardRoute}); $scope.legitimateLocationChange = true; $location.path(takeTourRoute); }; }]); /************************************/ /**** SIGN UP CONTROLLER ************/ /************************************/ controllers.controller('SignUpCtrl', ['$scope', '$location', '$timeout', 'UserSvc', function ($scope, $location, $timeout, UserSvc) { $scope.userSvc = UserSvc; $scope.name = ""; $scope.surname = ""; $scope.emailAddress = ""; $scope.mobileNumber = ""; $scope.backToOnboard = function () { $location.path(onBoardRoute); }; $scope.termsAndConditions = function () { $location.search({back: signUpRoute}); $location.path(termsAndConditionsRoute); }; $scope.privacyPolicy = function () { $location.search({back: signUpRoute}); $location.path(privacyPolicyRoute); }; var signUpResultHandler = function (data) { if (data.data.result) { //TODO: use settings object if (data.data.redirect_url) { $location.path(data.data.redirect_url); return; } if (featureFlags.creditCardPaymentsEnabled) { $location.path(signUpCreditCardRoute); } else { $location.path(signUpVoucherRoute); } } else { alert(String.format('Failed to sign up: {0} - {1}', data.data.errorCode, data.data.errorMessage)); } }; $scope.signUpWithFacebook = function () { FB.login(function (response) { if (response.status === 'connected') { UserSvc.socialSignUp($scope.name, $scope.emailAddress, $scope.mobileNumber).then(signUpResultHandler); //UserSvc.phonegapSocialSignUp($scope.name, $scope.emailAddress, $scope.mobileNumber, response.authResponse.accessToken).then(signUpResultHandler); } else { console.log('[ERROR] User cancelled login or did not fully authorise (Facebook).') } }, {scope: 'public_profile,email'}); }; $scope.signUp = function ($event) { var password = $event.currentTarget.elements['password-input'].value; // was done on purpose so as not to store the password on the scope var mobileCountryCode = $event.currentTarget.elements['mobile-country-code'].value; //TODO: can't have a 2-way binding on a hidden field, find a better way $scope.userSvc.signUp($scope.name, $scope.surname, $scope.emailAddress, password, $scope.mobileNumber, mobileCountryCode).then(signUpResultHandler); }; // init intlTelInput $timeout(function () { var telInput = $("#mobile-input"); var hiddenField = $("#mobile-country-code"); var errorMsg = $(".country-invalid"); var validMsg = $(".country-valid"); var countryData = $.fn.intlTelInput.getCountryData(); telInput.intlTelInput({ utilsScript: "/lib/intlTelInput/tel-utils.js", defaultCountry: "auto", preferredCountries: ["ng"], autoPlaceholder: true, numberType: "MOBILE" }); // on blur: validate telInput.blur(function () { if ($.trim(telInput.val())) { if (telInput.intlTelInput("isValidNumber")) { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.removeClass("hide"); } else { telInput.addClass("tel-error"); // TODO: set ng-invalid or something to prevent form submission? errorMsg.removeClass("hide"); validMsg.addClass("hide"); } } }); // on keydown: reset telInput.keydown(function () { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.addClass("hide"); }); telInput.change(function () { var countryCode = telInput.intlTelInput("getSelectedCountryData").dialCode; hiddenField.val(countryCode); }); }, 0); }]); /************************************/ /**** SIGN IN CONTROLLER ************/ /************************************/ controllers.controller('SignInCtrl', ['$scope', '$location', 'UserSvc', function ($scope, $location, UserSvc) { $scope.userSvc = UserSvc; $scope.backToOnboard = function () { $location.path(onBoardRoute); }; var signInResultHandler = function (data) { if (data.data.result) { //TODO: use settings object if (data.data.redirect_url) { $location.path(data.data.redirect_url); return; } //TODO: test that this works with both API versions // TripSvc.getPassengerFlagsAndStatus().then(function(response){ // if(!response.data.result || !response.data.data){ // return; // } // // TripSvc.recoveryHandler.recoverState(response.data); // }); $location.path(pickupRoute); } else { alert(String.format('Failed to sign in: {0} - {1}', data.data.errorCode, data.data.errorMessage)); } }; $scope.loginWithFacebook = function () { FB.login(function (response) { if (response.status === 'connected') { UserSvc.socialSignIn().then(signInResultHandler); //UserSvc.phonegapSocialSignIn(response.authResponse.accessToken).then(signInResultHandler); } else { console.log('[ERROR] User cancelled login or did not fully authorise (Facebook).'); } }, {scope: 'public_profile,email'}); if (ga) { ga('send', 'event', 'Facebook Sign In', 'click'); } }; $scope.signIn = function ($event) { var emailAddress = $event.currentTarget.elements['email-input'].value; var password = $event.currentTarget.elements['password-input'].value; $scope.userSvc.signIn(emailAddress, password).then(signInResultHandler); if (ga) { ga('send', 'event', 'Normal Sign In', 'click'); } }; }]); /********************************************/ /**** COMPLETE PASSENGER PROFILE CONTROLLER */ /********************************************/ controllers.controller('CompletePassengerProfileCtrl', ['$scope', '$location', 'UserSvc', function ($scope, $location, UserSvc) { $scope.userSvc = UserSvc; $scope.completeProfile = function ($event) { var mobileCountryCode = $event.currentTarget.elements['mobile-country-code'].value; //TODO: can't have a 2-way binding on a hidden field, find a better way $scope.userSvc.completeProfile($scope.name, $scope.surname, $scope.emailAddress, mobileCountryCode, $scope.mobileNumber) .then(function (data) { if (data.data.result) { $location.path(pickupRoute); } }); }; UserSvc.getPassengerDetails().then(function (result) { // TODO: refer directly to properties on userSvc in template? $scope.name = UserSvc.name; $scope.surname = UserSvc.surname; $scope.emailAddress = UserSvc.emailAddress; $scope.mobileNumber = UserSvc.mobileNumber; $scope.mobileCountryCode = UserSvc.mobileCountryCode; // init intlTelInput $timeout(function () { var telInput = $("#mobile-input"); var hiddenField = $("#mobile-country-code"); var errorMsg = $(".country-invalid"); var validMsg = $(".country-valid"); var countryData = $.fn.intlTelInput.getCountryData(); telInput.intlTelInput({ utilsScript: "/lib/intlTelInput/tel-utils.js", defaultCountry: "auto", preferredCountries: ["ng"], autoPlaceholder: true, numberType: "MOBILE" }); // on blur: validate telInput.blur(function () { if ($.trim(telInput.val())) { if (telInput.intlTelInput("isValidNumber")) { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.removeClass("hide"); } else { telInput.addClass("tel-error"); // TODO: set ng-invalid or something to prevent form submission? errorMsg.removeClass("hide"); validMsg.addClass("hide"); } } }); // on keydown: reset telInput.keydown(function () { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.addClass("hide"); }); telInput.change(function () { var countryCode = telInput.intlTelInput("getSelectedCountryData").dialCode; hiddenField.val(countryCode); }); // FIXME: this is a hack, we should use setNumber (https://github.com/Bluefieldscom/intl-tel-input) // need to return a fully formatted international number from the server // also see related todos in editProfile function and template $timeout(function () { if (angular.isDefined(UserSvc.mobileCountryCode)) { for (var i = 0; i < countryData.length; i++) { if (countryData[i].dialCode == UserSvc.mobileCountryCode) { telInput.intlTelInput("selectCountry", countryData[i].iso2); break; } } } }, 500); }, 0); }); }]); /************************************/ /**** EMAIL CONFIRMATION CONTROLLER */ /************************************/ controllers.controller('EmailConfirmationCtrl', ['$scope', '$location', 'UserSvc', function ($scope, $location, UserSvc) { $scope.userSvc = UserSvc; $scope.backToOnboard = function () { $location.path(onBoardRoute); }; }]); /*******************************************/ /**** FORGOT PASSWORD PASSENGER CONTROLLER */ /*******************************************/ controllers.controller('ForgotPasswordPassengerCtrl', ['$scope', '$location', 'UserSvc', function ($scope, $location, UserSvc) { $scope.userSvc = UserSvc; $scope.backToOnboard = function () { $location.path(onBoardRoute); }; var forgotPasswordResultsHandler = function (data) { if (data.data.result) { if (data.data.redirect_url) { $location.path(data.data.redirect_url); return; } alert('An email will be sent to you with a link to use to reset your password') } else { alert(String.format('Failed to sign in: {0} - {1}', data.data.errorCode, data.data.errorMessage)); } }; $scope.forgot = function ($event) { var emailAddress = $event.currentTarget.elements['email-input'].value; $scope.userSvc.forgotPassword(emailAddress).then(forgotPasswordResultsHandler); }; }]); /***************************************/ /**** PASSWORD RESET DRIVER CONTROLLER */ /***************************************/ controllers.controller('PasswordResetDriverCtrl', ['$scope', '$location', '$routeParams', 'UserSvc', function ($scope, $location, $routeParams, UserSvc) { $scope.userSvc = UserSvc; $scope.driverId = $routeParams['userId']; $scope.validationToken = $routeParams['validationToken']; var passwordResetResultsHandler = function (data) { if (data.data.result) { //TODO: use settings object if (data.data.redirect_url) { $location.path(data.data.redirect_url); return; } alert('Your password has successfully been reset!'); $location.path(passwordResetDriverMessageRoute); } else { alert(String.format('Failed to reset password: {0} - {1}', data.data.errorCode, data.data.errorMessage)); } }; $scope.reset = function ($event) { var password = $event.currentTarget.elements['password-input'].value; var passwordConfirmation = $event.currentTarget.elements['password-confirmation-input'].value; if (password != passwordConfirmation) { alert('The passwords do not match!'); return; } //kind, id, validationToken, new_password, password_confirmation $scope.userSvc.resetPassword('driver', $scope.driverId, $scope.validationToken, password, passwordConfirmation).then(passwordResetResultsHandler); }; }]); /******************************************/ /**** PASSWORD RESET PASSENGER CONTROLLER */ /******************************************/ controllers.controller('PasswordResetPassengerCtrl', ['$scope', '$location', '$routeParams', 'UserSvc', function ($scope, $location, $routeParams, UserSvc) { $scope.userSvc = UserSvc; $scope.passengerId = $routeParams['userId']; $scope.validationToken = $routeParams['validationToken']; $scope.backToOnboard = function () { $location.path(onBoardRoute); }; var passwordResetResultsHandler = function (data) { if (data.data.result) { //TODO: use settings object if (data.data.redirect_url) { $location.path(data.data.redirect_url); return; } alert('Your password has successfully been reset!'); $location.path(signInRoute); } else { alert(String.format('Failed to reset password: {0} - {1}', data.data.errorCode, data.data.errorMessage)); } }; $scope.reset = function ($event) { var password = $event.currentTarget.elements['password-input'].value; var passwordConfirmation = $event.currentTarget.elements['password-confirmation-input'].value; if (password != passwordConfirmation) { alert('The passwords do not match!'); return; } //kind, id, validationToken, new_password, password_confirmation $scope.userSvc.resetPassword('passenger', $scope.passengerId, $scope.validationToken, password, passwordConfirmation).then(passwordResetResultsHandler); }; }]); /************************************/ /***** PASSENGER USER NAVIGATION ****/ /************************************/ controllers.controller('PassengerNavigationCtrl', ['$scope', '$location', 'UserSvc', function ($scope, $location, UserSvc) { $scope.menuOpened = false; $scope.toggleMenu = function (event) { $scope.menuOpened = !($scope.menuOpened); //Without this, menu will be hidden immediately. event.stopPropagation(); }; window.onclick = function () { if ($scope.menuOpened) { $scope.menuOpened = false; $scope.$apply(); } }; $scope.userSvc = UserSvc; $scope.creditCardPaymentsEnabled = featureFlags.creditCardPaymentsEnabled; $scope.passengerAccountRoute = function () { if ($location.path() != passengerAccountRoute) { $location.path(passengerAccountRoute); } else { $scope.menuOpened = false; } }; $scope.pickupRoute = function () { if ($location.path() != pickupRoute) { $location.path(pickupRoute); } else { $scope.menuOpened = false; } }; $scope.passengerPaymentMethodsRoute = function () { if ($location.path() != passengerPaymentMethodsRoute) { $location.path(passengerPaymentMethodsRoute); } else { $scope.menuOpened = false; } }; $scope.passengerTripHistoryRoute = function () { if ($location.path() != passengerTripHistoryRoute) { $location.path(passengerTripHistoryRoute); } else { $scope.menuOpened = false; } }; $scope.passengerVoucherRoute = function () { if ($location.path() != passengerVoucherRoute) { $location.path(passengerVoucherRoute); } else { $scope.menuOpened = false; } }; $scope.passengerInformationRoute = function () { if ($location.path() != passengerInformationRoute) { $location.path(passengerInformationRoute); } else { $scope.menuOpened = false; } }; $scope.takeTourRoute = function () { if ($location.path() != takeTourRoute) { $location.path(takeTourRoute); } else { $scope.menuOpened = false; } }; UserSvc.getPassengerDetails(); }]); /************************************/ /****** PASSENGER ACCOUNT AREA ******/ /************************************/ controllers.controller('PassengerAccountCtrl', ['$scope', '$timeout', '$location', 'UserSvc', function ($scope, $timeout, $location, UserSvc) { $scope.userSvc = UserSvc; $scope.showSuccess = false; $scope.hideSaveButtonTextUntilAnimationFinished = false; $scope.logOff = function () { $scope.userSvc.signOut().then(function (data) { if (data.data.result) { $location.path(signInRoute); } }); }; $scope.editProfile = function ($event) { var mobileCountryCode = $event.currentTarget.elements['mobile-country-code'].value; //TODO: can't have a 2-way binding on a hidden field, find a better way $scope.userSvc.editProfile(mobileCountryCode).then(function (data) { if (data.data.result) { $scope.showSuccess = true; $scope.hideSaveButtonTextUntilAnimationFinished = true; $timeout(function () { $scope.showSuccess = false; }, 2000); $timeout(function () { $scope.hideSaveButtonTextUntilAnimationFinished = false; }, 2300); } }); }; UserSvc.getPassengerDetails().then(function (result) { // init intlTelInput $timeout(function () { var telInput = $("#mobile-input"); var hiddenField = $("#mobile-country-code"); var errorMsg = $(".country-invalid"); var validMsg = $(".country-valid"); var countryData = $.fn.intlTelInput.getCountryData(); telInput.intlTelInput({ utilsScript: "/lib/intlTelInput/tel-utils.js", defaultCountry: "auto", preferredCountries: ["ng"], autoPlaceholder: true, numberType: "MOBILE" }); // on blur: validate telInput.blur(function () { if ($.trim(telInput.val())) { if (telInput.intlTelInput("isValidNumber")) { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.removeClass("hide"); } else { telInput.addClass("tel-error"); // TODO: set ng-invalid or something to prevent form submission? errorMsg.removeClass("hide"); validMsg.addClass("hide"); } } }); // on keydown: reset telInput.keydown(function () { telInput.removeClass("tel-error"); errorMsg.addClass("hide"); validMsg.addClass("hide"); }); telInput.change(function () { var countryCode = telInput.intlTelInput("getSelectedCountryData").dialCode; hiddenField.val(countryCode); }); // FIXME: this is a hack, we should use setNumber (https://github.com/Bluefieldscom/intl-tel-input) // need to return a fully formatted international number from the server // also see related todos in editProfile function and template $timeout(function () { if (angular.isDefined(UserSvc.mobileCountryCode)) { for (var i = 0; i < countryData.length; i++) { if (countryData[i].dialCode == UserSvc.mobileCountryCode) { telInput.intlTelInput("selectCountry", countryData[i].iso2); break; } } } }, 500); }, 0); }); $scope.backToPickup = function () { $location.path(pickupRoute); }; }]); /************************************/ /****** PASSENGER VOUCHER AREA ******/ /************************************/ controllers.controller('PassengerVoucherCtrl', ['$scope', '$modal', '$location', 'UserSvc', 'TripSvc', function ($scope, $modal, $location, UserSvc, TripSvc) { $scope.userSvc = UserSvc; $scope.voucherCode = null; $scope.getPassengerVouchersPromise = null; $scope.getStripePaymentMethodsPromise = null; $scope.showMenu = true; var openCreditCardRequiredModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'creditcardrequiredmodal.html', controller: 'TripModalCtrl as modal' }); }; var openInvalidVoucherModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'invalidvouchermodal.html', controller: 'TripModalCtrl as modal' }); }; var openReplaceVoucherModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'replacevouchermodal.html', scope: $scope }); }; var openVoucherExpiredModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'voucherexpiredmodal.html', scope: $scope }); }; // Afro first time user welcome message, feels like a weird place to put this? var welcomeToAfroModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'welcometoafromodal.html', controller: 'TripModalCtrl as modal' }); }; $scope.redeemVoucher = function (confirm) { $scope.getPassengerVouchersPromise.then(function (data) { if (UserSvc.vouchers.length > 0 && !confirm) { openReplaceVoucherModal(); return; } $scope.getStripePaymentMethodsPromise.then(function (result) { var defaultCreditCard = UserSvc.getDefaultCreditCard(); var userHasValidCreditCard = defaultCreditCard != null && !defaultCreditCard.expired; UserSvc.redeemVoucher($scope.voucherCode, userHasValidCreditCard).then(function (data) { if (data.data.result) { TripSvc.paymentMethod = PaymentMethods.VOUCHER; if ($location.path() == signUpVoucherRoute) { welcomeToAfroModal(); } $scope.goBack(); return; } // TODO: distinguish between invalid vs. not available to use, see api if (data.data.errorCode == "8006") { openInvalidVoucherModal(); return; } if (data.data.errorCode == "8007") { openVoucherExpiredModal(); return; } //TODO: distinguish between no card vs. expired card as we do in FareCtrl if (data.data.errorCode == "8008") { openCreditCardRequiredModal(); $location.search({back: passengerVoucherRoute}); $location.path(passengerPaymentMethodsRoute); } }); }); }); }; $scope.goBack = function () { var backRoute = getBackLink($location, pickupRoute); $location.search({back: null}); $location.search({newPaymentMethod: null}); $location.path(backRoute); }; $scope.skipVoucher = function () { welcomeToAfroModal(); $location.path(pickupRoute); }; $scope.checkPreconditions = $scope.checkPreconditions || function () { return true; // maybe we want to stop you adding a voucher for some reason? like not having a credit card? }; $scope.init = function () { $scope.getPassengerVouchersPromise = UserSvc.getPassengerVouchers(); $scope.getStripePaymentMethodsPromise = UserSvc.getStripePaymentMethods(); // show the back button instead of the menu if we came from the fare screen var backRoute = getBackLink($location, pickupRoute); if (backRoute == fareRoute) { $scope.showMenu = false; } }; if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; $scope.goBack(); return; } $scope.init(); }]); /*******************************************/ /****** STRIPE PAYMENT METHODS CONTROLLER **/ /*******************************************/ controllers.controller('StripePaymentMethodsCtrl', ['$scope', '$location', '$q', '$modal', 'promiseTracker', 'stripe', 'TripSvc', 'UserSvc', function ($scope, $location, $q, $modal, promiseTracker, stripe, TripSvc, UserSvc) { $scope.userSvc = UserSvc; $scope.editingCardDetails = false; // used to toggle ui element visibility if the user chooses to edit their card details $scope.showMenu = false; $scope.showBackButton = false; $scope.inOnboardingFlow = false; $scope.modalErrorMessage = null; $scope.cardDetails = { cardNumber: null, cardType: null, expirationDate: null, cvcNumber: null }; $scope.onboardingFlowGoForward = function () { $location.path(signUpVoucherRoute); }; $scope.goBack = function () { var backRoute = getBackLink($location, pickupRoute); $location.search({back: null}); $location.search({newPaymentMethod: null}); $location.path(backRoute); }; var openErrorModal = function () { dismissCurrentOpenModal(); $modal.open({ templateUrl: 'paymentmethodserrormodal.html', scope: $scope }); }; $scope.createStripeTokenTracker = promiseTracker(); var tokenizeCardAndSetPaymentMethodToken = function () { var deferred = $q.defer(); $scope.createStripeTokenTracker.addPromise(deferred.promise); var expMonth = $scope.cardDetails.expirationDate.substr(0, 2); var expYear = $scope.cardDetails.expirationDate.substr(2); stripe.card.createToken({ number: $scope.cardDetails.cardNumber, cvc: $scope.cardDetails.cvcNumber, exp_month: expMonth, exp_year: expYear }).then(function (token) { UserSvc.setStripePaymentMethod(token.id).then(function (result) { if (!result.data.result) { $scope.modalErrorMessage = result.data.error_message; openErrorModal(); return; } if ($scope.inOnboardingFlow) { $scope.onboardingFlowGoForward(); } else { TripSvc.paymentMethod = getNewPaymentMethod(PaymentMethods.CREDIT_CARD); $scope.goBack(); } }); deferred.resolve(); }, function (error) { deferred.reject(); console.log(String.format( "Received Stripe Error - type: '{0}', code: '{1}', param: '{2}', message: '{3}'", error.type, error.code, error.param, error.message )); $scope.modalErrorMessage = error.message; openErrorModal(); }); }; var getNewPaymentMethod = function (defaultPaymentMethod) { var searchParms = $location.search(); if (searchParms['newPaymentMethod']) { return searchParms['newPaymentMethod']; } return defaultPaymentMethod; }; $scope.addPaymentMethod = function () { if (!$scope.creditCardForm.$valid) { return; } // currently we only allow for one credit card, so we delete any existing payment methods first // not doing this in editPaymentMethod because the user can cancel if (UserSvc.paymentMethods.length > 0) { var paymentMethodToken = UserSvc.paymentMethods[0].token; UserSvc.deleteStripePaymentMethod(paymentMethodToken).then(function (result) { if (!result.data.result) { $scope.modalErrorMessage = result.data.error_message; openErrorModal(); return; } TripSvc.paymentMethod = PaymentMethods.CASH; tokenizeCardAndSetPaymentMethodToken(); }); } else { tokenizeCardAndSetPaymentMethodToken(); } }; $scope.deletePaymentMethod = function (paymentMethodToken) { UserSvc.deleteStripePaymentMethod(paymentMethodToken).then(function (result) { if (!result.data.result) { $scope.modalErrorMessage = result.data.error_message; openErrorModal(); return; } TripSvc.paymentMethod = PaymentMethods.CASH; }); }; $scope.editPaymentMethod = function (paymentMethodToken) { $scope.editingCardDetails = true; }; $scope.checkPreconditions = $scope.checkPreconditions || function () { if (!featureFlags.creditCardPaymentsEnabled) { return false; } return true; }; $scope.init = function () { UserSvc.getStripePaymentMethods(); if (!$scope.inOnboardingFlow) { // don't show back or menu buttons if we're in the onboarding flow var backRoute = getBackLink($location, pickupRoute); if (backRoute == fareRoute) { // show the back button if we came from the fare screen // TODO: show the back button if we came from the voucher screen (via the fare screen or not) $scope.showBackButton = true; } else { $scope.showMenu = true; } } }; if ($location.path() == signUpCreditCardRoute) { $scope.inOnboardingFlow = true; } // if checkPreconditions() returns false (credit card payments are disabled) then // skip to the next onboarding step if we're in the onboarding flow, otherwise goBack() if (!$scope.checkPreconditions()) { $scope.legitimateLocationChange = true; if ($scope.inOnboardingFlow) { $scope.onboardingFlowGoForward(); } else { $scope.goBack(); } return; } $scope.init(); }]); /************************************/ /*** PASSENGER TRIP HISTORY AREA ****/ /************************************/ controllers.controller('PassengerTripHistoryCtrl', ['$scope', 'TripSvc', '$location', function ($scope, TripSvc, $location) { $scope.tripSvc = TripSvc; TripSvc.fetchPassengerTrips(); $scope.viewTrip = function (tripKey) { $location.url('/trip/' + tripKey + '/view/'); }; }]); /************************************/ /***** PASSENGER TRIP VIEW AREA *****/ /************************************/ controllers.controller('PassengerTripViewCtrl', ['$scope', '$routeParams', 'TripSvc', '$location', function ($scope, $routeParams, TripSvc, $location) { $scope.tripSvc = TripSvc; var tripKey = $routeParams.tripKey; $scope.backToHistory = function () { $location.path(passengerTripHistoryRoute); }; TripSvc.fetchSingleTrip(tripKey); }]); /************************************/ /***** PASSENGER ABOUT AFRO AREA ****/ /************************************/ controllers.controller('AboutAfroCtrl', ['$scope', '$location', function ($scope, $location) { $scope.backRoute = passengerInformationRoute; $scope.goBack = function () { $location.search({back: null}); $location.path($scope.backRoute); }; $scope.backToInformation = function () { $location.path(passengerInformationRoute); }; $scope.aboutAfro = function () { $location.path(aboutAfroRoute); }; $scope.termsAndConditions = function () { $location.path(termsAndConditionsRoute); }; $scope.privacyPolicy = function () { $location.path(privacyPolicyRoute); }; $scope.takeTourRoute = function () { $location.path(takeTourRoute); }; $scope.init = function () { $scope.backRoute = getBackLink($location, passengerInformationRoute); }; $scope.init(); }]); /************************************/ /****** TRIP DETAILS CONTROLLER *****/ /************************************/ controllers.controller('TripDetailsCtrl', ['$scope', 'TripSvc', '$location', function ($scope, TripSvc, $location) { $scope.backRoute = waitingDriverEnrouteRoute; $scope.tripSvc = TripSvc; $scope.goBack = function () { $location.search({back: null}); $location.path($scope.backRoute); }; $scope.init = function () { $scope.backRoute = getBackLink($location, waitingDriverEnrouteRoute); }; $scope.init(); }]); /************************************/ /****** TAKE A TOUR CONTROLLER ******/ /************************************/ controllers.controller('TakeTourCtrl', ['$scope', '$location', function ($scope, $location) { $scope.backRoute = pickupRoute; $scope.goBack = function () { $location.search({back: null}); $location.path($scope.backRoute); }; $scope.init = function () { $scope.backRoute = getBackLink($location, pickupRoute); }; $scope.init(); $scope.slides = [ { image: '/img/product_tour/AfroProductTour1.jpg' }, { image: '/img/product_tour/AfroProductTour2.jpg' }, { image: '/img/product_tour/AfroProductTour3.jpg' }, { image: '/img/product_tour/AfroProductTour4.jpg' }, { image: '/img/product_tour/AfroProductTour5.jpg' } ]; }]); /************************************/ /**** TRIP MODAL CONTROLLER */ /************************************/ controllers.controller('TripModalCtrl', ['$scope', '$modalInstance', '$location', function ($scope, $modalInstance, $location) { $scope.takeProductTour = function () { $location.path(takeTourRoute); } }]); /**********************************/ /* TRACK YOUR DELIVERY CONTROLLER */ /**********************************/ controllers.controller('TrackYourDeliveryCtrl', ['$scope', '$routeParams', '$timeout', 'ReceiverSvc', function ($scope, $routeParams, $timeout, ReceiverSvc) { var tripId = $routeParams.tripId; var mapRendered = false; var fetchDeliveryDetailsPromise; var updateEtaPromise; $scope.deliveryDetails = null; $scope.experienceRated = false; $scope.rating = 0; $scope.ratingStates = [ {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'}, {stateOn: 'glyphicon-star', stateOff: 'glyphicon-star gray-star'} ]; $scope.rateExperience = function (rating) { ReceiverSvc.rateExperience(rating, tripId).then(function () { $scope.experienceRated = true; }); }; function fetchDeliveryDetails() { ReceiverSvc.fetchDeliveryDetails(tripId).then(function (response) { $scope.deliveryDetails = response.data.trip; $scope.rating = response.data.rating; var status = $scope.deliveryDetails.status.toUpperCase(); if (status === 'DEPARTED' || status === 'ARRIVED') { if (!mapRendered) { ReceiverSvc.renderMap(response.data.trip.base_location, response.data.trip.driver.location, response.data.trip.delivery_location); updateEta(); mapRendered = true; } else { ReceiverSvc.setDriverLocationMarkerPosition(response.data.trip.driver.location); } } else if (status !== 'PENDING' && status !== 'ACCEPTED') { ReceiverSvc.removeDriverLocationMarker(); stopUpdateEta(); return; } fetchDeliveryDetailsPromise = $timeout(fetchDeliveryDetails, 10000); }); } function calculateEtaDiffInSeconds(etaDateTimeIso) { var now = new Date(); var eta = new Date(etaDateTimeIso); var diff = eta - now; // ms return diff / 1000; // seconds } function calculateEtaMinutesRemaining(etaDiffInSeconds) { return Math.max(Math.ceil(etaDiffInSeconds / 60), 0); // round up to nearest minute, never go below 0 } function updateEta() { var etaDiffInSeconds = calculateEtaDiffInSeconds($scope.deliveryDetails.eta.delivery_eta); var etaMinutesRemaining = calculateEtaMinutesRemaining(etaDiffInSeconds); console.log("etaDiffInSeconds: " + etaDiffInSeconds); console.log("etaMinutesRemaining: " + etaMinutesRemaining); // update the delay to the number of seconds to the next minute boundary // we do this every time, not just the first time, because on mobile timers can get paused var delay = (etaDiffInSeconds - Math.floor(etaDiffInSeconds / 60) * 60) * 1000; console.log("delay: " + delay); var labelText; if (etaMinutesRemaining <= 1) { // TODO: get an updated eta? currently the labelText will remain as '<1 min' labelText = "<1 min"; } else { labelText = etaMinutesRemaining + " mins"; } $scope.etaMinutesRemainingText = labelText; ReceiverSvc.setCustomerLocationMarkerLabel(labelText); updateEtaPromise = $timeout(updateEta, delay); } function stopUpdateEta() { if (angular.isDefined(updateEtaPromise)) { $timeout.cancel(updateEtaPromise); updateEtaPromise = undefined; } } $scope.init = function () { fetchDeliveryDetails(); }; $scope.init(); $scope.$on('$destroy', function() { stopUpdateEta(); if (angular.isDefined(fetchDeliveryDetailsPromise)) { $timeout.cancel(fetchDeliveryDetailsPromise); fetchDeliveryDetailsPromise = undefined; } }); }]);