installed plugin Easy Digital Downloads version 3.1.0.3

This commit is contained in:
2022-11-27 15:03:07 +00:00
committed by Gitium
parent 555673545b
commit c5dce2cec6
1200 changed files with 238970 additions and 0 deletions

View File

@ -0,0 +1,104 @@
/* global $, edd_stripe_admin */
/**
* Internal dependencies.
*/
import './../../../css/src/admin.scss';
import './settings/index.js';
let testModeCheckbox;
let testModeToggleNotice;
$( document ).ready( function() {
testModeCheckbox = document.getElementById( 'edd_settings[test_mode]' );
if ( testModeCheckbox ) {
testModeToggleNotice = document.getElementById( 'edd_settings[stripe_connect_test_mode_toggle_notice]' );
EDD_Stripe_Connect_Scripts.init();
}
// Toggle API keys.
$( '.edds-api-key-toggle button' ).on( 'click', function( event ) {
event.preventDefault();
$( '.edds-api-key-toggle, .edds-api-key-row' )
.toggleClass( 'edd-hidden' );
} );
} );
const EDD_Stripe_Connect_Scripts = {
init() {
this.listeners();
},
listeners() {
const self = this;
testModeCheckbox.addEventListener( 'change', function() {
// Don't run these events if Stripe is not enabled.
if ( ! edd_stripe_admin.stripe_enabled ) {
return;
}
if ( this.checked ) {
if ( 'false' === edd_stripe_admin.test_key_exists ) {
self.showNotice( testModeToggleNotice, 'warning' );
self.addHiddenMarker();
} else {
self.hideNotice( testModeToggleNotice );
const hiddenMarker = document.getElementById( 'edd-test-mode-toggled' );
if ( hiddenMarker ) {
hiddenMarker.parentNode.removeChild( hiddenMarker );
}
}
}
if ( ! this.checked ) {
if ( 'false' === edd_stripe_admin.live_key_exists ) {
self.showNotice( testModeToggleNotice, 'warning' );
self.addHiddenMarker();
} else {
self.hideNotice( testModeToggleNotice );
const hiddenMarker = document.getElementById( 'edd-test-mode-toggled' );
if ( hiddenMarker ) {
hiddenMarker.parentNode.removeChild( hiddenMarker );
}
}
}
} );
},
addHiddenMarker() {
const submit = document.getElementById( 'submit' );
if ( ! submit ) {
return;
}
submit.parentNode.insertAdjacentHTML( 'beforeend', '<input type="hidden" class="edd-hidden" id="edd-test-mode-toggled" name="edd-test-mode-toggled" />' );
},
showNotice( element = false, type = 'error' ) {
if ( ! element ) {
return;
}
if ( typeof element !== 'object' ) {
return;
}
element.className = 'notice notice-' + type;
},
hideNotice( element = false ) {
if ( ! element ) {
return;
}
if ( typeof element !== 'object' ) {
return;
}
element.className = 'edd-hidden';
},
};

View File

@ -0,0 +1,36 @@
/* global wp, jQuery */
/**
* Handle dismissing admin notices.
*/
jQuery( () => {
/**
* Loops through each admin notice on the page for processing.
*
* @param {HTMLElement} noticeEl Notice element.
*/
jQuery( '.edds-admin-notice' ).each( function() {
const notice = $( this );
const id = notice.data( 'id' );
const nonce = notice.data( 'nonce' );
/**
* Listens for a click event on the dismiss button, and dismisses the notice.
*
* @param {Event} e Click event.
* @return {jQuery.Deferred} Deferred object.
*/
notice.on( 'click', '.notice-dismiss', ( e ) => {
e.preventDefault();
e.stopPropagation();
return wp.ajax.post(
'edds_admin_notices_dismiss_ajax',
{
id,
nonce,
}
);
} );
} );
} );

View File

@ -0,0 +1,5 @@
/**
* Internal dependencies
*/
import './requirements.js';
import './stripe-connect.js';

View File

@ -0,0 +1,40 @@
/**
* Internal dependencies
*/
import { domReady } from 'utils';
/**
* Hides "Save Changes" button if showing the special settings placeholder.
*/
domReady( () => {
const containerEl = document.querySelector( '.edds-requirements-not-met' );
if ( ! containerEl ) {
return;
}
// Hide "Save Changes" button.
document.querySelector( '.edd-settings-wrap .submit' ).style.display = 'none';
} );
/**
* Moves "Payment Gateways" notice under Stripe.
* Disables/unchecks the checkbox.
*/
domReady( () => {
const noticeEl = document.getElementById( 'edds-payment-gateways-stripe-unmet-requirements' );
if ( ! noticeEl ) {
return;
}
const stripeLabel = document.querySelector( 'label[for="edd_settings[gateways][stripe]"]' );
stripeLabel.parentNode.insertBefore( noticeEl, stripeLabel.nextSibling );
const stripeCheck = document.getElementById( 'edd_settings[gateways][stripe]' );
stripeCheck.disabled = true;
stripeCheck.checked = false;
noticeEl.insertBefore( stripeCheck, noticeEl.querySelector( 'p' ) );
noticeEl.insertBefore( stripeLabel, noticeEl.querySelector( 'p' ) );
} );

View File

@ -0,0 +1,29 @@
/**
* Internal dependencies
*/
import { domReady, apiRequest } from 'utils';
// Wait for DOM.
domReady( () => {
const containerEl = document.getElementById( 'edds-stripe-connect-account' );
const actionsEl = document.getElementById( 'edds-stripe-disconnect-reconnect' );
if ( ! containerEl ) {
return;
}
return apiRequest( 'edds_stripe_connect_account_info', {
...containerEl.dataset,
} )
.done( ( response ) => {
containerEl.innerHTML = response.message;
containerEl.classList.add( `notice-${ response.status }` );
if ( response.actions ) {
actionsEl.innerHTML = response.actions;
}
} )
.fail( ( error ) => {
containerEl.innerHTML = error.message;
containerEl.classList.add( 'notice-error' );
} );
} );

View File

@ -0,0 +1,2 @@
export { default as Modal } from './modal';
export { paymentMethods } from './payment-methods';

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
// Import Polyfills for MicroModal IE 11 support.
// https://github.com/Ghosh/micromodal#ie-11-and-below
// https://github.com/ghosh/Micromodal/issues/49#issuecomment-424213347
// https://github.com/ghosh/Micromodal/issues/49#issuecomment-517916416
import 'core-js/modules/es.object.assign';
import 'core-js/modules/es.array.from';
import MicroModal from 'micromodal';
const DEFAULT_CONFIG = {
disableScroll: true,
awaitOpenAnimation: true,
awaitCloseAnimation: true,
};
function setup( options ) {
const config = {
...DEFAULT_CONFIG,
...options,
};
MicroModal.init( config );
}
function open( modalId, options ) {
const config = {
...DEFAULT_CONFIG,
...options,
};
MicroModal.show( modalId, config );
}
function close( modalId ) {
MicroModal.close( modalId );
}
export default {
setup,
open,
close,
};

View File

@ -0,0 +1,259 @@
/* global $ */
/**
* Internal dependencies.
*/
import { forEach, getNextSiblings } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
*
*/
export function paymentMethods() {
// Toggle only shows if using Full Address (for some reason).
if ( getBillingFieldsToggle() ) {
// Hide fields initially.
toggleBillingFields( false );
/**
* Binds change event to "Update billing address" toggle to show/hide address fields.
*
* @param {Event} e Change event.
*/
getBillingFieldsToggle().addEventListener( 'change', function( e ) {
return toggleBillingFields( e.target.checked );
} );
}
// Payment method toggles.
const existingPaymentMethods = document.querySelectorAll( '.edd-stripe-existing-card' );
if ( 0 !== existingPaymentMethods.length ) {
forEach( existingPaymentMethods, function( existingPaymentMethod ) {
/**
* Binds change event to credit card toggles.
*
* @param {Event} e Change event.
*/
return existingPaymentMethod.addEventListener( 'change', function( e ) {
return onPaymentSourceChange( e.target );
} );
} );
// Simulate change of payment method to populate current fields.
let currentPaymentMethod = document.querySelector( '.edd-stripe-existing-card:checked' );
if ( ! currentPaymentMethod ) {
currentPaymentMethod = document.querySelector( '.edd-stripe-existing-card:first-of-type' );
currentPaymentMethod.checked = true;
}
const paymentMethodChangeEvent = document.createEvent( 'Event' );
paymentMethodChangeEvent.initEvent( 'change', true, false );
currentPaymentMethod.dispatchEvent( paymentMethodChangeEvent );
}
}
/**
* Determines if the billing fields can be toggled.
*
* @return {Bool} True if the toggle exists.
*/
function getBillingFieldsToggle() {
return document.getElementById( 'edd-stripe-update-billing-address' );
}
/**
* Toggles billing fields visiblity.
*
* Assumes the toggle control is the first item in the "Billing Details" fieldset.
*
* @param {Bool} isVisible Billing item visibility.
*/
function toggleBillingFields( isVisible ) {
const updateAddressWrapperEl = document.querySelector( '.edd-stripe-update-billing-address-wrapper' );
if ( ! updateAddressWrapperEl ) {
return;
}
// Find all elements after the toggle.
const billingFieldWrappers = getNextSiblings( updateAddressWrapperEl );
const billingAddressPreview = document.querySelector( '.edd-stripe-update-billing-address-current' );
billingFieldWrappers.forEach( function( wrap ) {
wrap.style.display = isVisible ? 'block' : 'none';
} );
// Hide address preview.
if ( billingAddressPreview ) {
billingAddressPreview.style.display = isVisible ? 'none' : 'block';
}
}
/**
* Manages UI state when the payment source changes.
*
* @param {HTMLElement} paymentSource Selected payment source. (Radio element with data).
*/
function onPaymentSourceChange( paymentSource ) {
const isNew = 'new' === paymentSource.value;
const newCardForm = document.querySelector( '.edd-stripe-new-card' );
const billingAddressToggle = document.querySelector( '.edd-stripe-update-billing-address-wrapper' );
// Toggle card details field.
newCardForm.style.display = isNew ? 'block' : 'none';
if ( billingAddressToggle ) {
billingAddressToggle.style.display = isNew ? 'none' : 'block';
}
// @todo don't be lazy.
$( '.edd-stripe-card-radio-item' ).removeClass( 'selected' );
$( paymentSource ).closest( '.edd-stripe-card-radio-item' ).addClass( 'selected' );
const addressFieldMap = {
card_address: 'address_line1',
card_address_2: 'address_line2',
card_city: 'address_city',
card_state: 'address_state',
card_zip: 'address_zip',
billing_country: 'address_country',
};
// New card is being used, show fields and reset them.
if ( isNew ) {
// Reset all fields.
for ( const addressEl in addressFieldMap ) {
if ( ! addressFieldMap.hasOwnProperty( addressEl ) ) {
return;
}
const addressField = document.getElementById( addressEl );
if ( addressField ) {
addressField.value = '';
addressField.selected = '';
}
}
// Recalculate taxes.
if ( window.EDD_Checkout.recalculate_taxes ) {
window.EDD_Checkout.recalculate_taxes();
}
// Show billing fields.
toggleBillingFields( true );
// Existing card is being used.
// Ensure the billing fields are hidden, and update their values with saved information.
} else {
const addressString = [];
const billingDetailsEl = document.getElementById( paymentSource.id + '-billing-details' );
if ( ! billingDetailsEl ) {
return;
}
// Hide billing fields.
toggleBillingFields( false );
// Uncheck "Update billing address"
if ( getBillingFieldsToggle() ) {
getBillingFieldsToggle().checked = false;
}
// Update billing address fields with saved card values.
const billingDetails = billingDetailsEl.dataset;
for ( const addressEl in addressFieldMap ) {
if ( ! addressFieldMap.hasOwnProperty( addressEl ) ) {
continue;
}
const addressField = document.getElementById( addressEl );
if ( ! addressField ) {
continue;
}
const value = billingDetails[ addressFieldMap[ addressEl ] ];
// Set field value.
addressField.value = value;
// Generate an address string from values.
if ( '' !== value ) {
addressString.push( value );
}
// This field is required but does not have a saved value, show all fields.
if ( addressField.required && '' === value ) {
// @todo DRY up some of this DOM usage.
toggleBillingFields( true );
if ( getBillingFieldsToggle() ) {
getBillingFieldsToggle().checked = true;
}
if ( billingAddressToggle ) {
billingAddressToggle.style.display = 'none';
}
}
// Trigger change event when the Country field is updated.
if ( 'billing_country' === addressEl ) {
const changeEvent = document.createEvent( 'Event' );
changeEvent.initEvent( 'change', true, true );
addressField.dispatchEvent( changeEvent );
}
}
/**
* Monitor AJAX requests for address changes.
*
* Wait for the "State" field to be updated based on the "Country" field's
* change event. Once there is an AJAX response fill the "State" field with the
* saved card's State data and recalculate taxes.
*
* @since 2.7
*
* @param {Object} event
* @param {Object} xhr
* @param {Object} options
*/
$( document ).ajaxSuccess( function( event, xhr, options ) {
if ( ! options || ! options.data || ! xhr ) {
return;
}
if (
options.data.includes( 'action=edd_get_shop_states' ) &&
options.data.includes( 'field_name=card_state' ) &&
( xhr.responseText && xhr.responseText.includes( 'card_state' ) )
) {
const stateField = document.getElementById( 'card_state' );
if ( stateField ) {
stateField.value = billingDetails.address_state;
// Recalculate taxes.
if ( window.EDD_Checkout.recalculate_taxes ) {
window.EDD_Checkout.recalculate_taxes( stateField.value );
}
}
}
} );
// Update address string summary.
const billingAddressPreview = document.querySelector( '.edd-stripe-update-billing-address-current' );
if ( billingAddressPreview ) {
billingAddressPreview.innerText = addressString.join( ', ' );
const { brand, last4 } = billingDetails;
document.querySelector( '.edd-stripe-update-billing-address-brand' ).innerHTML = brand;
document.querySelector( '.edd-stripe-update-billing-address-last4' ).innerHTML = last4;
}
}
}

View File

@ -0,0 +1,64 @@
/* global Stripe, edd_stripe_vars */
/**
* Internal dependencies
*/
import './../../../css/src/frontend.scss';
import { domReady, apiRequest, generateNotice } from 'utils';
import {
setupCheckout,
setupProfile,
setupPaymentHistory,
setupBuyNow,
setupDownloadPRB,
setupCheckoutPRB,
} from 'frontend/payment-forms';
import {
paymentMethods,
} from 'frontend/components/payment-methods';
import {
mountCardElement,
createPaymentForm as createElementsPaymentForm,
getBillingDetails,
getPaymentMethod,
confirm as confirmIntent,
handle as handleIntent,
retrieve as retrieveIntent,
} from 'frontend/stripe-elements';
// eslint-enable @wordpress/dependency-group
( () => {
try {
window.eddStripe = new Stripe( edd_stripe_vars.publishable_key );
// Alias some functionality for external plugins.
window.eddStripe._plugin = {
domReady,
apiRequest,
generateNotice,
mountCardElement,
createElementsPaymentForm,
getBillingDetails,
getPaymentMethod,
confirmIntent,
handleIntent,
retrieveIntent,
paymentMethods,
};
// Setup frontend components when DOM is ready.
domReady(
setupCheckout,
setupProfile,
setupPaymentHistory,
setupBuyNow,
setupDownloadPRB,
setupCheckoutPRB,
);
} catch ( error ) {
alert( error.message );
}
} )();

View File

@ -0,0 +1,205 @@
/* global jQuery, edd_scripts, edd_stripe_vars */
/**
* Internal dependencies
*/
import { forEach, domReady, apiRequest } from 'utils';
import { Modal, paymentMethods } from 'frontend/components';
import { paymentForm } from 'frontend/payment-forms/checkout'
/**
* Adds a Download to the Cart.
*
* @param {number} downloadId Download ID.
* @param {number} priceId Download Price ID.
* @param {number} quantity Download quantity.
* @param {string} nonce Nonce token.
* @param {HTMLElement} addToCartForm Add to cart form.
*
* @return {Promise}
*/
function addToCart( downloadId, priceId, quantity, nonce, addToCartForm ) {
const data = {
download_id: downloadId,
price_id: priceId,
quantity: quantity,
nonce,
post_data: jQuery( addToCartForm ).serialize(),
};
return apiRequest( 'edds_add_to_cart', data );
}
/**
* Empties the Cart.
*
* @return {Promise}
*/
function emptyCart() {
return apiRequest( 'edds_empty_cart' );
}
/**
* Displays the Buy Now modal.
*
* @param {Object} args
* @param {number} args.downloadId Download ID.
* @param {number} args.priceId Download Price ID.
* @param {number} args.quantity Download quantity.
* @param {string} args.nonce Nonce token.
* @param {HTMLElement} args.addToCartForm Add to cart form.
*/
function buyNowModal( args ) {
const modalContent = document.querySelector( '#edds-buy-now-modal-content' );
const modalLoading = '<span class="edd-loading-ajax edd-loading"></span>';
// Show modal.
Modal.open( 'edds-buy-now', {
/**
* Adds the item to the Cart when opening.
*/
onShow() {
modalContent.innerHTML = modalLoading;
const {
downloadId,
priceId,
quantity,
nonce,
addToCartForm,
} = args;
addToCart(
downloadId,
priceId,
quantity,
nonce,
addToCartForm
)
.then( ( { checkout } ) => {
// Show Checkout HTML.
modalContent.innerHTML = checkout;
// Reinitialize core JS.
window.EDD_Checkout.init();
const totalEl = document.querySelector( '#edds-buy-now-modal-content .edd_cart_amount' );
const total = parseFloat( totalEl.dataset.total );
// Reinitialize Stripe JS if a payment is required.
if ( total > 0 ) {
paymentForm();
paymentMethods();
}
} )
.fail( ( { message } ) => {
// Show error message.
document.querySelector( '#edds-buy-now-modal-content' ).innerHTML = message;
} );
},
/**
* Empties Cart on close.
*/
onClose() {
emptyCart();
}
} );
}
// DOM ready.
export function setup() {
// Find all "Buy Now" links on the page.
forEach( document.querySelectorAll( '.edds-buy-now' ), ( el ) => {
// Don't use modal if "Free Downloads" is active and available for this download.
// https://easydigitaldownloads.com/downloads/free-downloads/
if ( el.classList.contains( 'edd-free-download' ) ) {
return;
}
/**
* Launches "Buy Now" modal when clicking "Buy Now" link.
*
* @param {Object} e Click event.
*/
el.addEventListener( 'click', ( e ) => {
const { downloadId, nonce } = e.currentTarget.dataset;
// Stop other actions if a Download ID is found.
if ( ! downloadId ) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
// Gather Download information.
let priceId = 0;
let quantity = 1;
const addToCartForm = e.currentTarget.closest(
'.edd_download_purchase_form'
);
// Price ID.
const priceIdEl = addToCartForm.querySelector(
`.edd_price_option_${downloadId}:checked`
);
if ( priceIdEl ) {
priceId = priceIdEl.value;
}
// Quantity.
const quantityEl = addToCartForm.querySelector(
'input[name="edd_download_quantity"]'
);
if ( quantityEl ) {
quantity = quantityEl.value;
}
buyNowModal( {
downloadId,
priceId,
quantity,
nonce,
addToCartForm
} );
} );
} );
/**
* Replaces submit button text after validation errors.
*
* If there are no other items in the cart the core javascript will replace
* the button text with the value for a $0 cart (usually "Free Download")
* because the script variables were constructed when nothing was in the cart.
*/
jQuery( document.body ).on( 'edd_checkout_error', () => {
const submitButtonEl = document.querySelector(
'#edds-buy-now #edd-purchase-button'
);
if ( ! submitButtonEl ) {
return;
}
const { i18n: { completePurchase } } = edd_stripe_vars;
const amountEl = document.querySelector( '.edd_cart_amount' );
const { total, totalCurrency } = amountEl.dataset;
if ( '0' === total ) {
return;
}
// For some reason a delay is needed to override the value set by
// https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/assets/js/edd-ajax.js#L414
setTimeout( () => {
submitButtonEl.value = `${ totalCurrency } - ${ completePurchase }`;
}, 10 );
} );
}

View File

@ -0,0 +1,36 @@
/* global $, edd_scripts */
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import { paymentMethods } from 'frontend/components';
// eslint-enable @wordpress/dependency-group
import { paymentForm } from './payment-form.js';
export * from './payment-form.js';
export function setup() {
if ( '1' !== edd_scripts.is_checkout ) {
return;
}
// Initial load for single gateway.
const singleGateway = document.querySelector( 'input[name="edd-gateway"]' );
if ( singleGateway && 'stripe' === singleGateway.value ) {
paymentForm();
paymentMethods();
}
// Gateway switch.
$( document.body ).on( 'edd_gateway_loaded', ( e, gateway ) => {
if ( 'stripe' !== gateway ) {
return;
}
paymentForm();
paymentMethods();
} );
}

View File

@ -0,0 +1,272 @@
/* global $, edd_stripe_vars, edd_global_vars */
/**
* Internal dependencies
*/
import {
createPaymentForm as createElementsPaymentForm,
getPaymentMethod,
capture as captureIntent,
handle as handleIntent,
} from 'frontend/stripe-elements'; // eslint-disable-line @wordpress/dependency-group
import { apiRequest, generateNotice } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Binds Payment submission functionality.
*
* Resets before rebinding to avoid duplicate events
* during gateway switching.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
// Bind form submission.
// Needs to be jQuery since that is what core submits against.
$( '#edd_purchase_form' ).off( 'submit', onSubmit );
$( '#edd_purchase_form' ).on( 'submit', onSubmit );
// SUPER ghetto way to watch for core form validation because no events are in place.
// Called after the purchase form is submitted (via `click` or `submit`)
$( document ).off( 'ajaxSuccess', watchInitialValidation );
$( document ).on( 'ajaxSuccess', watchInitialValidation );
}
/**
* Processes Stripe gateway-specific functionality after core AJAX validation has run.
*/
async function onSubmitDelay() {
try {
// Form data to send to intent requests.
let formData = $( '#edd_purchase_form' ).serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Retrieve or create a PaymentMethod.
const paymentMethod = await getPaymentMethod( document.getElementById( 'edd_purchase_form' ), window.eddStripe.cardElement );
// Run the modified `_edds_process_purchase_form` and create an Intent.
const {
intent: initialIntent,
nonce: refreshedNonce
} = await processForm( paymentMethod.id, paymentMethod.exists );
// Update existing nonce value in DOM form data in case data is retrieved
// again directly from the DOM.
$( '#edd-process-checkout-nonce' ).val( refreshedNonce );
// Handle any actions required by the Intent State Machine (3D Secure, etc).
const handledIntent = await handleIntent(
initialIntent,
{
form_data: formData += `&edd-process-checkout-nonce=${ refreshedNonce }`,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
}
);
// Create an EDD payment record.
const { intent, nonce } = await createPayment( handledIntent );
// Capture any unpcaptured intents.
const finalIntent = await captureIntent(
intent,
{},
nonce
);
// Attempt to transition payment status and redirect.
// @todo Maybe confirm payment status as well? Would need to generate a custom
// response because the private EDD_Payment properties are not available.
if (
( 'succeeded' === finalIntent.status ) ||
( 'canceled' === finalIntent.status && 'abandoned' === finalIntent.cancellation_reason )
) {
await completePayment( finalIntent, nonce );
window.location.replace( edd_stripe_vars.successPageUri );
} else {
window.location.replace( edd_stripe_vars.failurePageUri );
}
} catch ( error ) {
handleException( error );
enableForm();
}
}
/**
* Processes the purchase form.
*
* Generates purchase data for the current session and
* uses the PaymentMethod to generate an Intent based on data.
*
* @param {string} paymentMethodId PaymentMethod ID.
* @param {Bool} paymentMethodExists If the PaymentMethod has already been attached to a customer.
* @return {Promise} jQuery Promise.
*/
export function processForm( paymentMethodId, paymentMethodExists ) {
let tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_process_purchase_form', {
// Send available form data.
form_data: $( '#edd_purchase_form' ).serialize(),
payment_method_id: paymentMethodId,
payment_method_exists: paymentMethodExists,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
}
/**
* Complete a Payment in EDD.
*
* @param {object} intent Intent.
* @return {Promise} jQuery Promise.
*/
export function createPayment( intent ) {
const paymentForm = $( '#edd_purchase_form' ),
tokenInput = $( '#edd-process-stripe-token' );
let formData = paymentForm.serialize();
// Attempt to find the Checkout nonce directly.
if ( paymentForm.length === 0 ) {
const nonce = $( '#edd-process-checkout-nonce' ).val();
formData = `edd-process-checkout-nonce=${ nonce }`
}
return apiRequest( 'edds_create_payment', {
form_data: formData,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
intent,
} );
}
/**
* Complete a Payment in EDD.
*
* @param {object} intent Intent.
* @param {string} refreshedNonce A refreshed nonce that might be needed if the
* user logged in.
* @return {Promise} jQuery Promise.
*/
export function completePayment( intent, refreshedNonce ) {
const paymentForm = $( '#edd_purchase_form' );
let formData = paymentForm.serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Attempt to find the Checkout nonce directly.
if ( paymentForm.length === 0 ) {
const nonce = $( '#edd-process-checkout-nonce' ).val();
formData = `edd-process-checkout-nonce=${ nonce }`;
}
// Add the refreshed nonce if available.
if ( refreshedNonce ) {
formData += `&edd-process-checkout-nonce=${ refreshedNonce }`;
}
return apiRequest( 'edds_complete_payment', {
form_data: formData,
intent,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
}
/**
* Listen for initial EDD core validation.
*
* @param {Object} event Event.
* @param {Object} xhr AJAX request.
* @param {Object} options Request options.
*/
function watchInitialValidation( event, xhr, options ) {
if ( ! options || ! options.data || ! xhr ) {
return;
}
if (
options.data.includes( 'action=edd_process_checkout' ) &&
options.data.includes( 'edd-gateway=stripe' ) &&
( xhr.responseText && 'success' === xhr.responseText.trim() )
) {
return onSubmitDelay();
}
};
/**
* EDD core listens to a a `click` event on the Checkout form submit button.
*
* This submit event handler captures true submissions and triggers a `click`
* event so EDD core can take over as normoal.
*
* @param {Object} event submit Event.
*/
function onSubmit( event ) {
// Ensure we are dealing with the Stripe gateway.
if ( ! (
// Stripe is selected gateway and total is larger than 0.
$( 'input[name="edd-gateway"]' ).val() === 'stripe' &&
$( '.edd_cart_total .edd_cart_amount' ).data( 'total' ) > 0
) ) {
return;
}
// While this function is tied to the submit event, block submission.
event.preventDefault();
// Simulate a mouse click on the Submit button.
//
// If the form is submitted via the "Enter" key we need to ensure the core
// validation is run.
//
// When that is run and then the form is resubmitted
// the click event won't do anything because the button will be disabled.
$( '#edd_purchase_form #edd_purchase_submit [type=submit]' ).trigger( 'click' );
}
/**
* Enables the Checkout form for further submissions.
*/
function enableForm() {
// Update button text.
document.querySelector( '#edd_purchase_form #edd_purchase_submit [type=submit]' ).value = edd_global_vars.complete_purchase;
// Enable form.
$( '.edd-loading-ajax' ).remove();
$( '.edd_errors' ).remove();
$( '.edd-error' ).hide();
$( '#edd-purchase-button' ).attr( 'disabled', false );
}
/**
* Handles error output for stripe.js promises, or jQuery AJAX promises.
*
* @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/master/assets/js/edd-ajax.js#L390
*
* @param {Object} error Error data.
*/
function handleException( error ) {
let { code, message } = error;
const { elementsOptions: { i18n: { errorMessages } } } = window.edd_stripe_vars;
if ( ! message ) {
message = edd_stripe_vars.generic_error;
}
const localizedMessage = code && errorMessages[code] ? errorMessages[code] : message;
const notice = generateNotice( localizedMessage );
// Hide previous messages.
// @todo These should all be in a container, but that's not how core works.
$( '.edd-stripe-alert' ).remove();
$( edd_global_vars.checkout_error_anchor ).before( notice );
$( document.body ).trigger( 'edd_checkout_error', [ error ] );
if ( window.console && error.responseText ) {
window.console.error( error.responseText );
}
}

View File

@ -0,0 +1,8 @@
export { setup as setupCheckout, createPayment, completePayment } from './checkout';
export { setup as setupProfile } from './profile-editor';
export { setup as setupPaymentHistory } from './payment-receipt';
export { setup as setupBuyNow } from './buy-now';
export {
setupDownload as setupDownloadPRB,
setupCheckout as setupCheckoutPRB,
} from './payment-request';

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import { paymentMethods } from 'frontend/components/payment-methods';
// eslint-enable @wordpress/dependency-group
import { paymentForm } from './payment-form.js';
export function setup() {
if ( ! document.getElementById( 'edds-update-payment-method' ) ) {
return;
}
paymentForm();
paymentMethods();
}

View File

@ -0,0 +1,133 @@
/* global edd_stripe_vars */
/**
* Internal dependencies
*/
// eslint-disable @wordpress/dependency-group
import {
getPaymentMethod,
createPaymentForm as createElementsPaymentForm,
handle as handleIntent,
retrieve as retrieveIntent,
} from 'frontend/stripe-elements';
import { generateNotice, apiRequest } from 'utils';
// eslint-enable @wordpress/dependency-group
/**
* Binds events and sets up "Update Payment Method" form.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
document.getElementById( 'edds-update-payment-method' ).addEventListener( 'submit', onAuthorizePayment );
}
/**
* Setup PaymentMethods.
*
* Moves the active item to the currently authenticating PaymentMethod.
*/
function setPaymentMethod() {
const form = document.getElementById( 'edds-update-payment-method' );
const input = document.getElementById( form.dataset.paymentMethod );
// Select the correct PaymentMethod after load.
if ( input ) {
const changeEvent = document.createEvent( 'Event' );
changeEvent.initEvent( 'change', true, true );
input.checked = true;
input.dispatchEvent( changeEvent );
}
}
/**
* Authorize a PaymentIntent.
*
* @param {Event} e submtit event.
*/
async function onAuthorizePayment( e ) {
e.preventDefault();
const form = document.getElementById( 'edds-update-payment-method' );
disableForm();
try {
const paymentMethod = await getPaymentMethod( form, window.eddStripe.cardElement );
// Handle PaymentIntent.
const intent = await retrieveIntent( form.dataset.paymentIntent, 'payment_method' );
const handledIntent = await handleIntent( intent, {
payment_method: paymentMethod.id,
} );
// Attempt to transition payment status and redirect.
const authorization = await completeAuthorization( handledIntent.id );
if ( authorization.payment ) {
window.location.reload();
} else {
throw authorization;
}
} catch ( error ) {
handleException( error );
enableForm();
}
}
/**
* Complete a Payment after the Intent has been authorized.
*
* @param {string} intentId Intent ID.
* @return {Promise} jQuery Promise.
*/
export function completeAuthorization( intentId ) {
return apiRequest( 'edds_complete_payment_authorization', {
intent_id: intentId,
'edds-complete-payment-authorization': document.getElementById(
'edds-complete-payment-authorization'
).value
} );
}
/**
* Disables "Add New" form.
*/
function disableForm() {
const submit = document.getElementById( 'edds-update-payment-method-submit' );
submit.value = submit.dataset.loading;
submit.disabled = true;
}
/**
* Enables "Add New" form.
*/
function enableForm() {
const submit = document.getElementById( 'edds-update-payment-method-submit' );
submit.value = submit.dataset.submit;
submit.disabled = false;
}
/**
* Handles a notice (success or error) for authorizing a card.
*
* @param {Object} error Error with message to output.
*/
export function handleException( error ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
'error'
);
const container = document.getElementById( 'edds-update-payment-method-errors' );
container.innerHTML = '';
container.appendChild( notice );
}

View File

@ -0,0 +1,458 @@
/* global edd_scripts, jQuery */
/**
* Internal dependencies
*/
import { parseDataset } from './';
import { apiRequest, forEach, outputNotice, clearNotice } from 'utils';
import { handle as handleIntent } from 'frontend/stripe-elements';
import { createPayment, completePayment } from 'frontend/payment-forms';
let IS_PRB_GATEWAY;
/**
* Disables the "Express Checkout" payment gateway.
* Switches to the next in the list.
*/
function hideAndSwitchGateways() {
IS_PRB_GATEWAY = false;
const gatewayRadioEl = document.getElementById( 'edd-gateway-option-stripe-prb' );
if ( ! gatewayRadioEl ) {
return;
}
// Remove radio option.
gatewayRadioEl.remove();
// Recount available gateways and hide selector if needed.
const gateways = document.querySelectorAll( '.edd-gateway-option' );
const nextGateway = gateways[0];
const nextGatewayInput = nextGateway.querySelector( 'input' );
// Toggle radio.
nextGatewayInput.checked = true;
nextGateway.classList.add( 'edd-gateway-option-selected' );
// Load gateway.
edd_load_gateway( nextGatewayInput.value );
// Hide wrapper.
if ( 1 === gateways.length ) {
document.getElementById( 'edd_payment_mode_select_wrap' ).remove();
}
}
/**
* Handles the click event on the Payment Request Button.
*
* @param {Event} event Click event.
*/
function onClick( event ) {
const errorContainer = document.getElementById( 'edds-prb-error-wrap' );
const {
checkout_agree_to_terms,
checkout_agree_to_privacy,
} = edd_stripe_vars;
const termsEl = document.getElementById( 'edd_agree_to_terms' );
if ( termsEl ) {
if ( false === termsEl.checked ) {
event.preventDefault();
outputNotice( {
errorMessage: checkout_agree_to_terms,
errorContainer,
} );
} else {
clearNotice( errorContainer );
}
}
const privacyEl = document.getElementById( 'edd-agree-to-privacy-policy' );
if ( privacyEl && false === privacyEl.checked ) {
if ( false === privacyEl.checked ) {
event.preventDefault();
outputNotice( {
errorMessage: checkout_agree_to_privacy,
errorContainer,
} );
} else {
clearNotice( errorContainer );
}
}
}
/**
* Handles changes to the purchase link form by updating the Payment Request object.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
*/
async function onChange( paymentRequest, checkoutForm ) {
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options' );
// Update the Payment Request with server-side data.
paymentRequest.update( {
displayItems,
...paymentRequestData,
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: document.getElementById( 'edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
}
/**
* Handles Payment Method errors.
*
* @param {Object} event Payment Request event.
* @param {Object} error Error.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function onPaymentMethodError( event, error, checkoutForm ) {
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Remove spinner.
const spinner = checkoutForm.querySelector( '.edds-prb-spinner' );
if ( spinner ) {
spinner.parentNode.removeChild( spinner );
}
// Release loading state.
checkoutForm.classList.remove( 'loading' );
// Add notice.
outputNotice( {
errorMessage: error.message,
errorContainer: document.getElementById( 'edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
/**
* Handles recieving a Payment Method from the Payment Request.
*
* Adds an item to the cart and processes the Checkout as if we are
* in normal Checkout context.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
* @param {Object} event paymentmethod event.
*/
async function onPaymentMethod( paymentRequest, checkoutForm, event ) {
try {
// Retrieve information from the PRB event.
const { paymentMethod, payerEmail, payerName } = event;
// Loading state. Block interaction.
checkoutForm.classList.add( 'loading' );
// Create and append a spinner.
const spinner = document.createElement( 'span' );
[ 'edd-loading-ajax', 'edd-loading', 'edds-prb-spinner' ].forEach(
( className ) => spinner.classList.add( className )
);
checkoutForm.appendChild( spinner );
const data = {
email: payerEmail,
name: payerName,
paymentMethod,
context: 'checkout',
};
const tokenInput = $( '#edd-process-stripe-token' );
// Start the processing.
//
// Shims $_POST data to align with the standard Checkout context.
//
// This calls `_edds_process_purchase_form()` server-side which
// creates and returns a PaymentIntent -- just like the first step
// of a true Checkout.
const {
intent,
intent: {
client_secret: clientSecret,
object: intentType,
},
nonce: refreshedNonce,
} = await apiRequest( 'edds_prb_ajax_process_checkout', {
name: payerName,
paymentMethod,
form_data: $( '#edd_purchase_form' ).serialize(),
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
// Update existing nonce value in DOM form data in case data is retrieved
// again directly from the DOM.
$( '#edd-process-checkout-nonce' ).val( refreshedNonce );
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Confirm the card (SCA, etc).
const confirmFunc = 'setup_intent' === intentType
? 'confirmCardSetup'
: 'confirmCardPayment';
eddStripe[ confirmFunc ](
clientSecret,
{
payment_method: paymentMethod.id
},
{
handleActions: false,
}
)
.then( ( { error } ) => {
// Something went wrong. Alert the Payment Request.
if ( error ) {
throw error;
}
// Confirm again after the Payment Request dialog has been hidden.
// For cards that do not require further checks this will throw a 400
// error (in the Stripe API) and a log console error but not throw
// an actual Exception. This can be ignored.
//
// https://github.com/stripe/stripe-payments-demo/issues/133#issuecomment-632593669
eddStripe[ confirmFunc ]( clientSecret )
.then( async ( { error } ) => {
try {
if ( error ) {
throw error;
}
// Create an EDD Payment.
const { intent: updatedIntent, nonce } = await createPayment( intent );
// Complete the EDD Payment with the updated PaymentIntent.
await completePayment( updatedIntent, nonce );
// Redirect on completion.
window.location.replace( edd_stripe_vars.successPageUri );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, checkoutForm );
}
} );
} )
.catch( ( error ) => {
onPaymentMethodError( event, error, checkoutForm );
} );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, checkoutForm );
}
}
/**
* Determines if a full page reload is needed when applying a discount.
*
* A 100% discount switches to the "manual" gateway, bypassing the Stripe,
* however we are still bound to the Payment Request button and a standard
* Purchase button is not present in the DOM to switch back to.add-new-card
*
* @param {Event} e edd_discount_applied event.
* @param {Object} response Discount application response.
* @param {int} response.total_plain Cart total after discount.
*/
function onApplyDiscount( e, { total_plain: total } ) {
if ( true === IS_PRB_GATEWAY && 0 === total ) {
window.location.reload();
}
}
/**
* Binds purchase link form events.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} checkoutForm Checkout form.
*/
function bindEvents( paymentRequest, checkoutForm ) {
const $body = jQuery( document.body );
// Cart quantities have changed.
$body.on( 'edd_quantity_updated', () => onChange( paymentRequest, checkoutForm ) );
// Discounts have changed.
$body.on( 'edd_discount_applied', () => onChange( paymentRequest, checkoutForm ) );
$body.on( 'edd_discount_removed', () => onChange( paymentRequest, checkoutForm ) );
// Handle a PaymentMethod when available.
paymentRequest.on( 'paymentmethod', ( event ) => {
onPaymentMethod( paymentRequest, checkoutForm, event );
} );
// Handle 100% discounts that require a full gateway refresh.
$body.on( 'edd_discount_applied', onApplyDiscount );
}
/**
* Mounts Payment Request buttons (if possible).
*
* @param {HTMLElement} element Payment Request button mount wrapper.
*/
function mount( element ) {
const { eddStripe } = window;
const checkoutForm = document.getElementById( 'edd_checkout_form_wrap' );
try {
// Gather initial data.
const { 'display-items': displayItems, ...data } = parseDataset( element.dataset );
// Create a Payment Request object.
const paymentRequest = eddStripe.paymentRequest( {
// Only requested to prompt full address information collection for Apple Pay.
//
// On-page name fields are used to update the Easy Digital Downloads Customer.
// The Payment Request's Payment Method populate the Customer's Billing Details.
//
// @link https://stripe.com/docs/js/payment_request/create#stripe_payment_request-options-requestPayerName
requestPayerName: true,
displayItems,
...data,
} );
// Create a Payment Request button.
const elements = eddStripe.elements();
const prButton = elements.create( 'paymentRequestButton', {
paymentRequest: paymentRequest,
} );
const wrapper = document.querySelector( `#${ element.id }` );
// Check the availability of the Payment Request API.
paymentRequest.canMakePayment()
// Attempt to mount.
.then( function( result ) {
// Hide wrapper if nothing can be mounted.
if ( ! result ) {
return hideAndSwitchGateways();
}
// Hide wrapper if using Apple Pay but in Test Mode.
// The verification for Connected accounts in Test Mode is not reliable.
if ( true === result.applePay && 'true' === edd_stripe_vars.isTestMode ) {
return hideAndSwitchGateways();
}
// Mount.
wrapper.style.display = 'block';
checkoutForm.classList.add( 'edd-prb--is-active' );
prButton.mount( `#${ element.id } .edds-prb__button` );
// Bind variable pricing/quantity events.
bindEvents( paymentRequest, checkoutForm );
// Handle "Terms of Service" and "Privacy Policy" client validation.
prButton.on( 'click', onClick );
} );
} catch ( error ) {
outputNotice( {
errorMessage: error.message,
errorContainer: document.querySelector( '#edds-prb-checkout' ),
errorContainerReplace: false,
} );
}
};
/**
* Performs an initial check for Payment Request support.
*
* Used when Stripe is not the default gateway (and therefore Express Checkout is not
* loaded by default) to do a "background" check of support while a different initial
* gateway is loaded.
*
* @link https://github.com/easydigitaldownloads/edd-stripe/issues/652
*/
function paymentRequestPrecheck() {
const {
eddStripe: stripe,
edd_stripe_vars: config
} = window;
if ( ! config || ! stripe ) {
return;
}
const { currency, country, checkoutHasPaymentRequest } = config;
if ( 'false' === checkoutHasPaymentRequest ) {
return;
}
stripe.paymentRequest( {
country,
currency: currency.toLowerCase(),
total: {
label: 'Easy Digital Downloads',
amount: 100,
}
} )
.canMakePayment()
.then( ( result ) => {
if ( null === result ) {
hideAndSwitchGateways();
}
const checkoutForm = document.getElementById( 'edd_checkout_form_wrap' );
checkoutForm.classList.add( 'edd-prb--is-active' );
} );
}
/**
* Sets up Payment Request functionality for single purchase links.
*/
export function setup() {
if ( '1' !== edd_scripts.is_checkout ) {
return;
}
/**
* Mounts PRB when the gateway has loaded.
*
* @param {Event} e Gateway loaded event.
* @param {string} gateway Gateway ID.
*/
jQuery( document.body ).on( 'edd_gateway_loaded', ( e, gateway ) => {
if ( 'stripe-prb' !== gateway ) {
IS_PRB_GATEWAY = false;
// Always check for Payment Request support if Stripe is active.
paymentRequestPrecheck();
return;
}
const prbEl = document.querySelector( '.edds-prb.edds-prb--checkout' );
if ( ! prbEl ) {
return;
}
IS_PRB_GATEWAY = true;
mount( prbEl );
} );
}

View File

@ -0,0 +1,416 @@
/* global edd_stripe_vars, jQuery */
/**
* Internal dependencies
*/
import { parseDataset } from './';
import { apiRequest, forEach, outputNotice } from 'utils';
import { handle as handleIntent } from 'frontend/stripe-elements';
import { createPayment, completePayment } from 'frontend/payment-forms';
/**
* Finds the Download ID, Price ID, and quantity values for single Download.
*
* @param {HTMLElement} purchaseLink Purchase link form.
* @return {Object}
*/
function getDownloadData( purchaseLink ) {
let downloadId, priceId = false, quantity = 1;
// Download ID.
const downloadIdEl = purchaseLink.querySelector( '[name="download_id"]' );
downloadId = parseFloat( downloadIdEl.value );
// Price ID.
const priceIdEl = purchaseLink.querySelector(
`.edd_price_option_${downloadId}:checked`
);
if ( priceIdEl ) {
priceId = parseFloat( priceIdEl.value );
}
// Quantity.
const quantityEl = purchaseLink.querySelector(
'input[name="edd_download_quantity"]'
);
if ( quantityEl ) {
quantity = parseFloat( quantityEl.value );
}
return {
downloadId,
priceId,
quantity,
};
}
/**
* Handles changes to the purchase link form by updating the Payment Request object.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
async function onChange( paymentRequest, purchaseLink ) {
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options', {
downloadId,
priceId,
quantity,
} )
// Update the Payment Request with server-side data.
paymentRequest.update( {
displayItems,
...paymentRequestData,
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
}
/**
* Updates the Payment Request amount when the "Custom Amount" input changes.
*
* @param {HTMLElement} addToCartEl Add to cart button.
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
async function onChangeCustomPrice( addToCartEl, paymentRequest, purchaseLink ) {
const { price } = addToCartEl.dataset;
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
try {
// Calculate and gather price information.
const {
'display-items': displayItems,
...paymentRequestData
} = await apiRequest( 'edds_prb_ajax_get_options', {
downloadId,
priceId,
quantity,
} )
// Find the "Custom Amount" price.
const { is_zero_decimal: isZeroDecimal } = edd_stripe_vars;
let amount = parseFloat( price );
if ( 'false' === isZeroDecimal ) {
amount = Math.round( amount * 100 );
}
// Update the Payment Request with the returned server-side data.
// Force update the `amount` in all `displayItems` and `total`.
//
// "Custom Prices" does not support quantities and Payment Requests
// do not support taxes so the same amount applies across the board.
paymentRequest.update( {
displayItems: displayItems.map( ( { label } ) => ( {
label,
amount,
} ) ),
...paymentRequestData,
total: {
label: paymentRequestData.total.label,
amount,
},
} )
} catch ( error ) {
outputNotice( {
errorMessage: '',
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
}
/**
* Handles Payment Method errors.
*
* @param {Object} event Payment Request event.
* @param {Object} error Error.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function onPaymentMethodError( event, error, purchaseLink ) {
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Release loading state.
purchaseLink.classList.remove( 'loading' );
outputNotice( {
errorMessage: error.message,
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
// Item is in the cart at this point, so change the Purchase button to Checkout.
//
// Using jQuery which will preserve the previously set display value in order
// to provide better theme compatibility.
jQuery( 'a.edd-add-to-cart', purchaseLink ).hide();
jQuery( '.edd_download_quantity_wrapper', purchaseLink ).hide();
jQuery( '.edd_price_options', purchaseLink ).hide();
jQuery( '.edd_go_to_checkout', purchaseLink )
.show().removeAttr( 'data-edd-loading' );
}
/**
* Handles recieving a Payment Method from the Payment Request.
*
* Adds an item to the cart and processes the Checkout as if we are
* in normal Checkout context.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
* @param {Object} event paymentmethod event.
*/
async function onPaymentMethod( paymentRequest, purchaseLink, event ) {
try {
// Retrieve the latest data (price ID, quantity, etc).
const { downloadId, priceId, quantity } = getDownloadData( purchaseLink );
// Retrieve information from the PRB event.
const { paymentMethod, payerEmail, payerName } = event;
const tokenInput = jQuery( '#edd-process-stripe-token-' + downloadId );
// Start the processing.
//
// Adds the single Download to the cart and then shims $_POST
// data to align with the standard Checkout context.
//
// This calls `_edds_process_purchase_form()` server-side which
// creates and returns a PaymentIntent -- just like the first step
// of a true Checkout.
const {
intent,
intent: {
client_secret: clientSecret,
object: intentType,
}
} = await apiRequest( 'edds_prb_ajax_process_checkout', {
email: payerEmail,
name: payerName,
paymentMethod,
downloadId,
priceId,
quantity,
context: 'download',
post_data: jQuery( purchaseLink ).serialize(),
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
} );
// Complete the Payment Request to hide the payment sheet.
event.complete( 'success' );
// Loading state. Block interaction.
purchaseLink.classList.add( 'loading' );
// Confirm the card (SCA, etc).
const confirmFunc = 'setup_intent' === intentType
? 'confirmCardSetup'
: 'confirmCardPayment';
eddStripe[ confirmFunc ](
clientSecret,
{
payment_method: paymentMethod.id
},
{
handleActions: false,
}
)
.then( ( { error } ) => {
// Something went wrong. Alert the Payment Request.
if ( error ) {
throw error;
}
// Confirm again after the Payment Request dialog has been hidden.
// For cards that do not require further checks this will throw a 400
// error (in the Stripe API) and a log console error but not throw
// an actual Exception. This can be ignored.
//
// https://github.com/stripe/stripe-payments-demo/issues/133#issuecomment-632593669
eddStripe[ confirmFunc ]( clientSecret )
.then( async ( { error } ) => {
try {
if ( error ) {
throw error;
}
// Create an EDD Payment.
const { intent: updatedIntent, nonce } = await createPayment( intent );
// Complete the EDD Payment with the updated PaymentIntent.
await completePayment( updatedIntent, nonce );
// Redirect on completion.
window.location.replace( edd_stripe_vars.successPageUri );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, purchaseLink );
}
} );
} )
.catch( ( error ) => {
onPaymentMethodError( event, error, purchaseLink );
} );
// Something went wrong, output a notice.
} catch ( error ) {
onPaymentMethodError( event, error, purchaseLink );
}
}
/**
* Listens for changes to the "Add to Cart" button.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function observeAddToCartChanges( paymentRequest, purchaseLink ) {
const addToCartEl = purchaseLink.querySelector( '.edd-add-to-cart' );
if ( ! addToCartEl ) {
return;
}
const observer = new MutationObserver( ( mutations ) => {
mutations.forEach( ( { type, attributeName, target } ) => {
if ( type !== 'attributes' ) {
return;
}
// Update the Payment Request if the price has changed.
// Used for "Custom Prices" extension.
if ( 'data-price' === attributeName ) {
onChangeCustomPrice( target, paymentRequest, purchaseLink );
}
} );
} );
observer.observe( addToCartEl, {
attributes: true,
} );
}
/**
* Binds purchase link form events.
*
* @param {PaymentRequest} paymentRequest Payment Request object.
* @param {HTMLElement} purchaseLink Purchase link form.
*/
function bindEvents( paymentRequest, purchaseLink ) {
// Price option change.
const priceOptionsEls = purchaseLink.querySelectorAll( '.edd_price_options input[type="radio"]' );
forEach( priceOptionsEls, ( priceOption ) => {
priceOption.addEventListener( 'change', () => onChange( paymentRequest, purchaseLink ) );
} );
// Quantity change.
const quantityEl = purchaseLink.querySelector( 'input[name="edd_download_quantity"]' );
if ( quantityEl ) {
quantityEl.addEventListener( 'change', () => onChange( paymentRequest, purchaseLink ) );
}
// Changes to "Add to Cart" button.
observeAddToCartChanges( paymentRequest, purchaseLink );
}
/**
* Mounts Payment Request buttons (if possible).
*
* @param {HTMLElement} element Payment Request button mount wrapper.
*/
function mount( element ) {
const { eddStripe } = window;
try {
// Gather initial data.
const { 'display-items': displayItems, ...data } = parseDataset( element.dataset );
// Find the purchase link form.
const purchaseLink = element.closest(
'.edd_download_purchase_form'
);
// Create a Payment Request object.
const paymentRequest = eddStripe.paymentRequest( {
// Requested to prompt full address information collection for Apple Pay.
//
// Collected email address is used to create/update Easy Digital Downloads Customer.
//
// @link https://stripe.com/docs/js/payment_request/create#stripe_payment_request-options-requestPayerName
requestPayerEmail: true,
displayItems,
...data,
} );
// Create a Payment Request button.
const elements = eddStripe.elements();
const prButton = elements.create( 'paymentRequestButton', {
paymentRequest: paymentRequest,
} );
const wrapper = document.querySelector( `#${ element.id }` );
// Check the availability of the Payment Request API.
paymentRequest.canMakePayment()
// Attempt to mount.
.then( function( result ) {
// Hide wrapper if nothing can be mounted.
if ( ! result ) {
return;
}
// Hide wrapper if using Apple Pay but in Test Mode.
// The verification for Connected accounts in Test Mode is not reliable.
if ( true === result.applePay && 'true' === edd_stripe_vars.isTestMode ) {
return;
}
// Mount.
wrapper.style.display = 'block';
purchaseLink.classList.add( 'edd-prb--is-active' );
prButton.mount( `#${ element.id } .edds-prb__button` );
// Bind variable pricing/quantity events.
bindEvents( paymentRequest, purchaseLink );
} );
// Handle a PaymentMethod when available.
paymentRequest.on( 'paymentmethod', ( event ) => {
onPaymentMethod( paymentRequest, purchaseLink, event );
} );
} catch ( error ) {
outputNotice( {
errorMessage: error.message,
errorContainer: purchaseLink,
errorContainerReplace: false,
} );
}
};
/**
* Sets up Payment Request functionality for single purchase links.
*/
export function setup() {
forEach( document.querySelectorAll( '.edds-prb.edds-prb--download' ), mount );
}

View File

@ -0,0 +1,27 @@
/**
* Internal dependencies
*/
export { setup as setupDownload } from './download.js';
export { setup as setupCheckout } from './checkout.js';
/**
* Parses an HTML dataset and decodes JSON values.
*
* @param {Object} dataset HTML data attributes.
* @return {Object}
*/
export function parseDataset( dataset ) {
let data = {};
for ( const [ key, value ] of Object.entries( dataset ) ) {
let parsedValue = value;
try {
parsedValue = JSON.parse( value );
} catch ( e ) {}
data[ key ] = parsedValue;
}
return data;
}

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import { paymentMethodActions } from './payment-method-actions.js';
import { paymentForm } from './payment-form.js';
export function setup() {
if ( ! document.getElementById( 'edd-stripe-manage-cards' ) ) {
return;
}
paymentMethodActions();
paymentForm();
}

View File

@ -0,0 +1,197 @@
/* global edd_stripe_vars, location */
/**
* Internal dependencies.
*/
import {
createPaymentForm as createElementsPaymentForm,
getBillingDetails
} from 'frontend/stripe-elements';
import {
apiRequest,
hasValidInputs,
triggerBrowserValidation,
generateNotice,
forEach
} from 'utils';
/**
* Binds events and sets up "Add New" form.
*/
export function paymentForm() {
// Mount Elements.
createElementsPaymentForm( window.eddStripe.elements() );
// Toggles and submission.
document.querySelector( '.edd-stripe-add-new' ).addEventListener( 'click', onToggleForm );
document.getElementById( 'edd-stripe-add-new-cancel' ).addEventListener( 'click', onToggleForm );
document.getElementById( 'edd-stripe-add-new-card' ).addEventListener( 'submit', onAddPaymentMethod );
// Set "Card Name" field as required by HTML5
document.getElementById( 'card_name' ).required = true;
}
/**
* Handles toggling of "Add New" form button and submission.
*
* @param {Event} e click event.
*/
function onToggleForm( e ) {
e.preventDefault();
const form = document.getElementById( 'edd-stripe-add-new-card' );
const formFields = form.querySelector( '.edd-stripe-add-new-card' );
const isFormVisible = 'block' === formFields.style.display;
const cancelButton = form.querySelector( '#edd-stripe-add-new-cancel' );
// Trigger a `submit` event.
if ( isFormVisible && cancelButton !== e.target ) {
const submitEvent = document.createEvent( 'Event' );
submitEvent.initEvent( 'submit', true, true );
form.dispatchEvent( submitEvent );
// Toggle form.
} else {
formFields.style.display = ! isFormVisible ? 'block' : 'none';
cancelButton.style.display = ! isFormVisible ? 'inline-block' : 'none';
}
}
/**
* Adds a new Source to the Customer.
*
* @param {Event} e submit event.
*/
function onAddPaymentMethod( e ) {
e.preventDefault();
const form = e.target;
if ( ! hasValidInputs( form ) ) {
triggerBrowserValidation( form );
} else {
try {
disableForm();
createPaymentMethod( form )
.then( addPaymentMethod )
.catch( ( error ) => {
handleNotice( error );
enableForm();
} );
} catch ( error ) {
handleNotice( error );
enableForm();
}
}
}
/**
* Add a PaymentMethod.
*
* @param {Object} paymentMethod
*/
export function addPaymentMethod( paymentMethod ) {
var tokenInput = document.getElementById( '#edd-process-stripe-token' );
apiRequest( 'edds_add_payment_method', {
payment_method_id: paymentMethod.id,
nonce: document.getElementById( 'edd-stripe-add-card-nonce' ).value,
timestamp: tokenInput ? tokenInput.dataset.timestamp : '',
token: tokenInput ? tokenInput.dataset.token : '',
} )
/**
* Shows an error when the API request fails.
*
* @param {Object} response API Request response.
*/
.fail( handleNotice )
/**
* Shows a success notice and automatically redirect.
*
* @param {Object} response API Request response.
*/
.done( function( response ) {
handleNotice( response, 'success' );
// Automatically redirect on success.
setTimeout( function() {
location.reload();
}, 1500 );
} );
}
/**
* Creates a PaymentMethod from a card and billing form.
*
* @param {HTMLElement} billingForm Form with billing fields to retrieve data from.
* @return {Object} Stripe PaymentMethod.
*/
function createPaymentMethod( billingForm ) {
return window.eddStripe
// Create a PaymentMethod with stripe.js
.createPaymentMethod(
'card',
window.eddStripe.cardElement,
{
billing_details: getBillingDetails( billingForm ),
}
)
/**
* Handles PaymentMethod creation response.
*
* @param {Object} result PaymentMethod creation result.
*/
.then( function( result ) {
if ( result.error ) {
throw result.error;
}
return result.paymentMethod;
} );
}
/**
* Disables "Add New" form.
*/
function disableForm() {
const submit = document.querySelector( '.edd-stripe-add-new' );
submit.value = submit.dataset.loading;
submit.disabled = true;
}
/**
* Enables "Add New" form.
*/
function enableForm() {
const submit = document.querySelector( '.edd-stripe-add-new' );
submit.value = submit.dataset.submit;
submit.disabled = false;
}
/**
* Handles a notice (success or error) for card actions.
*
* @param {Object} error Error with message to output.
* @param {string} type Notice type.
*/
export function handleNotice( error, type = 'error' ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
type
);
// Hide previous notices.
forEach( document.querySelectorAll( '.edd-stripe-alert' ), function( alert ) {
alert.remove();
} );
// Show new notice.
document.querySelector( '.edd-stripe-add-card-actions' )
.insertBefore( notice, document.querySelector( '.edd-stripe-add-new' ) );
}

View File

@ -0,0 +1,188 @@
/* global edd_stripe_vars, location */
/**
* Internal dependencies
*/
import { apiRequest, generateNotice, fieldValueOrNull, forEach } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Binds events for card actions.
*/
export function paymentMethodActions() {
// Update.
forEach( document.querySelectorAll( '.edd-stripe-update-card' ), function( updateButton ) {
updateButton.addEventListener( 'click', onToggleUpdateForm );
} );
forEach( document.querySelectorAll( '.edd-stripe-cancel-update' ), function( cancelButton ) {
cancelButton.addEventListener( 'click', onToggleUpdateForm );
} );
forEach( document.querySelectorAll( '.card-update-form' ), function( updateButton ) {
updateButton.addEventListener( 'submit', onUpdatePaymentMethod );
} );
// Delete.
forEach( document.querySelectorAll( '.edd-stripe-delete-card' ), function( deleteButton ) {
deleteButton.addEventListener( 'click', onDeletePaymentMethod );
} );
// Set Default.
forEach( document.querySelectorAll( '.edd-stripe-default-card' ), function( setDefaultButton ) {
setDefaultButton.addEventListener( 'click', onSetDefaultPaymentMethod );
} );
}
/**
* Handle a generic Payment Method action (set default, update, delete).
*
* @param {string} action Payment action.
* @param {string} paymentMethodId PaymentMethod ID.
* @param {null|Object} data Additional AJAX data.
* @return {Promise} jQuery Promise.
*/
function paymentMethodAction( action, paymentMethodId, data = {} ) {
var tokenInput = document.getElementById( 'edd-process-stripe-token-' + paymentMethodId );
data.timestamp = tokenInput ? tokenInput.dataset.timestamp : '';
data.token = tokenInput ? tokenInput.dataset.token : '';
return apiRequest( action, {
payment_method: paymentMethodId,
nonce: document.getElementById( 'card_update_nonce_' + paymentMethodId ).value,
...data,
} )
/**
* Shows an error when the API request fails.
*
* @param {Object} response API Request response.
*/
.fail( function( response ) {
handleNotice( paymentMethodId, response );
} )
/**
* Shows a success notice and automatically redirect.
*
* @param {Object} response API Request response.
*/
.done( function( response ) {
handleNotice( paymentMethodId, response, 'success' );
// Automatically redirect on success.
setTimeout( function() {
location.reload();
}, 1500 );
} );
}
/**
*
* @param {Event} e
*/
function onToggleUpdateForm( e ) {
e.preventDefault();
const source = e.target.dataset.source;
const form = document.getElementById( source + '-update-form' );
const cardActionsEl = document.getElementById( source + '-card-actions' );
const isFormVisible = 'block' === form.style.display;
form.style.display = ! isFormVisible ? 'block' : 'none';
cardActionsEl.style.display = ! isFormVisible ? 'none' : 'block';
}
/**
*
* @param {Event} e
*/
function onUpdatePaymentMethod( e ) {
e.preventDefault();
const form = e.target;
const data = {};
// Gather form data.
const updateFields = [
'address_city',
'address_country',
'address_line1',
'address_line2',
'address_zip',
'address_state',
'exp_month',
'exp_year',
];
updateFields.forEach( function( fieldName ) {
const field = form.querySelector( '[name="' + fieldName + '"]' );
data[ fieldName ] = fieldValueOrNull( field );
} );
const submitButton = form.querySelector( 'input[type="submit"]' );
submitButton.disabled = true;
submitButton.value = submitButton.dataset.loading;
paymentMethodAction( 'edds_update_payment_method', e.target.dataset.source, data )
.fail( function( response ) {
submitButton.disabled = false;
submitButton.value = submitButton.dataset.submit;
} );
}
/**
*
* @param {Event} e
*/
function onDeletePaymentMethod( e ) {
e.preventDefault();
const loading = '<span class="edd-loading-ajax edd-loading"></span>';
const linkText = e.target.innerText;
e.target.innerHTML = loading;
paymentMethodAction( 'edds_delete_payment_method', e.target.dataset.source )
.fail( function( response ) {
e.target.innerText = linkText;
} );
}
/**
*
* @param {Event} e
*/
function onSetDefaultPaymentMethod( e ) {
e.preventDefault();
const loading = '<span class="edd-loading-ajax edd-loading"></span>';
const linkText = e.target.innerText;
e.target.innerHTML = loading;
paymentMethodAction( 'edds_set_payment_method_default', e.target.dataset.source )
.fail( function( response ) {
e.target.innerText = linkText;
} );
}
/**
* Handles a notice (success or error) for card actions.
*
* @param {string} paymentMethodId
* @param {Object} error Error with message to output.
* @param {string} type Notice type.
*/
export function handleNotice( paymentMethodId, error, type = 'error' ) {
// Create the new notice.
const notice = generateNotice(
( error && error.message ) ? error.message : edd_stripe_vars.generic_error,
type
);
// Hide previous notices.
forEach( document.querySelectorAll( '.edd-stripe-alert' ), function( alert ) {
alert.remove();
} );
const item = document.getElementById( paymentMethodId + '_card_item' );
// Show new notice.
item.insertBefore( notice, item.querySelector( '.card-details' ) );
}

View File

@ -0,0 +1,312 @@
/* global $, edd_stripe_vars */
/**
* Internal dependencies.
*/
import {
generateNotice,
fieldValueOrNull,
forEach
} from 'utils'; // eslint-disable-line @wordpress/dependency-group
// Intents.
export * from './intents.js';
const DEFAULT_ELEMENTS = {
'card': '#edd-stripe-card-element',
}
const DEFAULT_SPLIT_ELEMENTS = {
'cardNumber': '#edd-stripe-card-element',
'cardExpiry': '#edd-stripe-card-exp-element',
'cardCvc': '#edd-stripe-card-cvc-element',
}
let ELEMENTS_OPTIONS = { ...edd_stripe_vars.elementsOptions };
/**
* Mounts Elements based on payment form configuration.
*
* Assigns a `cardElement` object to the global `eddStripe` object
* that can be used to collect card data for tokenization.
*
* Integrations (such as Recurring) should pass a configuration of Element
* types and specific HTML IDs to mount based on settings and form markup
* to avoid attempting to mount to the same `HTMLElement`.
*
* @since 2.8.0
*
* @param {Object} elementsInstance Stripe Elements instance.
* @return {Element} The last Stripe Element to be mounted.
*/
export function createPaymentForm( elementsInstance, elements ) {
let mountedEl;
if ( ! elements ) {
elements = ( 'true' === edd_stripe_vars.elementsSplitFields )
? DEFAULT_SPLIT_ELEMENTS
: DEFAULT_ELEMENTS;
}
forEach( elements, ( selector, element ) => {
mountedEl = createAndMountElement( elementsInstance, selector, element );
} );
// Make at least one Element available globally.
window.eddStripe.cardElement = mountedEl;
return mountedEl;
}
/**
* Generates and returns an object of styles that can be used to change the appearance
* of the Stripe Elements iFrame based on existing form styles.
*
* Styles that can be applied to the current DOM are injected to the page via
* a <style> element.
*
* @link https://stripe.com/docs/stripe-js/reference#the-elements-object
*
* @since 2.8.0
*
* @return {Object}
*/
function generateElementStyles() {
// Try to mimick existing input styles.
const cardNameEl = document.querySelector( '.card-name.edd-input' );
if ( ! cardNameEl ) {
return null;
}
const inputStyles = window.getComputedStyle( cardNameEl );
// Inject inline CSS instead of applying to the Element so it can be overwritten.
if ( ! document.getElementById( 'edds-stripe-element-styles' ) ) {
const styleTag = document.createElement( 'style' );
styleTag.innerHTML = `
.edd-stripe-card-element.StripeElement,
.edd-stripe-card-exp-element.StripeElement,
.edd-stripe-card-cvc-element.StripeElement {
background-color: ${ inputStyles.getPropertyValue( 'background-color' ) };
${
[ 'top', 'right', 'bottom', 'left' ]
.map( ( dir ) => (
`border-${ dir }-color: ${ inputStyles.getPropertyValue( `border-${ dir }-color` ) };
border-${ dir }-width: ${ inputStyles.getPropertyValue( `border-${ dir }-width` ) };
border-${ dir }-style: ${ inputStyles.getPropertyValue( `border-${ dir }-style` ) };
padding-${ dir }: ${ inputStyles.getPropertyValue( `padding-${ dir }` ) };`
) )
.join( '' )
}
${
[ 'top-right', 'bottom-right', 'bottom-left', 'top-left' ]
.map( ( dir ) => (
`border-${ dir }-radius: ${ inputStyles.getPropertyValue( 'border-top-right-radius' ) };`
) )
.join( '' )
}
}`
// Remove whitespace.
.replace( /\s/g, '' );
styleTag.id = 'edds-stripe-element-styles';
document.body.appendChild( styleTag );
}
return {
base: {
color: inputStyles.getPropertyValue( 'color' ),
fontFamily: inputStyles.getPropertyValue( 'font-family' ),
fontSize: inputStyles.getPropertyValue( 'font-size' ),
fontWeight: inputStyles.getPropertyValue( 'font-weight' ),
fontSmoothing: inputStyles.getPropertyValue( '-webkit-font-smoothing' ),
},
};
}
/**
* Mounts an Elements Card to the DOM and adds event listeners to submission.
*
* @link https://stripe.com/docs/stripe-js/reference#the-elements-object
*
* @since 2.8.0
*
* @param {Elements} elementsInstance Stripe Elements instance.
* @param {string} selector Selector to mount Element on.
* @return {Element|undefined} Stripe Element.
*/
function createAndMountElement( elementsInstance, selector, element ) {
const el = document.querySelector( selector );
if ( ! el ) {
return undefined;
}
ELEMENTS_OPTIONS.style = jQuery.extend(
true,
{},
generateElementStyles(),
ELEMENTS_OPTIONS.style
);
// Remove hidePostalCode if not using a combined `card` Element.
if ( 'cardNumber' === element && ELEMENTS_OPTIONS.hasOwnProperty( 'hidePostalCode' ) ) {
delete ELEMENTS_OPTIONS.hidePostalCode;
}
// Remove unused parameter from options.
delete ELEMENTS_OPTIONS.i18n;
const stripeElement = elementsInstance
.create( element, ELEMENTS_OPTIONS );
stripeElement
.addEventListener( 'change', ( event ) => {
handleElementError( event, el );
handleCardBrandIcon( event );
} )
.mount( el );
return stripeElement;
}
/**
* Mounts an Elements Card to the DOM and adds event listeners to submission.
*
* @since 2.7.0
* @since 2.8.0 Deprecated
*
* @deprecated Use createPaymentForm() to mount specific elements.
*
* @param {Elements} elementsInstance Stripe Elements instance.
* @param {string} toMount Selector to mount Element on.
* @return {Element} Stripe Element.
*/
export function mountCardElement( elementsInstance, toMount = '#edd-stripe-card-element' ) {
const mountedEl = createPaymentForm( elementsInstance, {
'card': toMount,
} );
// Hide split card details fields because any integration that is using this
// directly has not properly implemented split fields.
const splitFields = document.getElementById( 'edd-card-details-wrap' );
if ( splitFields ) {
splitFields.style.display = 'none';
}
return mountedEl;
}
/**
* Handles error output for Elements Card.
*
* @param {Event} event Change event on the Card Element.
* @param {HTMLElement} el HTMLElement the Stripe Element is being mounted on.
*/
function handleElementError( event, el ) {
const newCardContainer = el.closest( '.edd-stripe-new-card' );
const errorsContainer = newCardContainer.querySelector( '#edd-stripe-card-errors' );
// Only show one error at once.
errorsContainer.innerHTML = '';
if ( event.error ) {
const { code, message } = event.error;
const { elementsOptions: { i18n: { errorMessages } } } = window.edd_stripe_vars;
const localizedMessage = errorMessages[ code ] ? errorMessages[ code ] : message;
errorsContainer.appendChild( generateNotice( localizedMessage ) );
}
}
/**
* Updates card brand icon if using a split form.
*
* @since 2.8.0
*
* @param {Event} event Change event on the Card Element.
*/
function handleCardBrandIcon( event ) {
const {
brand,
elementType,
} = event;
if ( 'cardNumber' !== event.elementType ) {
return;
}
const cardTypeEl = document.querySelector( '.card-type' );
if ( 'unknown' === brand ) {
cardTypeEl.className = 'card-type';
} else {
cardTypeEl.classList.add( brand );
}
}
/**
* Retrieves (or creates) a PaymentMethod.
*
* @param {HTMLElement} billingDetailsForm Form to find data from.
* @return {Object} PaymentMethod ID and if it previously existed.
*/
export function getPaymentMethod( billingDetailsForm, cardElement ) {
const selectedPaymentMethod = $( 'input[name="edd_stripe_existing_card"]:checked' );
// An existing PaymentMethod is selected.
if ( selectedPaymentMethod.length > 0 && 'new' !== selectedPaymentMethod.val() ) {
return Promise.resolve( {
id: selectedPaymentMethod.val(),
exists: true,
} );
}
// Create a PaymentMethod using the Element data.
return window.eddStripe
.createPaymentMethod(
'card',
cardElement,
{
billing_details: getBillingDetails( billingDetailsForm ),
}
)
.then( function( result ) {
if ( result.error ) {
throw result.error;
}
return {
id: result.paymentMethod.id,
exists: false,
};
} );
}
/**
* Retrieves billing details from the Billing Details sections of a form.
*
* @param {HTMLElement} form Form to find data from.
* @return {Object} Billing details
*/
export function getBillingDetails( form ) {
return {
// @todo add Phone
// @todo add Email
name: fieldValueOrNull( form.querySelector( '.card-name' ) ),
address: {
line1: fieldValueOrNull( form.querySelector( '.card-address' ) ),
line2: fieldValueOrNull( form.querySelector( '.card-address-2' ) ),
city: fieldValueOrNull( form.querySelector( '.card-city' ) ),
state: fieldValueOrNull( form.querySelector( '.card_state' ) ),
postal_code: fieldValueOrNull( form.querySelector( '.card-zip' ) ),
country: fieldValueOrNull( form.querySelector( '#billing_country' ) ),
},
};
}

View File

@ -0,0 +1,179 @@
/* global jQuery */
/**
* Internal dependencies
*/
import { apiRequest } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* Retrieve a PaymentIntent.
*
* @param {string} intentId Intent ID.
* @param {string} intentType Intent type. payment_intent or setup_intent.
* @return {Promise} jQuery Promise.
*/
export function retrieve( intentId, intentType = 'payment_intent' ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_get_intent', {
intent_id: intentId,
intent_type: intentType,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
} )
// Returns just the PaymentIntent object.
.then( function( response ) {
return response.intent;
} );
}
/**
* Confirm a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @return {Promise} jQuery Promise.
*/
export function confirm( intent ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_confirm_intent', {
intent_id: intent.id,
intent_type: intent.object,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Capture a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data Extra data to pass to the intent action.
* @param {string} refreshedNonce A refreshed nonce that might be needed if the
* user logged in.
* @return {Promise} jQuery Promise.
*/
export function capture( intent, data, refreshedNonce ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' );
if ( 'requires_capture' !== intent.status ) {
return Promise.resolve( intent );
}
let formData = form.serialize(),
tokenInput = $( '#edd-process-stripe-token' );
// Add the refreshed nonce if available.
if ( refreshedNonce ) {
formData += `&edd-process-checkout-nonce=${ refreshedNonce }`;
}
return apiRequest( 'edds_capture_intent', {
intent_id: intent.id,
intent_type: intent.object,
form_data: formData,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
...data,
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Update a PaymentIntent.
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data PaymentIntent data to update.
* @return {Promise} jQuery Promise.
*/
export function update( intent, data ) {
const form = $( window.eddStripe.cardElement._parent ).closest( 'form' ),
tokenInput = $( '#edd-process-stripe-token' );
return apiRequest( 'edds_update_intent', {
intent_id: intent.id,
intent_type: intent.object,
timestamp: tokenInput.length ? tokenInput.data( 'timestamp' ) : '',
token: tokenInput.length ? tokenInput.data( 'token' ) : '',
form_data: form.serialize(),
...data,
} )
// Returns just the PaymentIntent object for easier reprocessing.
.then( function( response ) {
return response.intent;
} );
}
/**
* Determines if the PaymentIntent requires further action.
*
* @link https://stripe.com/docs/stripe-js/reference
*
* @param {Object} intent Stripe PaymentIntent or SetupIntent.
* @param {Object} data Extra data to pass to the intent action.
*/
export async function handle( intent, data ) {
// requires_confirmation
if ( 'requires_confirmation' === intent.status ) {
// Attempt to capture.
const confirmedIntent = await confirm( intent );
// Run through again.
return await handle( confirmedIntent );
}
// requires_payment_method
// @link https://stripe.com/docs/payments/intents#intent-statuses
if (
'requires_payment_method' === intent.status ||
'requires_source' === intent.status
) {
// Attempt to update.
const updatedIntent = await update( intent, data );
// Run through again.
return await handle( updatedIntent, data );
}
// requires_action
// @link https://stripe.com/docs/payments/intents#intent-statuses
if (
( 'requires_action' === intent.status && 'use_stripe_sdk' === intent.next_action.type ) ||
( 'requires_source_action' === intent.status && 'use_stripe_sdk' === intent.next_action.type )
) {
let cardHandler = 'setup_intent' === intent.object ? 'handleCardSetup' : 'handleCardAction';
if ( 'automatic' === intent.confirmation_method ) {
cardHandler = 'handleCardPayment';
}
return window.eddStripe[ cardHandler ]( intent.client_secret )
.then( async ( result ) => {
if ( result.error ) {
throw result.error;
}
const {
setupIntent,
paymentIntent,
} = result;
// Run through again.
return await handle( setupIntent || paymentIntent );
} );
}
// Nothing done, return Intent.
return intent;
}

View File

@ -0,0 +1,51 @@
/* global $, edd_scripts, ajaxurl */
/**
* Sends an API request to admin-ajax.php
*
* @link https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-util.js#L49
*
* @param {string} action AJAX action to send to admin-ajax.php
* @param {Object} data Additional data to send to the action.
* @return {Promise} jQuery Promise.
*/
export function apiRequest( action, data ) {
const options = {
type: 'POST',
dataType: 'json',
xhrFields: {
withCredentials: true,
},
url: ( window.edd_scripts && window.edd_scripts.ajaxurl ) || window.ajaxurl,
data: {
action,
...data,
},
};
const deferred = $.Deferred( function( deferred ) {
// Use with PHP's wp_send_json_success() and wp_send_json_error()
deferred.jqXHR = $.ajax( options ).done( function( response ) {
// Treat a response of 1 or 'success' as successful for backward compatibility with existing handlers.
if ( response === '1' || response === 1 ) {
response = { success: true };
}
if ( typeof response === 'object' && typeof response.success !== undefined ) {
deferred[ response.success ? 'resolveWith' : 'rejectWith' ]( this, [ response.data ] );
} else {
deferred.rejectWith( this, [ response ] );
}
} ).fail( function() {
deferred.rejectWith( this, arguments );
} );
} );
const promise = deferred.promise();
promise.abort = function() {
deferred.jqXHR.abort();
return this;
};
return promise;
}

View File

@ -0,0 +1,43 @@
/**
* Internal dependencies.
*/
import { forEach } from 'utils'; // eslint-disable-line @wordpress/dependency-group
/**
* forEach implementation that can handle anything.
*/
export { default as forEach } from 'lodash.foreach';
/**
* DOM ready.
*
* Handles multiple callbacks.
*
* @param {Function} Callback function to run.
*/
export function domReady() {
forEach( arguments, ( callback ) => {
document.addEventListener( 'DOMContentLoaded', callback );
} );
}
/**
* Retrieves all following siblings of an element.
*
* @param {HTMLElement} el Starting element.
* @return {Array} siblings List of sibling elements.
*/
export function getNextSiblings( el ) {
const siblings = [];
let sibling = el.nextElementSibling;
while ( sibling ) {
if ( sibling.nodeType === 1 ) {
siblings.push( sibling );
}
sibling = sibling.nextElementSibling;
}
return siblings;
}

View File

@ -0,0 +1,58 @@
/**
* Internal dependencies.
*/
/**
* External dependencies
*/
import { forEach } from 'utils';
/**
* Checks is a form passes HTML5 validation.
*
* @param {HTMLElement} form Form to trigger validation on.
* @return {Bool} If the form has valid inputs.
*/
export function hasValidInputs( form ) {
let plainInputsValid = true;
forEach( form.querySelectorAll( 'input' ), function( input ) {
if ( input.checkValidity && ! input.checkValidity() ) {
plainInputsValid = false;
}
} );
return plainInputsValid;
}
/**
* Triggers HTML5 browser validation.
*
* @param {HTMLElement} form Form to trigger validation on.
*/
export function triggerBrowserValidation( form ) {
const submit = document.createElement( 'input' );
submit.type = 'submit';
submit.style.display = 'none';
form.appendChild( submit );
submit.click();
submit.remove();
}
/**
* Returns an input's value, or null.
*
* @param {HTMLElement} field Field to retrieve value from.
* @return {null|string} Value if the field has a value.
*/
export function fieldValueOrNull( field ) {
if ( ! field ) {
return null;
}
if ( '' === field.value ) {
return null;
}
return field.value;
}

View File

@ -0,0 +1,9 @@
import './polyfill-includes.js';
import './polyfill-closest.js';
import './polyfill-object-entries.js';
import './polyfill-remove.js';
export * from './api-request.js';
export * from './dom.js';
export * from './notice.js';
export * from './form.js';

View File

@ -0,0 +1,61 @@
/* global $, edd_stripe_vars */
/**
* Generates a notice element.
*
* @param {string} message The notice text.
* @param {string} type The type of notice. error or success. Default error.
* @return {Element} HTML element containing errors.
*/
export function generateNotice( message, type = 'error' ) {
const notice = document.createElement( 'p' );
notice.classList.add( 'edd-alert' );
notice.classList.add( 'edd-stripe-alert' );
notice.style.clear = 'both';
if ( 'error' === type ) {
notice.classList.add( 'edd-alert-error' );
} else {
notice.classList.add( 'edd-alert-success' );
}
notice.innerHTML = message || edd_stripe_vars.generic_error;
return notice;
}
/**
* Outputs a notice.
*
*
* @param {object} args Output arguments.
* @param {string} args.errorType The type of notice. error or success
* @param {string} args.errorMessasge The notice text.
* @param {HTMLElement} args.errorContainer HTML element containing errors.
* @param {bool} args.errorContainerReplace If true Appends the notice before
* the container.
*/
export function outputNotice( {
errorType,
errorMessage,
errorContainer,
errorContainerReplace = true,
} ) {
const $errorContainer = $( errorContainer );
const notice = generateNotice( errorMessage, errorType );
if ( true === errorContainerReplace ) {
$errorContainer.html( notice );
} else {
$errorContainer.before( notice );
}
}
/**
* Clears a notice.
*
* @param {HTMLElement} errorContainer HTML element containing errors.
*/
export function clearNotice( errorContainer ) {
$( errorContainer ).html( '' );
}

View File

@ -0,0 +1,21 @@
/// Polyfill .closest
// @link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
if ( ! Element.prototype.matches ) {
Element.prototype.matches =
Element.prototype.msMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}
if ( ! Element.prototype.closest ) {
Element.prototype.closest = function( s ) {
let el = this;
do {
if ( Element.prototype.matches.call( el, s ) ) return el;
el = el.parentElement || el.parentNode;
} while ( el !== null && el.nodeType === 1 );
return null;
};
}

View File

@ -0,0 +1,17 @@
// Polyfill string.contains
// @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill
if ( ! String.prototype.includes ) {
String.prototype.includes = function( search, start ) {
'use strict';
if ( typeof start !== 'number' ) {
start = 0;
}
if ( start + search.length > this.length ) {
return false;
} else {
return this.indexOf( search, start ) !== -1;
}
};
}

View File

@ -0,0 +1,15 @@
/// Polyfill Object.entries
// @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries#Polyfill
if ( ! Object.entries ) {
Object.entries = function( obj ) {
var ownProps = Object.keys( obj ),
i = ownProps.length,
resArray = new Array( i ); // preallocate the Array
while ( i-- ) {
resArray[ i ] = [ ownProps[ i ], obj[ ownProps[ i ] ] ];
}
return resArray;
};
}

View File

@ -0,0 +1,18 @@
/// Polyfill .remove
// @link https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
( function ( arr ) {
arr.forEach( function( item ) {
if ( item.hasOwnProperty( 'remove' ) ) {
return;
}
Object.defineProperty( item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
this.parentNode.removeChild( this );
}
} );
} );
} )( [ Element.prototype, CharacterData.prototype, DocumentType.prototype ] );