MediaWiki:FundraisingBanners/CoreJS-2018.js
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)
- Edge: Hold Ctrl while clicking Refresh, or 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 url = new URL('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
}
if ( frb.isDarkMode() ) {
frb.extraData.darkMode = 1;
}
// Skip form chooser for Venmo
if ( options.method === 'venmo' && options.gateway !== 'gravy' ) {
url = new URL('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.wmf_medium = 'endowment';
params.appeal = 'EndowmentQuote';
} else {
params.wmf_medium = 'sitenotice';
}
params.wmf_campaign = mw.centralNotice.data.campaign || 'test';
params.wmf_source = frb.buildTrackingSource(params);
frb.extraData.time = Math.round( (Date.now() - frb.loadedTime)/1000 );
if ( !$.isEmptyObject( frb.extraData ) ) {
params.wmf_key = frb.buildTrackingKey( frb.extraData );
}
// Link to Banner History if enabled
var mixins = mw.centralNotice.getDataProperty( 'mixins' );
if ( mixins && mixins.bannerHistoryLogger ) {
params.bannerhistlog = mw.centralNotice.bannerHistoryLogger.id;
}
for ( var key of Object.keys( params ) ) {
url.searchParams.set( key, params[key] );
}
// 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( url );
});
} else {
frb.goToPayments( url );
}
};
frb.goToPayments = function( url ) {
if ( window.top !== window.self ) {
// banner is in a frame, open payments in a new tab
window.open( url.toString() );
} else {
window.location.href = url.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 wmf_source for analytics.
*
* Own function so it can be overriden for weird tests
*
* @param {Object} params
* @return {string} wmf_source
*/
frb.buildTrackingSource = function(params) {
var wmf_source;
var fullDottedPaymentMethod = params.payment_method;
if ( params.recurring ) {
fullDottedPaymentMethod = 'r' + fullDottedPaymentMethod;
}
if ( params.payment_submethod ) {
fullDottedPaymentMethod = fullDottedPaymentMethod + '.' + params.payment_submethod;
}
wmf_source = mw.centralNotice.data.banner;
// Keeping opt-in in wmf_source for safety for now
// Eventually remove it, or move to wmf_key?
if ( params.opt_in ) {
wmf_source += '_optIn' + params.opt_in;
}
wmf_source += '.no-LP.' + fullDottedPaymentMethod;
return wmf_source;
};
/**
* Build a string for wmf_key from extra tracking data
*
* @param {Object} data
* @return {string} wmf_key
*/
frb.buildTrackingKey = 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;
}
// SEPA
if ( options.submethod === 'sepadirectdebit' ) {
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: '' }
);
});
};
frb.showDonateLinkTooltip = function ( content ) {
try {
mw.loader.using( [ 'oojs-ui-core' ] ).done( function () {
let $donateLink = $( '#pt-sitesupport-2 a, #pt-sitesupport a, #n-sitesupport a, #p-donation a' );
$donateLink.attr( 'href', ( i, oldUrl ) => {
let url = new URL( oldUrl, 'https://donate.wikimedia.org' ); // base needed because some links are protocol relative
url.searchParams.delete( 'utm_source' ); // Until we have updated sidebar links
url.searchParams.set( 'wmf_source', 'tooltipOnBannerClose' );
return url.toString();
});
let popupOptions = {
$content: $( '<p>' + content + '</p>' ),
padded: true,
autoclose: true,
align: 'forwards',
autoFlip: false
};
if ( document.querySelector( '#p-donation a' ) ) {
// Minerva
popupOptions.$floatableContainer = $( '.navigation-drawer' );
popupOptions.position = 'below';
} else if ( $( '#pt-sitesupport-2 a:visible' ).length > 0 ) {
// Vector 2022 user tools
popupOptions.$floatableContainer = $( '#pt-sitesupport-2 a' );
popupOptions.position = 'below';
} else if ( document.querySelector( '#pt-sitesupport a' ) ) {
// Vector 2022 user tools collapsed in menu
popupOptions.$floatableContainer = $( '#vector-user-links-dropdown' );
popupOptions.position = 'below';
} else if ( document.querySelector( '#vector-main-menu-dropdown #n-sitesupport a') ) {
// Vector 2022 main menu (only when logged in, so mostly here for testing)
popupOptions.$floatableContainer = $( '#vector-main-menu-dropdown' );
popupOptions.position = 'below';
} else if ( document.querySelector( '#n-sitesupport a' ) ) {
// Legacy Vector (sidebar)
popupOptions.$floatableContainer = $( '#n-sitesupport a' );
popupOptions.position = 'after';
}
let popup = new OO.ui.PopupWidget( popupOptions );
popup.$element.css('z-index', 5); // Fix so it shows above header
$( document.body ).append( popup.$element );
popup.toggle( true );
setTimeout( () => {
popup.$element.fadeOut( frb.fadeDuration );
}, 5000 );
} );
} catch (e) {
console.log('Problem showing banner close tooltip');
}
};
frb.showSidebarTooltip = frb.showDonateLinkTooltip; // Alias for old name
frb.isDarkMode = function() {
let rootClasses = document.documentElement.classList,
osDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return rootClasses.contains( 'skin-theme-clientpref-night' ) ||
( rootClasses.contains( 'skin-theme-clientpref-os' ) && osDark );
};
/**
* 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',
'Asian News International',
'Asian News International vs. Wikimedia Foundation'
];
var pageTitle = mw.config.get('wgTitle');
if (
hideTitles.indexOf( pageTitle ) !== -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'),
siteName = mw.config.get('wgSiteName');
if (
( bannerName.indexOf('_dsk_') !== -1 && skin === 'minerva' ) ||
( bannerName.indexOf('_m_') !== -1 && skin !== 'minerva' ) ||
skin === 'wikimediaapiportal' || // workaround for T270308
siteName === 'Wikitech'
) {
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 */