'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;
}
});
}]);