MediaWiki:FundraisingBanners/CoreJS-2018.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* jshint maxerr: 600 */
/* MediaWiki:FundraisingBanners/CoreJS-2018.js
 * Core code for banner forms, with new inline error messages
 */

var frb = frb || {};

/**
 * Test for general ES6 and <dialog> support
 *
 * Checks for arrow functions, default parameters, NodeList.prototype.forEach(), <dialog> support
 * Should be roughly Chrome 51+, Firefox 98+, Edge 79+, Safari 15.4+
 * Based on https://gist.github.com/bendc/d7f3dbc83d0f65ca0433caf90378cd95
 * @return {boolean}
 */
frb.supportedBrowser = function() {
    try {
        new Function('(a = 0) => a');
        document.querySelectorAll('.frb').forEach(a => a);
        if ( typeof HTMLDialogElement === 'function' ) {
            return true;
        } else {
            return false;
        }
    }
    catch (err) {
        return false;
    }
}();

if ( !mw.centralNotice.adminUi ) { // T262693
    frb.loadedTime = Date.now();
    frb.didSelectAmount = false;
    frb.optinRequiredCountries =
        [ 'AR', 'AT', 'BE', 'BR', 'CL', 'CO', 'CZ', 'DK', 'ES', 'FR', 'GB', 'GR', 'HU', 'IE', 'IT', 'IL',
          'LU', 'LV', 'MX', 'NL', 'NO', 'PE', 'PL', 'PT', 'RO', 'SE', 'SK', 'UA', 'UY' ];
    frb.optinRequired = frb.optinRequiredCountries.indexOf(mw.centralNotice.data.country) !== -1;
    frb.maxUSD = 25000;
    frb.reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

// Keyboard shortcut to go from banner preview to editor - Ctrl+Shift+E
if ( mw.config.get('wgUserName') ) {
    if ( mw.config.get('wgUserName').match(/\(WMF\)/) ) {
        window.addEventListener('keydown', function(e) {
            if ( e.ctrlKey && e.shiftKey && e.keyCode === 69 ) {
                window.open( 'https://meta.wikimedia.org/wiki/Special:CentralNoticeBanners/Edit/' + mw.centralNotice.data.banner );
            }
        });
    }
}

/**
 * Main function to submit to paymentswiki
 *
 * @param  {Object} options
 * - method (required)
 * - submethod (optional)
 * - gateway (optional)
 * - skipValidation (optional boolean, for pp-usd. Not yet implemented.)
 * @param  {Boolean} isEndowment - deprecated, set frb.isEndowment instead
 */
frb.submitForm = function( options, isEndowment ) {

    var uri = new mw.Uri('https://payments.wikimedia.org/index.php/Special:GatewayChooser');
    var params = {};

    if ( !frb.validateForm( options ) ) {
        frb.extraData.validateError = 1; // Flag they had an error, even if fixed later
        return false; // Error, bail out of submitting
    }

    // Skip form chooser for Apple Pay / Google Pay
    if ( options.method === 'apple' || options.method === 'google' ) {
        uri = new mw.Uri('https://payments.wikimedia.org/index.php/Special:AdyenCheckoutGateway');
    }

    // Skip form chooser for Venmo
    if ( options.method === 'venmo' ) {
        uri = new mw.Uri('https://payments.wikimedia.org/index.php/Special:BraintreeGateway');
    }

    // Form selection data
    params.payment_method = options.method;
    if ( options.submethod ) {
        params.payment_submethod = options.submethod;
    }
    if ( options.gateway ) {
        params.gateway = options.gateway;
    }
    if ( options.variant ) {
        params.variant = options.variant;
    }
    params.recurring = frb.getRecurring();

    if ( params.recurring && params.variant && params.variant.match( /monthlyConvert/ ) ) {
        // Post-payments monthly convert makes no sense if it's already recurring
        // Avoid things like T312905
        delete params.variant;
    }

    params.currency = frb.getCurrency(mw.centralNotice.data.country) || 'USD';

    params.uselang = mw.centralNotice.data.uselang || 'en';
    params.country = mw.centralNotice.data.country || 'XX';

    if ( params.uselang === 'pt' && params.country === 'BR' ) {
        params.uselang = 'pt-br';
    }
    if ( params.uselang === 'es' &&
        ( params.country === 'AR' || params.country === 'CL' ||
          params.country === 'CO' || params.country === 'MX' ||
          params.country === 'PE' || params.country === 'UY' ||
          params.country === 'US' )
    ) {
        params.uselang = 'es-419';
    }

    // dLocal override for South Africa
    if ( params.payment_method === 'cc' && params.country === 'ZA' ) {
        params.gateway = 'astropay';
    }

    // Amount
    var amount = frb.getAmount();
    if ( $('#frb-ptf-checkbox').prop('checked') ) {
        amount = amount + frb.calculateFee(amount);
        frb.extraData.ptf = 1;
    }
    params.amount = amount;

    // Email optin
    if ( frb.optinRequired && $('input[name="opt_in"]').length > 0 ) {
        var opt_inValue = $('input[name="opt_in"]:checked').val();
        params.opt_in   = opt_inValue; // frb.validateForm() already checked it's 1 or 0
    }

    // Tracking info
    if ( isEndowment || frb.isEndowment ) {
        params.utm_medium = 'endowment';
        params.appeal = 'EndowmentQuote';
    } else {
        params.utm_medium = 'sitenotice';
    }
    params.utm_campaign = mw.centralNotice.data.campaign || 'test';
    params.utm_source   = frb.buildUtmSource(params);

    frb.extraData.time = Math.round( (Date.now() - frb.loadedTime)/1000 );

    if ( !$.isEmptyObject( frb.extraData ) ) {
        params.utm_key = frb.buildUtmKey( frb.extraData );
    }

    // Link to Banner History if enabled
    var mixins = mw.centralNotice.getDataProperty( 'mixins' );
    if ( mixins && mixins.bannerHistoryLogger ) {
        params.bannerhistlog = mw.centralNotice.bannerHistoryLogger.id;
    }

    uri.extend(params);

    // Set a cookie with current location so we can return here from TY page
    mw.loader.using( [ 'mediawiki.cookie', 'mediawiki.util' ] ).then( function () {
        // Exclude URL parameters like banner, but cope with paths like /w/index.php?title=Foo
        var returnToUrl = window.location.origin + mw.util.getUrl();
        mw.cookie.set(
            'fundraising_returnTo',
            returnToUrl,
            { expires: 300, prefix: '', domain: '.wikipedia.org', secure: true }
        );
    });

    if ( mixins && mixins.bannerHistoryLogger ) {
        mw.centralNotice.bannerHistoryLogger.ensureLogSent().always(function() {
            frb.goToPayments( uri );
        });
    } else {
        frb.goToPayments( uri );
    }

};

frb.goToPayments = function( uri ) {
    if ( window.top !== window.self ) {
        // banner is in a frame, open payments in a new tab
        window.open( uri.toString() );
    } else {
        window.location.href = uri.toString();
    }
};

/**
 * Check the form for errors.
 *
 * Called on submission, can also be called on input
 *
 * @param {object} options
 * @return {boolean} Whether form is error-free
 */
frb.validateForm = function( options ) {
    var error = false;

    /* Reset all errors */
    $('.frb-haserror').removeClass('frb-haserror');
    $('.frb-error').hide();

    if ( !options.method ) {
        error = true;
        $('.frb-methods').addClass('frb-haserror');
        $('.frb-error-method').show();
    }

    if ( !frb.validateAmount() ) {
        error = true;
    }

    /* Email optin */
    if ( frb.optinRequired && $('.frb-optin').is(':visible') ) {
        var opt_inValue = $('input[name="opt_in"]:checked').val();
        if ( opt_inValue !== '1' && opt_inValue !== '0' ) {
            $('.frb-optin').addClass('frb-haserror');
            $('.frb-error-optin').show();
            error = true;
        }
    }

    return !error;
};

/**
 * Check if selected amount is valid i.e. a positive number, between minimum and maximum.
 * If not, show an error and return false.
 */
frb.validateAmount = function() {

    var amount = frb.getAmount(),
        currency  = frb.getCurrency( mw.centralNotice.data.country ),
        minAmount = frb.amounts.minimums[ currency ],
        maxAmount = Math.round( frb.maxUSD * minAmount );
        // Math.round to account for floating point math errors: https://phabricator.wikimedia.org/T246262

    if ( amount === null || isNaN(amount) || amount <= 0 || amount < minAmount ) {
        $('fieldset.frb-amounts').addClass('frb-haserror');
        $('.frb-error-bigamount').hide();
        $('.frb-error-smallamount').show();
        return false;
    } else if ( amount > Math.round( maxAmount ) ) {
        $('fieldset.frb-amounts').addClass('frb-haserror');
        $('.frb-error-bigamount').show();
        return false;
    } else {
        $('fieldset.frb-amounts').removeClass('frb-haserror');
        $('.frb-error-smallamount, .frb-error-bigamount').hide();
        return true;
    }
};

/**
 * Build the utm_source for analytics.
 *
 * Own function so it can be overriden for weird tests
 *
 * @param  {Object} params
 * @return {string} utm_source
 */
frb.buildUtmSource = function(params) {

    var utm_source;
    var fullDottedPaymentMethod = params.payment_method;
    if ( params.recurring ) {
        fullDottedPaymentMethod = 'r' + fullDottedPaymentMethod;
    }
    if ( params.payment_submethod ) {
        fullDottedPaymentMethod = fullDottedPaymentMethod + '.' + params.payment_submethod;
    }

    utm_source = mw.centralNotice.data.banner;

    // Keeping opt-in in utm_source for safety for now
    // Eventually remove it, or move to utm_key?
    if ( params.opt_in ) {
        utm_source += '_optIn' + params.opt_in;
    }

    utm_source += '.no-LP.' + fullDottedPaymentMethod;

    return utm_source;
};

/**
 * Build a string for utm_key from extra tracking data
 *
 * @param  {Object} data
 * @return {string} utm_key
 */
frb.buildUtmKey = function(data) {
    var dataArray = [];
    for (var key in data) {
        if (data.hasOwnProperty(key)) {
            dataArray.push( key + '_' + data[key] );
        }
    }
    return dataArray.join('~');
};

/**
 * Determine if we should show recurring choice on step 2
 * 
 * NOTE 2023-12-07: we don't currently use this for step 2, since there are no
 *	banners where users select method before frequency. However it is used by
 *	frb.shouldShowMonthlyConvert()
 *
 * @param  {Object} options     Including method and optional gateway
 * @param  {String} country
 * @return {boolean}
 */
frb.shouldShowRecurring = function( options, country ) {

    if ( frb.isEndowment ) {
        return false;
    }
    if ( frb.noRecurringCountries.indexOf( country ) !== -1 ) { // Defined in LocalizeJS-2017.js
        return false;
    }
    if ( options.method === undefined ) {
        return true; // Show if a method hasn't been selected yet
    }
    if ( [ 'cc', 'venmo', 'apple', 'google' ].indexOf( options.method ) !== -1 ) {
        return true;
    }
    if ( options.method === 'paypal' ) {
    	if ( [ 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ].includes( country ) ) {
    		return false;
    	} else {
    		return true;
    	}
    }
    // Adyen iDEAL
    if ( options.submethod === 'rtbt_ideal' ) {
        return true;
    }
    if ( options.submethod === 'upi' || options.submethod === 'paytmwallet' ) {
        return true;
    }
    return false;
};

/* Is recurring method selected? This function can be overriden for different forms */
frb.getRecurring = function() {
    // Can't use simple form.frequency.value, doesn't work in IE
    var selected = $('#frb-form input[name="frequency"]:checked').val();
    return selected === 'monthly';
};

/* Return amount selected */
frb.getAmount = function() {
    var form = document.getElementById('frb-form');
    var amount = null;
    frb.extraData.otherAmt = 0;

    // If there are some amount radio buttons, then look for the checked one
    if (form.amount) {
        for (var i = 0; i < form.amount.length; i++) {
            if (form.amount[i].checked) {
                amount = form.amount[i].value;
            }
        }
    }

    // Check the "other" amount box
    if (form.otherAmount.value !== '') {
        var otherAmount = form.otherAmount.value;
        otherAmount = otherAmount.replace(/[,.](\d)$/, ':$10');
        otherAmount = otherAmount.replace(/[,.](\d)(\d)$/, ':$1$2');
        otherAmount = otherAmount.replace(/[$£€¥,.]/g, '');
        otherAmount = otherAmount.replace(/:/, '.');
        amount = otherAmount;
        frb.extraData.otherAmt = 1;
    }

    amount = parseFloat(amount);

    if ( isNaN(amount) ) {
        return 0;
    } else {
        return amount;
    }

};

/* Localize the amount errors. Call when initialising banner. */
frb.localizeErrors = function() {
    var currency  = frb.getCurrency( mw.centralNotice.data.country ),
        language = mw.centralNotice.data.uselang,
        minAmount = frb.amounts.minimums[ currency ],
        maxAmount = Math.round( frb.maxUSD * minAmount );
        // Math.round to account for floating point math errors: https://phabricator.wikimedia.org/T246262

    $('.frb-error-smallamount').text( function( index, oldText ) {
        return oldText.replace( '$1', frb.formatCurrency(currency, minAmount, language)  );
    });

    $('.frb-error-bigamount').text( function( index, oldText ) {
        // We cannot accept donations greater than $1 $2 through our website. Please contact our major gifts staff at $3.
        return oldText.replace( '$1', maxAmount )
                      .replace( '$2', currency )
                      .replace( '$3', 'benefactors@wikimedia.org' );
    });
};

/**
 * Shared code for amount input handling
 */
frb.initAmountOptions = function() {

    // Reset "Other" input if user clicks a preset amount
    $('#frb-form [id^=frb-amt-ps]').click(function() {
        $('#frb-amt-other-input').val('');
    });

    // Track if they selected and then later changed amount
    var checkAmountChange = function(e) {
        if ( frb.didSelectAmount ) {
            frb.extraData.changedAmt = 1;
        }
        // check if amount radio button is selected OR there is a value in the other amount
        if ( $('.frb-amounts input[type="radio"]:checked').val() !== 'Other' || $('#frb-amt-other-input').val().length > 0 ) {
            frb.didSelectAmount = true;
        }
        return;
    };

    $('.frb-amounts input[type="radio"]').on('change', checkAmountChange);
    $('#frb-amt-other-input').on('focusout', checkAmountChange);

    // Block typing non-numerics in input field, otherwise Safari allows them and then chokes
    // https://phabricator.wikimedia.org/T118741, https://phabricator.wikimedia.org/T173431
    var blockNonNumeric = function(e) {
        // Allow special keys in Firefox
        if ((e.code == 'ArrowLeft') || (e.code == 'ArrowRight') ||
            (e.code == 'ArrowUp') || (e.code == 'ArrowDown') ||
            (e.code == 'Delete') || (e.code == 'Backspace')) {
            return;
        }
        var chr = String.fromCharCode(e.which);
        if ("0123456789., ".indexOf(chr) === -1) {
            return false;
        }
    };
    $('#frb-amt-other-input').on('keypress', blockNonNumeric);
    $('#frb-amt-monthly-other-input').on('keypress', blockNonNumeric);

};

/**
 * Calculate approximate transaction fee on given amount
 *
 * @param  {number} amount
 * @return {number}        Rounded to 2 decimal places
 */
frb.calculateFee = function(amount) {
    var currency = frb.getCurrency(mw.centralNotice.data.country),
        feeMultiplier = 0.04,
        feeMinimum = frb.amounts.feeMinimums[currency] || 0.35,
        feeAmount = amount * feeMultiplier;

    if ( feeAmount < feeMinimum ) {
      feeAmount = feeMinimum;
    }
    return parseFloat(feeAmount.toFixed(2));
};

frb.updateFeeDisplay = function() {
    var currency = frb.getCurrency(mw.centralNotice.data.country),
        language = mw.centralNotice.data.uselang,
        amount, feeAmount, totalAmount;

    amount = frb.getAmount();
    feeAmount = frb.calculateFee(amount);
    if ( $('#frb-ptf-checkbox').prop('checked') ) {
        totalAmount = amount + feeAmount;
    } else {
        totalAmount = amount;
    }

    var feeAmountFormatted = frb.formatCurrency(currency, feeAmount, language);
    $('.frb-ptf-fee').text(feeAmountFormatted);

    var totalAmountFormatted = frb.formatCurrency(currency, totalAmount, language);
    $('.frb-ptf-total').text(totalAmountFormatted);

    $('.frb-ptf').slideDown( frb.reduceMotion ? 0 : 400 );
};

/**
 * Custom hide cookie function
 *
 * Purposely sets only for this domain.
 * CentralNotice builtin method seems buggy - see T270401
 *
 * @param {string} reason Reason to store in the hide cookie
 * @param {number} duration Cookie duration, in seconds
 */
frb.altSetHideCookie = function ( reason, duration ) {

    mw.loader.using( 'mediawiki.cookie' ).then( function () {

        var cookieName = 'centralnotice_hide_fundraising',
            date = new Date(),
            hideData = {
                v: 1,
                created: Math.floor( date.getTime() / 1000 ),
                reason: reason
            };

        // Re-use the same date object to set the cookie's expiry time
        date.setSeconds( date.getSeconds() + duration );

        mw.cookie.set(
            cookieName,
            JSON.stringify( hideData ),
            { expires: date, path: '/', domain: 'wikipedia.org', prefix: '' }
        );

    });

};

/**
 * Determine if banner should be shown, and set correct data for impression logging
 *
 * @return {boolean} Show banner?
 */
frb.shouldShowBanner = function() {

    mw.centralNotice.bannerData.hideResult = false;

    /* Hide in unsupported browsers */
    if ( !frb.supportedBrowser ) {
        mw.centralNotice.bannerData.hideResult = true;
        mw.centralNotice.bannerData.hideReason = 'browser';
    }

    /* Hide outside main namespace (except Main Page, for sites where it isn't in main namespace) */
    if ( mw.config.get('wgNamespaceNumber') > 0 && !mw.config.get('wgIsMainPage') ) {
        mw.centralNotice.bannerData.hideResult = true;
        mw.centralNotice.bannerData.hideReason = 'namespace';
    }

    // Hide banner on sensitive articles
    // TODO - possibly add wgWikibaseItemId for multilingual support and resilience to moves?
    var hideTitles = [ 'Murder of Don Banfield' ];
    if ( hideTitles.indexOf( mw.config.values.wgTitle ) !== -1 ) {
        mw.centralNotice.bannerData.hideResult = true;
        mw.centralNotice.bannerData.hideReason = 'article';
    }

    /* Hide banner if on wrong site (desktop/mobile) in case wrong device settings were chosen */
    var bannerName = mw.centralNotice.data.banner,
        skin = mw.config.get('skin');
    if (
         ( bannerName.indexOf('_dsk_') !== -1 && skin === 'minerva' ) ||
         ( bannerName.indexOf('_m_') !== -1 && skin !== 'minerva' )
    ) {
        mw.centralNotice.bannerData.hideResult = true;
        mw.centralNotice.bannerData.hideReason = 'other';
        console.warn('Hiding fundraising banner on wrong site (desktop/mobile)');
    }

    return !mw.centralNotice.bannerData.hideResult;

};

/* Debug function to highlight dynamically replaced elements */
frb.highlightReplacements = function() {
    $('.frb [class^="frb-replace"], .frb-ptf-fee, .frb-ptf-total, .frb-upsell-ask, frb-amt').css('background-color', '#fa0');
};

if ( !mw.centralNotice.adminUi ) { // T262693
    /**
     * Provides alterImpressionData hook for CentralNotice
     * This info will be sent back with Special:RecordImpression
     * TODO: check if/when we can remove this (and RecordImpression)
     */
    mediaWiki.centralNotice.bannerData.alterImpressionData = function( impressionData ) {
        // Returning true from this function indicates the banner was shown
        if (mediaWiki.centralNotice.bannerData.hideReason) {
            impressionData.reason = mediaWiki.centralNotice.bannerData.hideReason;
        }
        if (mediaWiki.centralNotice.bannerData.cookieCount) {
            impressionData.banner_count = mediaWiki.centralNotice.bannerData.cookieCount;
        }

        return !mediaWiki.centralNotice.bannerData.hideResult;
    };
}

/* End of MediaWiki:FundraisingBanners/CoreJS-2018.js */