HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //home/arjun/projects/buyercall_new/buyercall/buyercall/assets/components/widgets/call_widget.js
/* jshint esversion: 6 */

var _ = require('underscore');
var __ = require('localize');
var Backbone = require('backbone');
var utils = require('../utils');
var moment = require('moment');

require('validate/jquery.validate');
require('validate/additional-methods');

require('moment-timezone');

var animate = require('./call_widget_animation.js');
var moment = require('moment');
require('moment-timezone');

require('./styles/call_widget.scss');

var beforeCallTemplate = require('./templates/call_widget_beforecall.tpl');
var unavailableTemplate = require('./templates/call_widget_unavailable.tpl');
var inProgressTemplate = require('./templates/call_widget_inprogress.tpl');
var errorTemplate = require('./templates/call_widget_error.tpl');
var thankYouTemplate = require('./templates/call_widget_thankyou.tpl');
var closedTemplate = require('./templates/call_widget_closed.tpl');

const defaultSettings = require('./default_settings');
const defaultImages = require('./default_images');

const WidgetModel = require('./models');

// The URLs to retrieve settings and post lead information.  Since the widget
// is loaded on another site, the SERVER_NAME environment variable must be
// initialized with the URL of the site (default: buyercall.com)
const URL_ROOT = `${URL_SCHEME}://${SERVER_NAME}/api`;
const CALL_URL = `${URL_ROOT}/call`;
const SAVE_URL = `${URL_ROOT}/save_lead`;
const STATUS_URL = `${URL_ROOT}/call_status`;
const SETTINGS_URL = `${URL_ROOT}/outbound/settings/`;

const ANIMATION_INTERVAL = 20;
const CALL_POLL_TIMEOUT = 360;

const ERR_NO_AGENTS_MSG = 'Sadly, no agents are available at the moment. Please try again later.';
const ERR_TOOLONG_MSG = 'The call seems to be taking longer than usual. An agent might not be available right now. We will call you back as soon as possible.';
const ERR_GENERIC_MSG = 'We\'re sorry, your request cannot be fulfilled at the moment. Please try again later.';


var LocalStorage = function (guid) {
    function getAll() {
        try {
            var allStorage = JSON.parse(window.localStorage['buyercall']);
            if (guid in allStorage) {
                return allStorage[guid];
            }
        } catch (e) { }
        return {};
    }
    function setAll(dict) {
        if (!window.localStorage)
            return;

        var allStorage = JSON.parse(window.localStorage['buyercall'] || '{}');
        allStorage[guid] = _.extend(getAll(), dict);
        window.localStorage['buyercall'] = JSON.stringify(allStorage);
    }
    this.get = function (key) {
        return getAll()[key];
    };
    this.set = function (key, value) {
        var dict = getAll();
        dict[key] = value;
        setAll(dict);
    };
};

var CallWidgetImage = Backbone.View.extend({
    id: 'buyercall-widget-image',

    template: `
  <div id="buyercall-widget-image-close" class="buyercall-hide"></div>
    `,

    events: {
        'click img': 'handleClick',
        'click #buyercall-widget-image-close': 'handleClose',
    },

    initialize: function (options) {
        this.widget = options.widget;
        this.mobile = utils.isMobileDevice();
    },

    render: function () {
        this.$el.html(this.template);
        this.$el.css('z-index', '50000');
        if (!this.el.parentNode)
            return this;
    },

    updateSettings: function (imageUrl, settings) {
        if (!imageUrl || !settings.typeImage) {
            this.$el.find('img').remove();
        }
        if (this.mobile && !settings.showOnMobile){
            $('#buyercall-widget-image').css('display','none');
        }
        if (this.mobile && settings.mobileOptimized){
            $('#buyercall-widget-image').css('display','none');
        }
        if (imageUrl !== this.imageUrl) {
            this.$el.find('img').remove();
        }
        if (imageUrl && settings.typeImage && !this.$el.find('img').length &&
            this.widget.getState() == 'closed') {
            this.$el.append(`<img src="${imageUrl}" />`);
        }
        this.imageUrl = imageUrl;

        // Image manipulation
        var translateX = settings.imageHorizontal - 50;
        var translateY = 50 - settings.imageVertical;
        var scale = settings.imageSize;

        var transformImage = () => {
            this.imageWidth = this.$('img').prop('naturalWidth');
            this.imageHeight = this.$('img').prop('naturalHeight');
            var newWidth = this.imageWidth * settings.imageSize / 100;
            var newHeight = this.imageHeight * settings.imageSize / 100;
            var parentHeight, parentWidth, position;

            var container = this.el.parentElement;
            if (container) {
                if ($(container).hasClass('widget-preview')) {
                    parentHeight = $(container).innerHeight();
                    parentWidth = $(container).innerWidth();
                    position = 'absolute';
                } else {
                    container = window;
                    parentHeight = container.innerHeight;
                    parentWidth = container.innerWidth;
                    position = 'fixed';
                }

                var left = ((parentWidth - newWidth) * settings.imageHorizontal / 100);
                var top = ((parentHeight - newHeight) * settings.imageVertical / 100);

                this.$el.find('img').css({
                    'width': newWidth + 'px',
                });
                this.$el.css({
                    'position': position,
                    'left': left + 'px',
                    'top': top + 'px',
                    'width': newWidth + 'px',
                });
            }

            return this;
        };

        this.imageWidth = this.$('img').prop('naturalWidth');
        if (!this.imageWidth) {
            utils.afterImageLoad(this.imageUrl, transformImage);
        } else {
            transformImage();
        }

        return this;
    },

    hide: function () {
        this.$el.hide();
    },

    show: function () {
        this.$el.show();
    },

    handleClick: function (e) {
        this.trigger('click');
    },

    handleClose: function (e) {
        this.trigger('close');
    },
});

var CallWidgetView = Backbone.View.extend({
    events: {
        'click .buyercall-widget-header': 'handleHeaderClick',
        'click .buyercall-widget-collapse-button': 'handleCollapseClick',
        'click #buyercall-talk-button': 'handleTalkClick',
        'click #buyercall-submit-button': 'handleSubmitClick',
        'click .buyercall-widget-hide-button': 'handleHideClick'
    },

    id: 'buyercall-widget',

    className: 'state-closed',

    animationShown: null,

    previewMode: false,

    imageWidth: 1,

    $class: function (class_, condition) {
        if (condition) {
            this.$el.addClass(class_);
        } else {
            this.$el.removeClass(class_);
        }
    },

    initialize: function (options) {
        this.guid = options.guid;
        this.settings = new WidgetModel();
        this.mobile = utils.isMobileDevice();

        this.setState(options.state || 'closed');

        $.ajax(SETTINGS_URL + this.guid, {
            method: 'GET',
            success: (data) => {
                this.settings.set(data);

                // Only render after image has loaded
                let imageUrl = this.getImageUrl();
                utils.afterImageLoad(imageUrl, () => { this.renderSettings(); });
            }
        });
    },

    renderSettings: function () {
        this.render();
        this.listenTo(this.settings, 'change', () => {
            this.widgets.forEach((widget) => {
                widget.render();
            });
            this.updateSettings(this.settings.toJSON());
        });
    },

    render: function () {
        //if (this.mobile && !this.settings.get('showOnMobile')) {
            //return;
        //}

        var widgetOptions = [
            { id: 'buyercall-before-call', template: beforeCallTemplate },
            { id: 'buyercall-unavailable', template: unavailableTemplate },
            { id: 'buyercall-inprogress', template: inProgressTemplate },
            { id: 'buyercall-error', template: errorTemplate },
            { id: 'buyercall-thankyou', template: thankYouTemplate },
            { id: 'buyercall-closed', template: closedTemplate }
        ];

        this.widgets = [];
        widgetOptions.forEach((opt, i) => {
            var widget = new TemplateView({
                model: this.settings,
                template: opt.template,
                id: opt.id
            });
            this.widgets.push(widget);
            widget.$el.appendTo(this.$el);
        });

        this.imageView = new CallWidgetImage({
            settings: this.settings,
            widget: this,
        });
        clearInterval(this.imageInterval);
        this.imageInterval = setInterval(() => {
            if (this.el.parentNode && !this.imageView.el.parentNode) {
                clearInterval(this.imageInterval);
                this.imageView.render().$el.appendTo(this.el.parentNode);
                this.imageView.updateSettings(this.getImageUrl(), this.settings.toJSON());
            }
        }, 200);
        this.imageView.on('click', () => {
            this.handleHeaderClick();
        });
        this.imageView.on('close', () => {
            this.handleImageCloseClick();
        });

        this.updateSettings(this.settings.toJSON());

        this.addButtonListener();

        this.setupValidation();

        if (this.hasType('tab') || this.hasType('image')) {
            this.$el.show();
        }

        return this;
    },

    /**
     * Show modal pop-up on button and link clicks for this widget.
     */
    addButtonListener: function () {
        if (!this.hasType('link') && !this.hasType('button')) {
            return;
        }

        $(document.body).on('click', (evt) => {
            var $button = $(evt.originalEvent.target).closest('.buyercall-button');
            if ($button.length && $button.data('widget-guid') === this.guid) {
                this.showLightbox();
            }
        });
    },

    setupValidation: function () {
        this.$('#buyercall-before-call form').validate({
            rules: {
                'widget-lead-phone-number': {
                    phoneUS: true
                }
            },
            messages: {},
            highlight: function (element) {
                $(element).parent().addClass('has-error');
            },
            unhighlight: function (element) {
                $(element).parent().removeClass('has-error');
            }
        });
        this.$('#buyercall-unavailable form').validate({
            rules: {
                'widget-unavailable-lead-phone-number': {
                    phoneUS: true
                }
            },
            messages: {},
            highlight: function (element) {
                $(element).parent().addClass('has-error');
            },
            unhighlight: function (element) {
                $(element).parent().removeClass('has-error');
            }
        });
    },

    updateSettings: function (settings) {
        settings = _.extend({}, defaultSettings, settings);

        // Animation
        if (this.animationShown !== settings.animation) {
            this.animationShown = settings.animation;
            this.startAnimation();
        }

        // Header color
        this.$header = this.$('.buyercall-widget-header');
        this.$header.css('background-color', settings.color);
        var luminance = utils.getLuminance(settings.color);
        if (luminance < 0.7) {
            this.$header.css('color', 'white');
        } else {
            this.$header.css('color', 'black');
        }

        // Position
        this.$el.removeClass('top-right top-left bottom-right bottom-left');
        this.$el.addClass(settings.position);

        // Type
        for (let type of ['button', 'link', 'tab', 'image']) {
            this.$class(`type-${type}`, this.hasType(type));
        }

        // Tab
        this.updateTabWidth(settings);

        // Image
        let imageUrl = this.getImageUrl();
        this.imageView.updateSettings(imageUrl, settings);

        // Repeat animation
        this.scheduleAnimation();

        // Image in-front of tab
        this.$class('image-in-front', settings.imageInFront);

        // Mobile optimize
        if (this.settings.get('showOnMobile') && !this.settings.get('mobileOptimized')) {
            this.$class('mobile', this.mobile);
        } else if (this.settings.get('mobileOptimized') && !this.settings.get('showOnMobile')) {
            this.$class('mobile-optimized', settings.mobileOptimized);
            this.$class('mobile', this.mobile);
        } else if (this.settings.get('mobileOptimized') && this.settings.get('showOnMobile')) {
            this.$class('mobile-optimized', settings.mobileOptimized);
            this.$class('mobile', this.mobile);
        } else {
            this.$class('no-show-no-opt', this.mobile);
            // console.log('The buyercall widget has been turned off on mobile.')
        }
        // Trigger
        let triggering = false;
        if (settings.triggerBasedOnPage) {
            for (let i = 0; i < settings.pages.length; i++) {
                let page = settings.pages[i];
                if (window.location.href === page) {
                    setTimeout(() => {
                        if (this.getState() === 'closed') {
                            this.setState('beforecall');
                        }
                    }, settings.triggerPageInterval * 1000);
                    triggering = true;
                    break;
                }
            }
        }
        if (!triggering && settings.triggerBasedOnTime) {
            setTimeout(() => {
                if (this.getState() === 'closed') {
                    this.setState('beforecall');
                }
            }, settings.triggerInterval * 1000);
        }
    },

    updateTabWidth: function (settings) {
        if (this.mobile && settings.mobileOptimized) {
            return;
        }

        if (this.el.parentElement) {
            var containerWidth = this.el.parentElement.clientWidth;
            if (this.getState() === 'closed') {
                this.$el.css(
                    'width',
                    (containerWidth * settings.tabWidth / 100) + 'px');
                return;
            }
        }

        this.$el.css('width', '360px');
    },

    scheduleAnimation: function () {
        if (this.settings.get('repeatAnimation') && this.agentsAvailable()) {
            clearInterval(this.repeat);
            this.repeat = setInterval(() => this.startAnimation(), ANIMATION_INTERVAL * 1000);
        }
    },

    shouldHideImage: function () {
        var storage = new LocalStorage(this.guid),
            hideDate = storage.get('image-hide-date');
        if (!hideDate) {
            return false;
        }
        if (new Date().getTime() - hideDate < 3600 * 1000) {
            return true;
        }
        storage.set('image-hide-date', undefined);
        return false;
    },

    saveHideImageTime: function () {
        var storage = new LocalStorage(this.guid);
        storage.set('image-hide-date', new Date().getTime());
    },

    getImageUrl: function () {
        if (this.shouldHideImage()) {
            return '';
        }

        return this.settings.getImageUrl(this.guid);
    },

    startAnimation: function () {
        var that = this,
            animation = this.settings.get('animation'),
                type = this.settings.get('type');

                if (this.animationRunning) {
                    return;
                }

                if (animation && (this.hasType('image') || this.hasType('tab')) && this.getState() == 'closed') {
                    let isMobile = this.settings.get('mobileOptimized') && this.mobile;

                    this.animationRunning = true;
                    // TODO: Get rid of this crutch
                    this.undelegateEvents();

                    animate(this.$el, {
                        isMobile: isMobile,
                        animation: animation,
                        duration: 1000,
                        callback: () => {
                            this.$el.css({
                                'top': '',
                                'bottom': '',
                                'left': '',
                                'right': '',
                                'transform': ''
                            });
                            this.animationRunning = false;
                            this.delegateEvents();
                        }
                    });
                }
    },

    isLightbox: function () {
        return this.$el.hasClass('buyercall-lightbox');
    },

    closeLightbox: function () {
        document.body.style.overflow = document.body.getAttribute('data-old-overflow');
        $('.buyercall-lightbox-mask').remove();
        this.$el.removeClass('buyercall-lightbox').css({
            'position': '',
            'left': '',
            'top': '',
            'bottom': '',
            'right': ''
        });
    },

    showLightbox: function () {
        var oldOverflow = '';

        // Prevent scrolling the window
        document.body.setAttribute('data-old-overflow', document.body.style.overflow);
        document.body.style.overflow = 'hidden';

        var mask = $('<div class="buyercall-lightbox-mask">').insertBefore(
            this.$el
        ).click((evt) => { this.setState('closed'); });

        if (!this.settings.get('transparentBackground')) {
            mask.css('opacity', '0');
        }

        this.$el.addClass('buyercall-lightbox');

        if (!this.agentsAvailable()) {
            this.setState('unavailable');
        } else {
            this.setState('beforecall');
        }

        this.$el.css({
            'position': 'fixed',
            'left': ($(window).width() - this.$el.width()) / 2,
            'top': ($(window).height() - this.$el.height()) / 2,
            'bottom': 'auto',
            'right': 'auto'
        });
        this.$el.show();
    },

    getClasses: function (re, invert) {
        return this.el.className
        .split(' ')
        .filter((x) => re.test(x) ? !invert : !!invert);
    },

    hasType: function(type) {
        return !!this.settings.get(utils.idToPropertyName(`type-${type}`));
    },

    setState: function (newState) {
        if (this.getState() === newState) {
            return;
        }

        var classes = this.getClasses(/^state-/, true);
        classes.push(`state-${newState}`);
        this.el.className = classes.join(' ');

        this.updateTabWidth(this.settings.toJSON());

        if (newState == 'beforecall' && this.imageView) {
            this.imageView.hide();
        }
        if (newState == 'beforecall' && this.settings.get('alwaysModal') && !this.isLightbox()) {
            this.showLightbox();
        }
        if ((newState == 'closed' || newState == 'hidden') && this.isLightbox()) {
            this.closeLightbox();
        }
        if ((newState == 'closed' || newState == 'hidden') && this.imageView) {
            this.imageView.show();
        }
        if (newState == 'hidden') {
            // Unhide widget on hover
            setTimeout(() => {
                this.$('.buyercall-widget-header').one('mousemove', () => {
                    this.setState('closed');
                });
            }, 1000);
        }
    },

    getState: function () {
        var classes = this.getClasses(/^state-/);
        if (classes.length) {
            return classes[0].substr(6);
        }
        return null;
    },

    /**
     * Determines whether any agents are currently available.
     */
    agentsAvailable: function () {
        return this.settings.get('agentsAvailable');
    },

    /**
     * Check that all mandatory fields are filled.
     */
    validateWidget: function() {
	var form = null,
	    isValid = true;

        if (this.getState() == 'beforecall') {
            form = this.$('#buyercall-before-call form ');
        } else if (this.getState() == 'unavailable') {
            form = this.$('#buyercall-unavailable form');
        }

	if (form) {
	    isValid = form.valid();
	}

	return isValid;
    },

    /**
     * Starts a Twilio call to the lead's phone number, which must be passed as
     * part of the lead information. Additionally, the following fields are
     * supported:
     *
     * phoneNumber - the lead's phone number
     * firstName - the lead's first name
     * lastName - the lead's last name
     * emailAddress - the lead's email address
     * question - the question the lead is calling about.
     * ...any custom information you might want to include. This will be stored
     * in the buyercall database.
     *
     * @param {object} settings - The lead information
     */
    startCall: function (settings) {
        var deferred = $.Deferred();

        $.ajax(CALL_URL + '?guid=' + this.guid, {
            method: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(settings)
        }).done(function (result) {
            // Is the call being established?
            if (result.success === true) {
                let connect = $.Deferred();

                if (result.callId != undefined) {
                    let pollCounter = 0,
                    interval = setInterval(function () {
                        if (pollCounter > CALL_POLL_TIMEOUT) {
                            clearInterval(interval);
                            connect.reject(__(ERR_TOOLONG_MSG));
                            return;
                        }
                        pollCounter += 2;

                        $.ajax({
                            url: STATUS_URL + '/' + result.callId,
                            success: function (data) {
                                if (data.callConnect) {
                                    clearInterval(interval);
                                    connect.resolve();
                                } else if (data.error) {
                                    clearInterval(interval);
                                    connect.reject(__(ERR_GENERIC_MSG))
                                }
                            }
                        });
                    }, 2000);
                }
                deferred.resolve({
                    success: true,
                    connectPromise: connect.promise()
                });
                return;
            }

            var errorMsg = __(ERR_GENERIC_MSG);
            if (result.code === 'ERR_NO_AGENTS') {
                errorMsg = __(ERR_NO_AGENTS_MSG);
            }
            deferred.reject({
                success: false,
                errorMessage: errorMsg
            });
        }).fail(function () {
            deferred.reject({
                success: false,
                errorMessage: __(ERR_GENERIC_MSG)
            });
        });

        return deferred.promise();
    },

    handleHeaderClick: function (event) {
        if (this.isLightbox()) {
            return;
        }

        // Open or close the widget
        switch (this.getState()) {
            case 'closed':
            case 'hidden':
                // Are the agents available?
                if (!this.agentsAvailable()) {
                    this.setState('unavailable');
                } else {
                    this.setState('beforecall');
                }
                break;
                // case 'beforecall':
            default:
                this.setState('closed');
                break;
        }
    },

    handleCollapseClick: function (e) {
        this.setState('closed');
        // Ensure that the custom image is not showing when the popup is closed
        if (this.mobile && this.settings.get('mobileOptimized')) {
            $('#buyercall-widget-image').css('display','none');
        }
        e.stopPropagation();
    },

    handleHideClick: function (e) {
        this.setState('hidden');
        e.stopImmediatePropagation();
    },

    handleTalkClick: function () {
        if (!this.validateWidget()) {
            return;
        }

        var settings = {
            firstName: this.$('#widget-lead-first-name').val(),
            lastName: this.$('#widget-lead-last-name').val(),
            emailAddress: this.$('#widget-lead-email-address').val(),
            phoneNumber: this.$('#widget-lead-phone-number').val(),
            question: this.$('#widget-question').val(),
            source: ""
        };

        this.startCall(settings).done((result) => {
            this.setState('inprogress');
            result.connectPromise.done(() => {
                this.setState('closed');
            }).fail((errorMessage) => {
                this.setState('error');
                this.$('#buyercall-error .buyercall-preamble').text(errorMessage);
            });
        }).fail((result) => {
            this.setState('error');
            this.$('#buyercall-error .buyercall-preamble').text(result.errorMessage);
        });
    },

    handleSubmitClick: function () {
        if (!this.validateWidget()) {
            return;
        }

        var settings = {
            firstName: this.$('#widget-unavailable-lead-first-name').val(),
            lastName: this.$('#widget-unavailable-lead-last-name').val(),
            emailAddress: this.$('#widget-unavailable-lead-email-address').val(),
            phoneNumber: this.$('#widget-unavailable-lead-phone-number').val(),
            question: this.$('#widget-unavailable-question').val(),
            source: ""
        };

        $.ajax(SAVE_URL + '?guid=' + this.guid, {
            method: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(settings),
            success: (data) => {
                this.setState('thankyou');
            },
            error: () => {
                this.setState('error');
                this.$('#buyercall-error .buyercall-preamble').text(__(ERR_GENERIC_MSG));
            }
        });
    },

    handleImageCloseClick: function (e) {
        this.$('#buyercall-widget-image img').remove();
        this.settings.set({
            'customImage': '',
            'defaultImage': ''
        });

        this.saveHideImageTime();

        if (e) {
            e.stopPropagation();
        }
    }
});

// An instance of the widget
var CallWidgetPreView = CallWidgetView.extend({
    events: {},

    previewMode: true,

    className: 'state-closed preview-mode',

    initialize: function (options) {
        this.guid = options.guid;
        this.settings = new WidgetModel();
        this.mobile = utils.isMobileDevice();

        this.setState(options.state || 'closed');

        this.settings = options.settings;
        this.renderSettings();
    },

    agentsAvailable: function () {
        return true;
    },

    addButtonListener: function () {},

    scheduleAnimation: function() {},

    saveHideImageTime: function() {},

    shouldHideImage: function() { return false; }
});

var TemplateView = Backbone.View.extend({
    initialize: function (options) {
        this.template = options.template;
        this.render();
    },

    render: function () {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    }
});


module.exports = { CallWidgetView, CallWidgetPreView };

// Export the widget view, in case the file is loaded externally
var global = Function('return this')();
global.CallWidgetView = CallWidgetView;