paypal.js
241 lines
| 1 | jQuery(function ($) { |
| 2 | const containerSelector = 'paypal-standard-container'; |
| 3 | let orderReceivedUrl = ''; |
| 4 | let orderId = ''; |
| 5 | let orderKey = ''; |
| 6 | let productPageCartData = {}; |
| 7 | |
| 8 | function renderButtons() { |
| 9 | const container = document.getElementById( containerSelector ); |
| 10 | if ( ! container ) { |
| 11 | return; |
| 12 | } |
| 13 | |
| 14 | // If PayPal is not loaded, don't try to render the buttons. |
| 15 | if ( typeof paypal === 'undefined' ) { |
| 16 | return; |
| 17 | } |
| 18 | |
| 19 | applyStyles(); |
| 20 | |
| 21 | /** |
| 22 | * Manage the cart contents when placing an order from the product page. |
| 23 | * |
| 24 | * @returns {Promise<boolean>} |
| 25 | */ |
| 26 | const manageCartForProductPageOrder = async () => { |
| 27 | // Get product ID from the value of the "add-to-cart" button. |
| 28 | const addToCartBtn = document.querySelector('[name="add-to-cart"]'); |
| 29 | let productId = addToCartBtn ? addToCartBtn.value : null; |
| 30 | const variationIdField = document.querySelector( '[name="variation_id"]' ); |
| 31 | const variationId = variationIdField ? variationIdField.value : null; |
| 32 | |
| 33 | if ( variationId ) { |
| 34 | productId = variationId; |
| 35 | } |
| 36 | |
| 37 | if ( ! productId ) { |
| 38 | return false; |
| 39 | } |
| 40 | |
| 41 | // Get quantity from the value of the "quantity" input field. |
| 42 | const quantityField = document.querySelector( '[name="quantity"]' ); |
| 43 | const quantity = quantityField ? quantityField.value : '1'; |
| 44 | if ( quantity === '' ) { |
| 45 | return false; |
| 46 | } |
| 47 | |
| 48 | // Clearing the cart and re-adding the item causes the current WooCommerce draft order to be lost. |
| 49 | // If the user is re-opening the payment modal and has not changed anything, do nothing; |
| 50 | // we want to resume the existing draft order if the cart has not changed. |
| 51 | if ( orderId && productPageCartData.id === productId && productPageCartData.quantity === quantity ) { |
| 52 | return true; |
| 53 | } |
| 54 | |
| 55 | try { |
| 56 | // Empty the cart before adding the product. |
| 57 | const emptyCartResponse = await window.wp.apiFetch( { |
| 58 | method: 'DELETE', |
| 59 | path: '/wc/store/v1/cart/items', |
| 60 | } ); |
| 61 | |
| 62 | // Expected response is an empty array. |
| 63 | if ( ! emptyCartResponse || emptyCartResponse.length != 0 ) { |
| 64 | throw new Error( 'Failed to empty cart' ); |
| 65 | } |
| 66 | |
| 67 | // Add the product to the cart. |
| 68 | const addToCartResponse = await window.wp.apiFetch( { |
| 69 | method: 'POST', |
| 70 | path: '/wc/store/v1/cart/items', |
| 71 | data: { |
| 72 | id: productId, |
| 73 | quantity, |
| 74 | }, |
| 75 | } ); |
| 76 | |
| 77 | if ( ! addToCartResponse || ! addToCartResponse.key ) { |
| 78 | throw new Error( 'Failed to add product to cart' ); |
| 79 | } |
| 80 | } catch ( error ) { |
| 81 | return false; |
| 82 | } |
| 83 | |
| 84 | // Remember what we added to the cart, so we don't have to repeat the action |
| 85 | // when the user re-opens the payment modal. |
| 86 | productPageCartData = { |
| 87 | id: productId, |
| 88 | quantity, |
| 89 | }; |
| 90 | |
| 91 | return true; |
| 92 | } |
| 93 | |
| 94 | const buttons = paypal.Buttons( { |
| 95 | appSwitchWhenAvailable: true, |
| 96 | async createOrder( data ) { |
| 97 | // If we're inside the product page, we need to manage the cart contents |
| 98 | // ourselves. |
| 99 | if ( paypal_standard.is_product_page ) { |
| 100 | const cartSuccess = await manageCartForProductPageOrder(); |
| 101 | if ( ! cartSuccess ) { |
| 102 | return null; |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | let responseData; |
| 107 | try { |
| 108 | // Create a draft order in WooCommerce. |
| 109 | responseData = await window.wp.apiFetch( { |
| 110 | method: 'GET', |
| 111 | path: '/wc/store/v1/checkout', |
| 112 | headers: { |
| 113 | Nonce: paypal_standard.wc_store_api_nonce, |
| 114 | }, |
| 115 | } ); |
| 116 | |
| 117 | if ( ! responseData.order_id || ! responseData.order_key ) { |
| 118 | // eslint-disable-next-line no-console |
| 119 | console.error( 'Failed to create WooCommerce order', responseData ); |
| 120 | return null; |
| 121 | } |
| 122 | |
| 123 | // Create a PayPal order. |
| 124 | const paypalResponseData = await window.wp.apiFetch( { |
| 125 | method: 'POST', |
| 126 | path: '/wc/v3/paypal-buttons/create-order', |
| 127 | headers: { |
| 128 | Nonce: paypal_standard.create_order_nonce, |
| 129 | }, |
| 130 | data: { |
| 131 | order_id: responseData.order_id, |
| 132 | order_key: responseData.order_key, |
| 133 | payment_source: data.paymentSource || '', |
| 134 | app_switch_request_origin: paypal_standard.app_switch_request_origin, |
| 135 | }, |
| 136 | } ); |
| 137 | |
| 138 | orderId = paypalResponseData.order_id; |
| 139 | orderKey = responseData.order_key; |
| 140 | orderReceivedUrl = paypalResponseData.return_url; |
| 141 | |
| 142 | return paypalResponseData.paypal_order_id; |
| 143 | } catch ( error ) { |
| 144 | // eslint-disable-next-line no-console |
| 145 | console.error( 'Failed to create order', error ); |
| 146 | return null; |
| 147 | } |
| 148 | }, |
| 149 | |
| 150 | onApprove() { |
| 151 | if ( orderReceivedUrl ) { |
| 152 | window.location.href = orderReceivedUrl; |
| 153 | } |
| 154 | }, |
| 155 | |
| 156 | async onCancel( data ) { |
| 157 | if ( ! orderId ) { |
| 158 | // When coming back from App Switch, the order ID may not be available in the |
| 159 | // client-side data. Check the URL for the order ID. |
| 160 | orderId = new URLSearchParams( window.location.search ).get( 'order_id' ); |
| 161 | } |
| 162 | |
| 163 | if ( ! orderId ) { |
| 164 | return; |
| 165 | } |
| 166 | |
| 167 | try { |
| 168 | await window.wp.apiFetch( { |
| 169 | method: 'POST', |
| 170 | path: '/wc/v3/paypal-buttons/cancel-payment', |
| 171 | headers: { |
| 172 | Nonce: paypal_standard.cancel_payment_nonce, |
| 173 | }, |
| 174 | data: { |
| 175 | order_id: orderId, |
| 176 | paypal_order_id: data.orderID, |
| 177 | }, |
| 178 | } ); |
| 179 | |
| 180 | orderReceivedUrl = ''; |
| 181 | } catch ( error ) { |
| 182 | // eslint-disable-next-line no-console |
| 183 | console.error( 'Failed to cancel PayPal payment', error ); |
| 184 | } |
| 185 | }, |
| 186 | |
| 187 | onError: function ( error ) { |
| 188 | const sanitizedErrorMessage = $( '<div>' ).text( error.message || paypal_standard.generic_error_message ).html(); |
| 189 | const messageWrapper = |
| 190 | '<ul class="woocommerce-error" role="alert"><li>' + |
| 191 | sanitizedErrorMessage + |
| 192 | '</li></ul>'; |
| 193 | |
| 194 | const $noticeContainer = $( '.woocommerce-notices-wrapper' ).first(); |
| 195 | |
| 196 | if ( ! $noticeContainer.length ) { |
| 197 | return; |
| 198 | } |
| 199 | |
| 200 | $( |
| 201 | '.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message' |
| 202 | ).remove(); |
| 203 | $noticeContainer.prepend( messageWrapper ); |
| 204 | }, |
| 205 | |
| 206 | }); |
| 207 | |
| 208 | if ( buttons.hasReturned() ) { |
| 209 | // App Switch resume flow. |
| 210 | buttons.resume(); |
| 211 | } |
| 212 | |
| 213 | buttons.render( container ).catch( function ( err ) { |
| 214 | // eslint-disable-next-line no-console |
| 215 | console.error( 'Failed to render PayPal buttons', err ); |
| 216 | }); |
| 217 | |
| 218 | } |
| 219 | |
| 220 | // Align the PayPal buttons to the center of the container on classic checkout page. |
| 221 | function applyStyles() { |
| 222 | const paypalContainer = document.getElementById( containerSelector ); |
| 223 | const containerWidth = paypalContainer.offsetWidth; |
| 224 | |
| 225 | // PayPal buttons have max-width: 750px inside the iframe. |
| 226 | // Calculate the left margin to center a 750px button container. |
| 227 | const leftMargin = Math.max( 0, ( containerWidth - 750 ) / 2 ); |
| 228 | paypalContainer.style.marginLeft = leftMargin + 'px'; |
| 229 | } |
| 230 | |
| 231 | // Re-render when cart is updated and the html is rerendered on the Cart page. |
| 232 | $( document.body ).on( 'updated_cart_totals', function () { |
| 233 | // If the container was replaced, re-render PayPal buttons |
| 234 | const buttonsContainer = document.getElementById( containerSelector ); |
| 235 | if ( buttonsContainer && ! buttonsContainer.querySelector( 'iframe' ) ) { |
| 236 | renderButtons(); |
| 237 | } |
| 238 | } ); |
| 239 | |
| 240 | renderButtons(); |
| 241 | }); |