├── DEVELOPER.md ├── assets ├── css │ ├── admin │ │ └── ppec-upgrade-notice.css │ └── wc-gateway-ppec-frontend.css ├── img │ └── in-context-composite.png └── js │ ├── admin │ └── ppec-upgrade-notice.js │ ├── dist │ ├── fetch-polyfill.min.js │ └── promise-polyfill.min.js │ ├── wc-gateway-ppec-frontend-in-context-checkout.js │ ├── wc-gateway-ppec-generate-cart.js │ ├── wc-gateway-ppec-order-review.js │ ├── wc-gateway-ppec-settings.js │ └── wc-gateway-ppec-smart-payment-buttons.js ├── changelog.txt ├── includes ├── abstracts │ ├── abstract-wc-gateway-ppec-client-credential.php │ ├── abstract-wc-gateway-ppec-paypal-request-handler.php │ └── abstract-wc-gateway-ppec.php ├── class-wc-gateway-ppec-address.php ├── class-wc-gateway-ppec-admin-handler.php ├── class-wc-gateway-ppec-api-error.php ├── class-wc-gateway-ppec-cart-handler.php ├── class-wc-gateway-ppec-checkout-details.php ├── class-wc-gateway-ppec-checkout-handler.php ├── class-wc-gateway-ppec-client-credential-certificate.php ├── class-wc-gateway-ppec-client-credential-signature.php ├── class-wc-gateway-ppec-client.php ├── class-wc-gateway-ppec-gateway-loader.php ├── class-wc-gateway-ppec-ipn-handler.php ├── class-wc-gateway-ppec-ips-handler.php ├── class-wc-gateway-ppec-payment-details.php ├── class-wc-gateway-ppec-plugin.php ├── class-wc-gateway-ppec-privacy.php ├── class-wc-gateway-ppec-refund.php ├── class-wc-gateway-ppec-session-data.php ├── class-wc-gateway-ppec-settings.php ├── class-wc-gateway-ppec-with-paypal-addons.php ├── class-wc-gateway-ppec-with-paypal-credit.php ├── class-wc-gateway-ppec-with-paypal.php ├── class-wc-gateway-ppec-with-spb-addons.php ├── class-wc-gateway-ppec-with-spb.php ├── exceptions │ ├── class-wc-gateway-ppec-api-exception.php │ └── class-wc-gateway-ppec-missing-session-exception.php ├── functions.php ├── pem │ └── bundle.pem └── settings │ └── settings-ppec.php ├── languages └── woocommerce-gateway-paypal-express-checkout.pot ├── readme.txt ├── templates └── paypal-payments-upgrade-notice.php └── woocommerce-gateway-paypal-express-checkout.php /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # DEVELOPER.md 2 | 3 | ## Sandbox URLs 4 | * Developer: https://developer.paypal.com/ 5 | 6 | ## Setup 7 | 1. Make sure your store has set the country to US and is using US dollars (for easier testing). 8 | 2. Sign up for a PayPal account. 9 | 3. Go to "My Apps & Credentials" and create a REST API apps. 10 | 4. Use the information from step 4 information to fill in the settings: Sandbox API Username, Password, Signature. 11 | 5. Remember that your buyer email and facilitator email are automatically generated and are available in Sandbox > Accounts. Likewise, the store email must match the facilitator email. 12 | -------------------------------------------------------------------------------- /assets/css/admin/ppec-upgrade-notice.css: -------------------------------------------------------------------------------- 1 | .plugins tr[data-slug=woocommerce-gateway-paypal-express-checkout].hide-border th, .plugins tr[data-slug=woocommerce-gateway-paypal-express-checkout].hide-border td { 2 | box-shadow: none !important; 3 | } 4 | 5 | #ppec-migrate-notice .notice { 6 | padding: 1px 0 20px 0; 7 | } 8 | #ppec-migrate-notice .ppec-notice-section { 9 | padding: 0 20px; 10 | } 11 | #ppec-migrate-notice .ppec-notice-title p { 12 | padding: 10px 0 0 0; 13 | font-size: 110%; 14 | } 15 | #ppec-migrate-notice .ppec-notice-title p:before { 16 | vertical-align: middle; 17 | } 18 | #ppec-migrate-notice .ppec-notice-content p:first-of-type { 19 | margin-top: 20px; 20 | } 21 | #ppec-migrate-notice .ppec-notice-content p:before { 22 | display: none; 23 | } 24 | #ppec-migrate-notice .ppec-notice-content ul { 25 | list-style-type: disc; 26 | margin-left: 30px; 27 | } 28 | #ppec-migrate-notice .ppec-notice-buttons a { 29 | text-decoration: none; 30 | line-height: 34px; 31 | margin-right: 16px; 32 | margin-top: 16px; 33 | } 34 | #ppec-migrate-notice .ppec-notice-buttons a:last-of-type { 35 | margin-right: 0; 36 | } 37 | #ppec-migrate-notice .ppec-notice-buttons a:before { 38 | vertical-align: middle; 39 | margin: -2.5px 5px 0 0; 40 | } 41 | #ppec-migrate-notice .ppec-notice-buttons a.dismiss { 42 | float: right; 43 | } 44 | #ppec-migrate-notice .ppec-notice-buttons a.updating-message:before { 45 | -webkit-animation: rotation 2s infinite linear; 46 | animation: rotation 2s infinite linear; 47 | } 48 | -------------------------------------------------------------------------------- /assets/css/wc-gateway-ppec-frontend.css: -------------------------------------------------------------------------------- 1 | .wcppec-checkout-buttons { 2 | text-align: center; 3 | margin: 1em 0; 4 | overflow: hidden; 5 | } 6 | .wcppec-checkout-buttons .woocommerce-error { 7 | text-align: left; 8 | } 9 | .wcppec-checkout-buttons__separator { 10 | display: block; 11 | margin: 0 0 1em; 12 | } 13 | .wcppec-checkout-buttons__button { 14 | display: inline-block; 15 | text-decoration: none !important; 16 | border: 0 !important; 17 | padding-top: 1em; 18 | } 19 | .wcppec-checkout-buttons__button img { 20 | margin: 0 auto; 21 | } 22 | .paypal-button-widget .paypal-button, 23 | .paypal-button-widget .paypal-button:hover { 24 | background: transparent; 25 | box-shadow: none; 26 | border: none; 27 | } 28 | .wcppec-cart-widget-button { 29 | display: inline-block; 30 | text-decoration: none !important; 31 | border: 0 !important; 32 | } 33 | .site-header .widget_shopping_cart p.buttons.wcppec-cart-widget-spb { 34 | padding: 0 1em 1em; 35 | } 36 | .site-header .widget_shopping_cart .woocommerce-mini-cart__empty-message + p.buttons.wcppec-cart-widget-spb { 37 | display: none; 38 | } 39 | 40 | .payment_method_ppec_paypal img { 41 | max-height: 68px !important; 42 | border-radius: 0; 43 | } 44 | 45 | .wc-gateway-ppec-cancel { 46 | display: block; 47 | text-align: center; 48 | padding: 10px; 49 | } 50 | 51 | #woo_pp_ec_button_checkout { 52 | display: none; 53 | } 54 | 55 | #payment .place-order .button { 56 | display: block; 57 | } 58 | /** 59 | * PayPal Payment buttons generated via the SDK need to be styled via CSS. 60 | * To be backwards compatible, these rules are inline with the widths used by PayPal JS. 61 | * 62 | * @see https://developer.paypal.com/docs/archive/checkout/how-to/customize-button/#size 63 | * @see https://developer.paypal.com/docs/checkout/integration-features/customize-button/#size 64 | */ 65 | .wc_ppec_small_payment_buttons { 66 | width: 150px; 67 | display: inline-block; 68 | } 69 | .wc_ppec_medium_payment_buttons { 70 | width: 250px; 71 | display: inline-block; 72 | } 73 | .wc_ppec_large_payment_buttons { 74 | width: 350px; 75 | display: inline-block; 76 | } 77 | -------------------------------------------------------------------------------- /assets/img/in-context-composite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-paypal-express-checkout/03c9b5d24db5983bca7d461c17a1674ab7e70075/assets/img/in-context-composite.png -------------------------------------------------------------------------------- /assets/js/admin/ppec-upgrade-notice.js: -------------------------------------------------------------------------------- 1 | ;(function ( $, window, document ) { 2 | 'use strict'; 3 | 4 | // Plugin rows. 5 | let $notice_row = $( 'tr#ppec-migrate-notice' ); 6 | let $ppec_row = $notice_row.prev(); 7 | let $ppcp_row = $( 'tr[data-slug="woocommerce-paypal-payments"]' ); 8 | 9 | $ppec_row.toggleClass( 'hide-border', true ); 10 | 11 | // Check whether PayPal Payments is installed. 12 | let is_paypal_payments_installed = $ppcp_row.length > 0; 13 | let is_paypal_payments_active = is_paypal_payments_installed && $ppcp_row.hasClass( 'active' ); 14 | 15 | let updateUI = function() { 16 | // Dynamically update plugin activation link to handle plugin folder renames. 17 | if ( is_paypal_payments_installed > 0 ) { 18 | $notice_row.find( 'a#ppec-activate-paypal-payments' ).attr( 'href', $ppcp_row.find( 'span.activate a' ).attr( 'href' ) ); 19 | } 20 | 21 | // Hide notice/buttons conditionally. 22 | $notice_row.find( 'a#ppec-install-paypal-payments' ).toggle( ! is_paypal_payments_installed ); 23 | $notice_row.find( 'a#ppec-activate-paypal-payments' ).toggle( is_paypal_payments_installed && ! is_paypal_payments_active ); 24 | 25 | // Display buttons area. 26 | $notice_row.find( '.ppec-notice-buttons' ).removeClass( 'hidden' ); 27 | }; 28 | 29 | // Handle delete event for PayPal Payments. 30 | $( document ).on( 'wp-plugin-delete-success', function( event, response ) { 31 | if ( 'woocommerce-paypal-payments' === response.slug ) { 32 | is_paypal_payments_installed = false; 33 | is_paypal_payments_active = false; 34 | updateUI(); 35 | } 36 | } ); 37 | 38 | // Change button text when install link is clicked. 39 | $notice_row.find( '#ppec-install-paypal-payments' ).click( function( e ) { 40 | e.preventDefault(); 41 | $( this ).addClass( 'updating-message' ).text( 'Installing...' ); 42 | const install_link = $( this ).attr('href'); 43 | setTimeout( function(){ 44 | window.location = install_link; 45 | }, 50 ); 46 | } ); 47 | 48 | // Dismiss button. 49 | $( document).on( 'click', '#ppec-migrate-notice button.notice-dismiss', function( e ) { 50 | $.ajax( 51 | { 52 | url: ajaxurl, 53 | method: 'POST', 54 | data: { 55 | action: 'ppec_dismiss_ppec_upgrade_notice', 56 | _ajax_nonce: $notice_row.attr( 'data-dismiss-nonce' ) 57 | }, 58 | dataType: 'json', 59 | success: function( res ) { 60 | $ppec_row.removeClass( 'hide-border' ); 61 | } 62 | } 63 | ); 64 | } ); 65 | 66 | updateUI(); 67 | 68 | })( jQuery, window, document ); 69 | -------------------------------------------------------------------------------- /assets/js/dist/fetch-polyfill.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.WHATWGFetch={})}(this,function(a){"use strict";var e="URLSearchParams"in self,r="Symbol"in self&&"iterator"in Symbol,h="FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),o="FormData"in self,n="ArrayBuffer"in self;if(n)var i=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],s=ArrayBuffer.isView||function(t){return t&&-1n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o}); -------------------------------------------------------------------------------- /assets/js/wc-gateway-ppec-frontend-in-context-checkout.js: -------------------------------------------------------------------------------- 1 | ;(function ( $, window, document ) { 2 | 'use strict'; 3 | 4 | var $wc_ppec = { 5 | init: function() { 6 | window.paypalCheckoutReady = function() { 7 | paypal.checkout.setup( 8 | wc_ppec_context.payer_id, 9 | { 10 | environment: wc_ppec_context.environment, 11 | button: ['woo_pp_ec_button', 'woo_pp_ppc_button'], 12 | locale: wc_ppec_context.locale, 13 | container: ['woo_pp_ec_button', 'woo_pp_ppc_button'] 14 | } 15 | ); 16 | } 17 | } 18 | } 19 | 20 | var costs_updated = false; 21 | 22 | $( '#woo_pp_ec_button' ).on( 'click', function( event ) { 23 | if ( costs_updated ) { 24 | costs_updated = false; 25 | 26 | return; 27 | } 28 | 29 | event.stopPropagation(); 30 | 31 | var data = { 32 | 'nonce': wc_ppec_context.update_shipping_costs_nonce, 33 | }; 34 | 35 | var href = $(this).attr( 'href' ); 36 | 37 | $.ajax( { 38 | type: 'POST', 39 | data: data, 40 | url: wc_ppec_context.ajaxurl, 41 | success: function( response ) { 42 | costs_updated = true; 43 | $( '#woo_pp_ec_button' ).trigger( 'click' ); 44 | } 45 | } ); 46 | } ); 47 | 48 | if ( wc_ppec_context.show_modal ) { 49 | $wc_ppec.init(); 50 | } 51 | })( jQuery, window, document ); 52 | -------------------------------------------------------------------------------- /assets/js/wc-gateway-ppec-generate-cart.js: -------------------------------------------------------------------------------- 1 | /* global wc_ppec_generate_cart_context */ 2 | ;(function( $, window, document ) { 3 | 'use strict'; 4 | 5 | // This button state is only applicable to non-SPB click handler below. 6 | var button_enabled = true; 7 | $( '#woo_pp_ec_button_product' ) 8 | .on( 'enable.legacy', function() { 9 | button_enabled = true; 10 | } ) 11 | .on( 'disable.legacy', function() { 12 | button_enabled = false; 13 | } ); 14 | 15 | $( '#woo_pp_ec_button_product' ) 16 | .on( 'enable', function() { 17 | $( '#woo_pp_ec_button_product' ) 18 | .css( { 19 | 'cursor': '', 20 | '-webkit-filter': '', // Safari 6.0 - 9.0 21 | 'filter': '', 22 | } ) 23 | .off( 'mouseup' ) 24 | .find( '> *' ) 25 | .css( 'pointer-events', '' ); 26 | } ) 27 | .on( 'disable', function() { 28 | $( '#woo_pp_ec_button_product' ) 29 | .css( { 30 | 'cursor': 'not-allowed', 31 | '-webkit-filter': 'grayscale( 100% )', // Safari 6.0 - 9.0 32 | 'filter': 'grayscale( 100% )', 33 | } ) 34 | .on( 'mouseup', function( event ) { 35 | event.stopImmediatePropagation(); 36 | form.find( ':submit' ).trigger( 'click' ); 37 | } ) 38 | .find( '> *' ) 39 | .css( 'pointer-events', 'none' ); 40 | } ); 41 | 42 | // True if the product is simple or the user selected a valid variation. False on variable product without a valid variation selected 43 | var variation_valid = true; 44 | 45 | // True if all the fields of the product form are valid (such as required fields configured by Product Add-Ons). False otherwise 46 | var fields_valid = true; 47 | 48 | var form = $( 'form.cart' ); 49 | 50 | var update_button = function() { 51 | $( '#woo_pp_ec_button_product' ).trigger( ( variation_valid && fields_valid ) ? 'enable' : 'disable' ); 52 | }; 53 | 54 | var validate_form = function() { 55 | // Check fields are valid and allow third parties to attach their own validation checks 56 | fields_valid = form.get( 0 ).checkValidity() && $( document ).triggerHandler( 'wc_ppec_validate_product_form', [ fields_valid, form ] ) !== false; 57 | 58 | update_button(); 59 | }; 60 | 61 | // It's a variations form, button availability should depend on its events 62 | if ( $( '.variations_form' ).length ) { 63 | variation_valid = false; 64 | 65 | $( '.variations_form' ) 66 | .on( 'show_variation', function( event, form, purchasable ) { 67 | variation_valid = purchasable; 68 | update_button(); 69 | } ) 70 | .on( 'hide_variation', function() { 71 | variation_valid = false; 72 | update_button(); 73 | } ); 74 | } 75 | 76 | // Disable the button if there are invalid fields in the product page (like required fields from Product Addons) 77 | form.on( 'change', 'select, input, textarea', function() { 78 | // Hack: IE11 uses the previous field value for the checkValidity() check if it's called in the onChange handler 79 | setTimeout( validate_form, 0 ); 80 | } ); 81 | 82 | $( document ).ready(function() { 83 | validate_form(); 84 | } ); 85 | 86 | var generate_cart = function( callback ) { 87 | var data = { 88 | 'nonce': wc_ppec_generate_cart_context.generate_cart_nonce, 89 | 'attributes': {}, 90 | }; 91 | 92 | var field_pairs = form.serializeArray(); 93 | 94 | for ( var i = 0; i < field_pairs.length; i++ ) { 95 | // Prevent the default WooCommerce PHP form handler from recognizing this as an "add to cart" call 96 | if ( 'add-to-cart' === field_pairs[ i ].name ) { 97 | field_pairs[ i ].name = 'ppec-add-to-cart'; 98 | } 99 | 100 | // Save attributes as a separate prop in `data` object, 101 | // so that `attributes` can be used later on when adding a variable product to cart 102 | if ( -1 !== field_pairs[ i ].name.indexOf( 'attribute_' ) ) { 103 | data.attributes[ field_pairs[ i ].name ] = field_pairs[ i ].value; 104 | continue; 105 | } 106 | 107 | data[ field_pairs[ i ].name ] = field_pairs[ i ].value; 108 | } 109 | 110 | // If this is a simple product, the "Submit" button has the product ID as "value", we need to include it explicitly 111 | data[ 'ppec-add-to-cart' ] = $( '[name=add-to-cart]' ).val(); 112 | 113 | $.ajax( { 114 | type: 'POST', 115 | data: data, 116 | url: wc_ppec_generate_cart_context.ajaxurl, 117 | success: callback, 118 | } ); 119 | }; 120 | 121 | window.wc_ppec_generate_cart = generate_cart; 122 | 123 | // Non-SPB mode click handler, namespaced as 'legacy' as it's replaced by `payment` callback of Button API. 124 | $( '#woo_pp_ec_button_product' ).on( 'click.legacy', function( event ) { 125 | event.preventDefault(); 126 | 127 | if ( ! button_enabled ) { 128 | return; 129 | } 130 | 131 | $( '#woo_pp_ec_button_product' ).trigger( 'disable' ); 132 | 133 | var href = $(this).attr( 'href' ); 134 | 135 | generate_cart( function() { 136 | window.location.href = href; 137 | } ); 138 | } ); 139 | 140 | })( jQuery, window, document ); 141 | -------------------------------------------------------------------------------- /assets/js/wc-gateway-ppec-order-review.js: -------------------------------------------------------------------------------- 1 | ;(function ( $, window, document ) { 2 | 'use strict'; 3 | 4 | $( 'form.checkout' ).on( 'click', 'input[name="payment_method"]', function() { 5 | // Avoid toggling submit button if on confirmation screen 6 | if ( $( '#payment' ).find( '.wc-gateway-ppec-cancel' ).length ) { 7 | return; 8 | } 9 | 10 | var isPPEC = $( this ).is( '#payment_method_ppec_paypal' ); 11 | var togglePPEC = isPPEC ? 'show' : 'hide'; 12 | var toggleSubmit = isPPEC ? 'hide' : 'show'; 13 | 14 | $( '#woo_pp_ec_button_checkout' ).animate( { opacity: togglePPEC, height: togglePPEC, padding: togglePPEC }, 230 ); 15 | $( '#place_order' ).animate( { opacity: toggleSubmit, height: toggleSubmit, padding: toggleSubmit }, 230 ); 16 | } ); 17 | })( jQuery, window, document ); 18 | -------------------------------------------------------------------------------- /assets/js/wc-gateway-ppec-smart-payment-buttons.js: -------------------------------------------------------------------------------- 1 | /* global wc_ppec_context */ 2 | ;( function ( $, window, document ) { 3 | 'use strict'; 4 | 5 | // Use global 'paypal' object or namespaced 'paypal_sdk' as PayPal API (depends on legacy/SDK mode). 6 | var paypal = wc_ppec_context.use_checkout_js ? window.paypal : window.paypal_sdk; 7 | 8 | // Show error notice at top of checkout form, or else within button container 9 | var showError = function( errorMessage, selector ) { 10 | var $container = $( '.woocommerce-notices-wrapper, form.checkout' ); 11 | 12 | if ( ! $container || ! $container.length ) { 13 | $( selector ).prepend( errorMessage ); 14 | return; 15 | } else { 16 | $container = $container.first(); 17 | } 18 | 19 | // Adapted from https://github.com/woocommerce/woocommerce/blob/ea9aa8cd59c9fa735460abf0ebcb97fa18f80d03/assets/js/frontend/checkout.js#L514-L529 20 | $( '.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message' ).remove(); 21 | $container.prepend( '
' + errorMessage + '
' ); 22 | $container.find( '.input-text, select, input:checkbox' ).trigger( 'validate' ).trigger( 'blur' ); 23 | 24 | var scrollElement = $( '.woocommerce-NoticeGroup-checkout' ); 25 | if ( ! scrollElement.length ) { 26 | scrollElement = $container; 27 | } 28 | 29 | if ( $.scroll_to_notices ) { 30 | $.scroll_to_notices( scrollElement ); 31 | } else { 32 | // Compatibility with WC <3.3 33 | $( 'html, body' ).animate( { 34 | scrollTop: ( $container.offset().top - 100 ) 35 | }, 1000 ); 36 | } 37 | 38 | $( document.body ).trigger( 'checkout_error' ); 39 | } 40 | 41 | // Map funding method settings to enumerated options provided by PayPal (checkout.js). 42 | var getFundingMethods = function( methods ) { 43 | if ( ! methods ) { 44 | return undefined; 45 | } 46 | 47 | var paypal_funding_methods = []; 48 | 49 | $.each( methods, function( index, method_name ) { 50 | var method = paypal.FUNDING[ method_name.toUpperCase() ]; 51 | if ( method ) { 52 | paypal_funding_methods.push( method ); 53 | } 54 | } ); 55 | 56 | return paypal_funding_methods; 57 | } 58 | 59 | var renderCreditMessaging = function( buttonSelector ) { 60 | if ( 'undefined' === typeof wc_ppec_context.credit_messaging || ! wc_ppec_context.credit_messaging || 'undefined' === typeof paypal.Messages ) { 61 | return; 62 | } 63 | 64 | if ( 'undefined' != typeof paypal.isFundingEligible && ! paypal.isFundingEligible( paypal.FUNDING.CREDIT ) && ! paypal.isFundingEligible( paypal.FUNDING.PAYLATER ) ) { 65 | return; 66 | } 67 | 68 | if ( 0 === $( buttonSelector ).length ) { 69 | return; 70 | } 71 | 72 | // Add an element for messaging. 73 | var messagingWrapper = $( '
' ).prependTo( buttonSelector ).get( 0 ); 74 | paypal.Messages( wc_ppec_context.credit_messaging ).render( messagingWrapper ); 75 | } 76 | 77 | var render = function( isMiniCart ) { 78 | var prefix = isMiniCart ? 'mini_cart_' : ''; 79 | var button_size = wc_ppec_context[ prefix + 'button_size' ]; 80 | var button_layout = wc_ppec_context[ prefix + 'button_layout' ]; 81 | var button_label = ( 'undefined' !== wc_ppec_context[ prefix + 'button_label' ] ) ? wc_ppec_context[ prefix + 'button_label' ] : wc_ppec_context['button_label']; 82 | var allowed = wc_ppec_context[ prefix + 'allowed_methods' ]; 83 | var disallowed = wc_ppec_context[ prefix + 'disallowed_methods' ]; 84 | 85 | var selector = isMiniCart ? '#woo_pp_ec_button_mini_cart' : '#woo_pp_ec_button_' + wc_ppec_context.page; 86 | var fromCheckout = 'checkout' === wc_ppec_context.page && ! isMiniCart; 87 | const return_url = wc_ppec_context['return_url']; 88 | const cancel_url = wc_ppec_context['cancel_url']; 89 | 90 | // Don't render if selector doesn't exist or is already rendered in DOM. 91 | if ( ! $( selector ).length || $( selector ).children().length ) { 92 | return; 93 | } 94 | 95 | var button_args = { 96 | env: wc_ppec_context.environment, 97 | locale: wc_ppec_context.locale, 98 | commit: fromCheckout, 99 | 100 | funding: { 101 | allowed: getFundingMethods( allowed ), 102 | disallowed: getFundingMethods( disallowed ), 103 | }, 104 | 105 | style: { 106 | color: wc_ppec_context.button_color, 107 | shape: wc_ppec_context.button_shape, 108 | label: button_label, 109 | layout: button_layout, 110 | size: button_size, 111 | branding: true, 112 | tagline: false, 113 | }, 114 | 115 | validate: function( actions ) { 116 | // Only enable on variable product page if purchasable variation selected. 117 | $( '#woo_pp_ec_button_product' ).off( '.legacy' ) 118 | .on( 'enable', actions.enable ) 119 | .on( 'disable', actions.disable ); 120 | }, 121 | 122 | payment: function() { 123 | // Clear any errors from previous attempt. 124 | $( '.woocommerce-error', selector ).remove(); 125 | 126 | return new Promise( function( resolve, reject ) { 127 | // First, generate cart if triggered from single product. 128 | if ( 'product' === wc_ppec_context.page && ! isMiniCart ) { 129 | window.wc_ppec_generate_cart( resolve ); 130 | } else { 131 | resolve(); 132 | } 133 | } ).then( function() { 134 | // Make PayPal Checkout initialization request. 135 | var data = $( selector ).closest( 'form' ) 136 | .add( $( ' ' ) 137 | .attr( 'value', wc_ppec_context.start_checkout_nonce ) 138 | ) 139 | .add( $( ' ' ) 140 | .attr( 'value', fromCheckout ? 'yes' : 'no' ) 141 | ) 142 | .serialize(); 143 | 144 | var request_callback = function( response ) { 145 | if ( ! response.success ) { 146 | // Error messages may be preformatted in which case response structure will differ 147 | var messages = response.data ? response.data.messages : response.messages; 148 | if ( 'string' === typeof messages ) { 149 | showError( messages ); 150 | } else { 151 | var messageItems = messages.map( function( message ) { 152 | return '
  • ' + message + '
  • '; 153 | } ).join( '' ); 154 | showError( '', selector ); 155 | } 156 | return null; 157 | } 158 | return response.data.token; 159 | }; 160 | 161 | if ( ! wc_ppec_context.use_checkout_js ) { 162 | return fetch( wc_ppec_context.start_checkout_url, { 163 | method: 'post', 164 | cache: 'no-cache', 165 | credentials: 'same-origin', 166 | headers: { 167 | 'Content-Type': 'application/x-www-form-urlencoded', 168 | }, 169 | body: data 170 | } ).then( function ( response ) { 171 | return response.json(); 172 | } ).then( request_callback ); 173 | } else { 174 | return paypal.request( { 175 | method: 'post', 176 | url: wc_ppec_context.start_checkout_url, 177 | body: data, 178 | } ).then( request_callback ); 179 | } 180 | } ); 181 | }, 182 | 183 | onAuthorize: function( data, actions ) { 184 | if ( fromCheckout ) { 185 | // Pass data necessary for authorizing payment to back-end. 186 | $( 'form.checkout' ) 187 | .append( $( ' ' ).attr( 'value', ! wc_ppec_context.use_checkout_js ? data.orderID : data.paymentToken ) ) 188 | .append( $( ' ' ).attr( 'value', data.payerID ) ) 189 | .trigger( 'submit' ); 190 | } else { 191 | // Navigate to order confirmation URL specified in original request to PayPal from back-end. 192 | if ( ! wc_ppec_context.use_checkout_js ) { 193 | const query_args = '?woo-paypal-return=true&token=' + data.orderID + '&PayerID=' + data.payerID; 194 | return actions.redirect( return_url + query_args ); 195 | } 196 | 197 | return actions.redirect(); 198 | } 199 | }, 200 | 201 | onCancel: function( data, actions ) { 202 | if ( cancel_url && 'orderID' in data ) { 203 | const query_args = '?woo-paypal-cancel=true&token=' + data.orderID; 204 | return actions.redirect( cancel_url + query_args ); 205 | } 206 | }, 207 | 208 | onError: function() { 209 | jQuery( selector ).empty(); 210 | render(); 211 | }, 212 | }; 213 | 214 | if ( ! wc_ppec_context.use_checkout_js ) { 215 | if ( ! isMiniCart ) { 216 | renderCreditMessaging( selector ); 217 | } 218 | 219 | // 'payment()' and 'onAuthorize()' callbacks from checkout.js are now 'createOrder()' and 'onApprove()'. 220 | Object.defineProperty( button_args, 'createOrder', Object.getOwnPropertyDescriptor( button_args, 'payment' ) ); 221 | Object.defineProperty( button_args, 'onApprove', Object.getOwnPropertyDescriptor( button_args, 'onAuthorize' ) ); 222 | 223 | // 'style.size' is no longer supported in the JS SDK. See https://developer.paypal.com/docs/checkout/integration-features/customize-button/#size. 224 | delete button_args['style']['size']; 225 | 226 | // Add a class selector so the buttons can be styled via css. 227 | $( selector ).addClass( 'wc_ppec_' + button_size + '_payment_buttons' ); 228 | 229 | // Drop other args no longer needed in the JS SDK. 230 | var args_to_remove = [ 'env', 'locale', 'commit', 'funding', 'payment', 'onAuthorize' ]; 231 | args_to_remove.forEach( function( arg ) { 232 | delete button_args[ arg ] 233 | }); 234 | 235 | var disabledFundingSources = getFundingMethods( disallowed ); 236 | if ( 'undefined' === typeof( disabledFundingSources ) || ! disabledFundingSources || 0 === disabledFundingSources.length ) { 237 | paypal.Buttons( button_args ).render( selector ); 238 | } else { 239 | // Render context specific buttons. 240 | paypal.getFundingSources().forEach( function( fundingSource ) { 241 | if ( -1 !== disabledFundingSources.indexOf( fundingSource ) ) { 242 | return; 243 | } 244 | 245 | var buttonSettings = { 246 | createOrder: button_args.createOrder, 247 | onApprove: button_args.onApprove, 248 | onError: button_args.onError, 249 | onCancel: button_args.onCancel, 250 | fundingSource: fundingSource, 251 | style: ( paypal.FUNDING.PAYPAL === fundingSource ) ? button_args.style : { layout: button_args.style.layout, shape: button_args.style.shape } 252 | }; 253 | 254 | var button = paypal.Buttons( buttonSettings ); 255 | 256 | if ( button.isEligible() ) { 257 | button.render( selector ); 258 | } 259 | } ); 260 | } 261 | } else { 262 | paypal.Button.render( button_args, selector ); 263 | } 264 | }; 265 | 266 | // Render cart, single product, or checkout buttons. 267 | if ( wc_ppec_context.page ) { 268 | if ( 'checkout' !== wc_ppec_context.page ) { 269 | render(); 270 | } 271 | 272 | $( document.body ).on( 'updated_cart_totals updated_checkout', render.bind( this, false ) ); 273 | } 274 | 275 | // Render buttons in mini-cart if present. 276 | $( document.body ).on( 'wc_fragments_loaded wc_fragments_refreshed', function() { 277 | var $button = $( '.widget_shopping_cart #woo_pp_ec_button_mini_cart' ); 278 | if ( $button.length ) { 279 | // Clear any existing button in container, and render. 280 | $button.empty(); 281 | render( true ); 282 | } 283 | } ); 284 | } )( jQuery, window, document ); 285 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | *** Changelog *** 2 | 3 | = 2.1.3 - 2021-09-16 = 4 | * Tweak - Remove broken URL from plugin headers. PR#887 5 | * Tweak - Update notice on plugins page about support and EOL. PR#886 6 | 7 | = 2.1.2 - 2021-06-28 = 8 | * Fix - Prevent fatal error when a line item isn't a WC_Product instance. PR#872 9 | * Fix - [WC Subscriptions] Update the shipping packages address using a dynamic key rather than assuming a 0 index. PR#871 10 | * New - Allow hiding of funding methods MercadoPago and BLIK. PR#870 11 | * Tweak - Make labels/descriptions more consistent on the settings screen. PR#771 12 | * Tweak - Make WooCommerce 3.2.0 explicit. PR#868 13 | * Dev - Add hooks to alter names and descriptions of line items sent to PayPal. PR#869 14 | * Fix - Create session cookie only when needed. PR#793, PR#845. 15 | * Tweak - Mark as compatible with latest WordPress and WooCommerce. PR#867 16 | * Fix - Replace jQuery 3.x deprecated functions. PR#852 17 | * Fix - Honor shape settings when rendering buttons for alternative funding sources. PR#844 18 | * New - Add notice on plugins page to upgrade to PayPal Payments. PR#866 19 | 20 | = 2.1.1 - 2020-11-24 = 21 | * Fix - Update the bundle.pem file to use the certificates from PayPal. PR#822 22 | * Tweak - PHP 8.0 compatibility. PR#837 23 | 24 | = 2.1.0 - 2020-10-06 = 25 | * New - Add support for PayPal Credit messaging. PR#810 26 | * Fix - Hide the "Pay Later" funding method when "PayPal Credit" is disabled. PR#811 27 | * Fix - Display correct image size in the PayPal Checkout window. PR#779 28 | 29 | = 2.0.3 - 2020-07-01 = 30 | * Fix - Records the proper refunded_amount to _woo_pp_txnData in the database PR#764 31 | * Fix - Redirect customers back to the original page they left on after closing PayPal modal PR#765 32 | * Fix - Preserve horizontal layout style setting when using standalone buttons PR#774 33 | * Fix - Smart payment buttons compatibility with older browsers PR#778 34 | * Tweak - Update the Require Phone Number field description PR#772 35 | * Dev - Make the SDK script args filterable PR#763 36 | 37 | = 2.0.2 - 2020-05-28 = 38 | * Fix - Javascript errors during checkout when the Payment Action is set to Authorize. PR#754 39 | * Fix - Style the Smart Payment Buttons according to the chosen button size setting. PR#753 40 | * Tweak - Change the "or" separator used on the cart page to be consistent with other payment gateways (uppercase and 100% opacity). PR#755 41 | 42 | = 2.0.1 - 2020-05-26 = 43 | * Fix - PayPal buttons not loading on the page, accompanied with the javascript/console error: "paypal.getFundingSources (or paypal.Buttons) is not a function". PR#740 44 | 45 | = 2.0.0 - 2020-05-25 = 46 | * New - Upgrade to the latest PayPal Checkout Javascript SDK. PR#668 47 | * Add - New setting found under Button Styles for choosing a Smart Payment Button label. PR#666 48 | * Add - Support for more locales. PR#658 49 | * Fix - Display Smart Payment Buttons on Product pages built from a shortcode. PR#665 50 | * Fix - Send the product SKU to PayPal so it's displayed in the order/transaction details and reports on PayPal. PR#664 51 | * Fix - Show an error when saving incomplete/missing API credentials. PR#712 52 | * Fix - Remove PHP warnings in later versions of PHP when a PayPal Session doesn't exist. PR#727 53 | * Fix - Error when processing refunds (Already Refunded. No Amount to Refund). PR#710 54 | * Fix - Required state field errors on the "Confirm your PayPal Order" page when returning from PayPal. PR#725 55 | * Fix - Display WC Add To Cart validation errors on the product page when clicking the PayPal Smart Payment Buttons. PR#707 56 | * Update - Smart Payment Buttons are enabled by default and settings to toggle these on/off have been removed and replaced with a filter. PR#660 57 | * Update - Deprecate unused/incomplete function `WC_Gateway_PPEC_Client::update_billing_agreement()`. PR#602 58 | * Update - Move inline javascript found in `settings-ppec.php` to `ppec-settings.js`. PR#676 59 | * Update - Move Support and Documentation links from the plugin actions to plugin meta section on the Plugin activation/deactivation page. PR#735 60 | * Update - WooCommerce 4.1 and WordPress 5.4 compatibility. PR#732 61 | 62 | = 1.6.21 - 2020-04-14 = 63 | * Fix - Ensure Puerto Rico and supported Locales are eligible for PayPal Credit. PR#693 64 | * Fix - Support purchasing subscriptions with $0 initial payment - free trials, synced etc. PR#698 65 | * Fix - Only make the billing fields optional during an active PayPal Checkout session. PR#697 66 | * Fix - Uncaught JS errors on product page when viewing and out-of-stock product. PR#704 67 | * Fix - Loading API certificates and improves managing certificate settings. PR#696 68 | * Fix - Displaying PayPal Smart Payment buttons on pages with single product shortcode. PR#665 69 | * Fix - Do not add discounts to total item amount and cause line item amount offset. PR#677 70 | * Fix - Redirect to Confirm your PayPal Order page for subscriptions initial purchases using PayPal Smart Buttons. PR#702 71 | * Fix - Display missing checkout notice when email format is incorrect. PR#708 72 | * Add - Filter product form validity via a new `wc_ppec_validate_product_form` event. PR#695 73 | * Add - Translation tables for states of more countries. PR#659 74 | * Update - WooCommerce 4.0 compatibility 75 | 76 | = 1.6.20 - 2020-02-18 = 77 | * Fix - Upgrade the plugin on plugins loaded rather than on plugin init. PR#682 78 | 79 | = 1.6.19 - 2020-02-06 = 80 | * Fix - Check if order exists before adding order actions. PR #653 81 | * Fix - Global attributes stripped before sent to PayPal if unicode characters. PR#470 82 | * Fix - Handle subscription payment change. PR#640 83 | * Fix - Fixes error "Trying to get property of non-object" found during onboarding wizard. PR#654 84 | * Fix - Hide smart payment buttons on mini cart when cart is empty. PR#450 85 | * Fix - Only display smart buttons on product page if product is in stock. PR#662 86 | * Fix - Do not display smart buttons for external products and grouped products. PR#663 87 | * Update - Display a WooCommerce pre 3.0 admin notice warning. In an upcoming release PayPal Checkout will drop support for WC 2.6 and below. PR#671 88 | 89 | = 1.6.18 - 2019-12-05 = 90 | * Fix - Send fees to PayPal as line items 91 | * Fix - Fix error 10426 when coupons are used 92 | * Fix - Call to a member function has_session() on null 93 | * Add - Notice about legacy payment buttons deprecation 94 | * Fix - Use order currency when renewing subscription instead of store currency 95 | * Update - WooCommerce 3.8 compatibility 96 | * Update - WordPress 5.3 compatibility 97 | 98 | = 1.6.17 - 2019-08-08 = 99 | * Update - WooCommerce 3.7 compatibility 100 | * Add - Filter to require display of billing agreement during checkout 101 | * Add - Add CURRENCYCODE to capture_payment 102 | * Add - Add filter for buttons on products 103 | * Fix - Skip wasteful render on initial Checkout page load 104 | * Fix - Appearance tweaks on Checkout screen 105 | 106 | = 1.6.16 - 2019-07-18 = 107 | * Fix - Don't require address for renewal of virtual subscriptions 108 | * Fix - Avoid broken confirmation screen edge case after 10486 redirect 109 | 110 | = 1.6.15 - 2019-06-19 = 111 | * Fix - Prevent PHP errors when no billing details are present in PP response 112 | * Fix - Require billing address for virtual products when enabled 113 | * Add - Hook when a payment error occurs 114 | 115 | = 1.6.14 - 2019-05-08 = 116 | * Fix - Failing checkout when no addons are used 117 | 118 | = 1.6.12 - 2019-05-08 = 119 | * Fix - Better handling of virtual subscriptions when billing address is not required 120 | * Fix - Prevent errors showing when purchasing a virtual product with WP_DEBUG enabled 121 | 122 | = 1.6.11 - 2019-04-17 = 123 | * Fix/Performance - Prevent db option updates during bootstrap on each page load 124 | * Tweak = WC 3.6 compatibiliy. 125 | 126 | = 1.6.10 - 2019-03-05 = 127 | * Fix - Use only product attributes when adding to cart 128 | 129 | = 1.6.9 - 2019-02-03 = 130 | * Fix - Avoid SPB render error by tweaking 'allowed' funding methods' empty value 131 | 132 | = 1.6.8 - 2019-01-25 = 133 | * Fix - Guard against themes applying filter with too few params 134 | 135 | = 1.6.7 - 2019-01-25 = 136 | * Fix - Error 10413 when using coupons 137 | * Fix: All variation details when using buttons on product pages are kept 138 | * Fix: Always render the PayPal buttons in the mini cart 139 | 140 | = 1.6.6 - 2019-01-09 = 141 | * Fix - Discount items were not being included 142 | * Add - Filter for order details to accept decimal quantities of products 143 | * Fix - Unable to buy variation from product page 144 | * Fix - Can use PayPal from product page without inputting required fields 145 | * Add - Display PayPal fees under the totals on the order admin page 146 | * Add - Prefill name, phone, and email info in PayPal Guest Checkout from checkout screen 147 | 148 | = 1.6.5 - 2018-10-31 = 149 | * Fix - Truncate the line item descriptions to avoid exceeding PayPal character limits. 150 | * Update - WC 3.5 compatibility. 151 | * Fix - checkout.js script loading when not needed. 152 | * Fix - Missing shipping total and address when starting from checkout page. 153 | 154 | = 1.6.4 - 2018-09-27 = 155 | * Fix - Billing address from Checkout form not being passed to PayPal via Smart Payment Button. 156 | * Fix - Checkout form not being validated until after Smart Payment Button payment flow. 157 | 158 | = 1.6.3 - 2018-08-15 = 159 | * Fix - Fatal error caused by a fix for Smart Payment Buttons. 160 | 161 | = 1.6.2 - 2018-08-15 = 162 | * Fix - Tax not applied on the (Confirm your PayPal order) page at the checkout. 163 | 164 | = 1.6.1 - 2018-07-04 = 165 | * Fix - GDPR Fatal error exporting user data when they have PPEC subscriptions. 166 | * Fix - PayPal Credit still being disabled by default. 167 | * Update - Rename 'PayPal Express Checkout' to 'PayPal Checkout'. 168 | * Fix - Missing PayPal branding in "Buy Now" Smart Payment Button. 169 | * Fix - PHP warning when PayPal Credit not supported and no funding methods hidden. 170 | * Fix - Smart Payment Buttons gateway not inheriting IPN and subscription handling. 171 | * Fix - Single product Smart Payment Button failing without existing session. 172 | * Fix - When cart is empty, JS error on cart page and mini-cart payment buttons showing. 173 | * Add - Locale filter. 174 | 175 | = 1.6.0 - 2018-06-27 = 176 | * Add - Smart Payment Buttons mode as alternative to directly embedded image links for all instances of PayPal button. 177 | * Fix - Help tip alignment for image settings. 178 | * Update - Enable PayPal Credit by default, and restrict its support by currency. 179 | * Update - Omit 'Express Checkout' portion of default payment method title. 180 | * Update - Enable Express Checkout on regular checkout page by default. 181 | * Update - Enable Express Checkout on single product page by default. 182 | 183 | = 1.5.6 - 2018-06-06 = 184 | * Fix - Virtual products cause issues with billing details validation. 185 | 186 | = 1.5.5 - 2018-05-23 = 187 | * Update - WC 3.4 compatibility 188 | * Update - Privacy policy notification. 189 | * Update - Export/erasure hooks added. 190 | 191 | = 1.5.4 - 2018-05-08 = 192 | * Add - Hook to make billing address not required `woocommerce_paypal_express_checkout_address_not_required` (bool). 193 | * Fix - Duplicate checkout settings when PP Credit option is enabled. 194 | * Fix - Impossible to open API credentials after saving Settings. 195 | * Fix - Prevent filtering if PPEC is not enabled. 196 | * Fix - Single Product checkout: Quantity being duplicated due to multiple AJAX calls. 197 | * Fix - When returning from PayPal, place order buttons says "proceed to payment". 198 | * Tweak - Default billing address to be required. 199 | 200 | = 1.5.3 - 2018-03-28 = 201 | * Fix - wp_enqueue_media was not correctly loaded causing weird behavior with other parts of system wanting to use it. 202 | * Fix - Typo in activation hook. 203 | 204 | = 1.5.2 - 2018-02-20 = 205 | * Tweak - Express checkout shouldn't display "Review your order before the payment". 206 | * Fix - Compatibility with Subscriptions and Checkout from Single Product page. 207 | * Fix - Make sure session object exists before use to prevent fatal error. 208 | 209 | = 1.5.1 = 210 | * Add - Hooks for Settings. 211 | * Fix - Missing Settings link on Plugins page. 212 | * Fix - Use correct image URL for PayPal image logo. 213 | * Tweak - Default to signature method if certificate missing, rather than other way around. 214 | 215 | = 1.5.0 = 216 | * Add - PayPal credit is now available on checkout. 217 | * Fix - WC 3.3 compatibility. 218 | * Add - Ability to select existing / upload new image(s) for logo / header fields. 219 | * Fix - Shipping address overridden when PayPal returns billing address. 220 | 221 | = 1.4.7 = 222 | * Fix - Issue with missing PayPal session information. 223 | * Fix - Dependency error when using LibreSSL. 224 | * Fix - Additional compatibility with shipping plugins 225 | * Fix - Issue where deprecated `WC_Cart::get_cart_url` is being used. 226 | * Tweak - Makes admin notification dismissible. 227 | 228 | = 1.4.6 = 229 | * Fix - Coupon related PayPal error 10413. 230 | 231 | = 1.4.5 = 232 | * Fix - Title/Description fields in the settings should appear based on Enable PayPal Express Checkout. 233 | * Add - Invoice Prefix now has the ability to be empty. 234 | * Fix - Additional compatibility fixes for line items. 235 | * Fix - PHP notice for Subscription id. 236 | 237 | = 1.4.4 = 238 | * Fix - PayPal error (10431). 239 | * Fix - PHP notices. 240 | 241 | = 1.4.3 = 242 | * Fix - Refunds not working on authorize then captured transactions. 243 | * Fix - Checkout on single product available before variations are chosen. 244 | * Fix - Not Returning PayPal Transaction Fee. 245 | * Fix - 10431 (Item Amount Invalid at Checkout) error with discounts. 246 | * Fix - Phone not returned and "Require Phone Number" setting not working. 247 | 248 | = 1.4.2 = 249 | * Fix - _paypal_status on Authorize transactions not updating to processing after capture. 250 | * Fix - 10413 (The totals of the cart item amounts do not match order amounts) error with discounts. 251 | * Fix - Shipping Address being required on Virtual products. 252 | 253 | = 1.4.1 = 254 | * Fix - Properly calculate whether Billing phone is required or not. 255 | * Fix - Set NOSHIPPING based on product shipping requiredness (e.g. virtual products do not need shipping, etc). 256 | 257 | = 1.4.0 = 258 | * Tweak - Use shipping discount instead of tax when adjustment negative. 259 | * Fix - Cannot process refunds on "authorize" transactions. 260 | * Add - Option for displaying express checkout button on the product page. 261 | * Fix - If there are no shipping options in WooCommerce, PayPal doesn't pass a shipping address to WC. 262 | * Add - Option to set Billing phone number mandatory. 263 | * Add - Option to disable checkout with PayPal button on Cart page. 264 | * Fix - Trigger required shipping cost before checkout. 265 | 266 | = 1.3.0 = 267 | * Fix - Fatal Error calling is_main_query. 268 | * Fix - Customer invoice email doesn't allow payment with PPEC. 269 | * Fix - Double stock reduction. 270 | * Fix - Payment automatically goes to complete when payment action set to Authorize. 271 | 272 | = 1.2.1 = 273 | * Fix - Avoid plugin links notice when WooCommerce is not active - props rellect 274 | * Fix - Do not show this gateway when the cart amount is zero 275 | * Fix - Fix 10413 error that prevents checking out with a coupon 276 | * Fix - Filter default address fields to ensure they are not required 277 | 278 | = 1.2.0 = 279 | * Fix - Prevent conflict with other gateways. 280 | * Fix - Compatibility with WooCommerce 3.0, including ensuring the customer address is saved correctly. 281 | 282 | = 1.1.3 = 283 | * Fix - Guest users can checkout without giving shipping information when required. 284 | * Fix - Modal popup not working properly. Changed to full page redirect with a hook to add back the modal/popup. 285 | * Tweak - Guest checkout is on by default. Should be turned off by using this filter: woocommerce_paypal_express_checkout_allow_guests. 286 | 287 | = 1.1.2 = 288 | * Fix - Make sure translations are loaded properly. 289 | * Fix - Added IPN (Instant Payment Notification) handler. 290 | * Fix - Make sure guest payment is enabled by default. 291 | 292 | = 1.1.1 = 293 | * Fixed fatal error prior to PHP 5.5 caused by passing empty() a non-variables. 294 | 295 | = 1.1.0 = 296 | * Improved flow after express checkout by removing billing and shipping fields and simply allowing shipping method selection. 297 | * Fix - Fixed in-context checkout to work after ajax cart reload. 298 | * Fix - Added missing 'large' button size. 299 | * Fix - Prevent double stock reduction when payment complete. 300 | * Fix - Allow PPE from pay page and don't use in-context checkout for PayPal Mark on checkout. 301 | * Fix - Increase timeout to 30 to prevent error #3. 302 | * Tweak - If the store owner decides to enable PayPal standard, respect that decision and remove EC from checkout screen. 303 | * Tweak - Change place order button to "continue to payment". 304 | * Tweak - Moved default button location to woocommerce_proceed_to_checkout hook. 305 | * Tweak - Improved button appearance and look alongside regular checkout button. 306 | 307 | = 1.0.4 = 308 | * Fix - Wrong section slug / tab after redirected from connect.woocommerce.com 309 | * Fix - Make sure to check if credentials were set in cart and checkout pages 310 | * Fix - Removed configuration of chipers to use for TLS 311 | 312 | = 1.0.3 = 313 | * Fix - Issue where missing rounding two decimal digits of tax causing transaction being refused 314 | * Fix - Issue where custom logo image URL is not saved 315 | 316 | = 1.0.2 = 317 | * Fix - Strip out HTML tags from item descriptions to prevent warning from PayPal 318 | * Fix - Issue of incorrect plugin's setting link from admin plugins page when using WooCommerce 2.6 319 | * Tweak - Make enabled option to default to true 320 | * Fix - Issue of missing help icon when plugin directory is not the same as plugin's slug. 321 | * Tweak - Add admin notice to setup / connect after plugin is activated. 322 | 323 | = 1.0.1 = 324 | * Fix - Make sure OpenSSL is installed with 1.0.1 as the minimum required version, otherwise display warning 325 | * Fix - Make sure cURL transport is available for WP HTTP API, otherwise display warning 326 | * Fix - Unhandled certificate-style API credential 327 | * Fix - Fixed calculated tax and coupons data that sent over to PayPal 328 | * Fix - Fixed calculated shipping discount data that sent over to PayPal 329 | 330 | = 1.0.0 = 331 | * Initial stable release 332 | 333 | = 0.2.0 = 334 | * Fix - Add cancel link on checkout page when session for PPEC is active 335 | * Fix - In-context mini browser keeps spinning because failure xhr response is not handled properly 336 | 337 | = 0.1.0 = 338 | * Beta release 339 | -------------------------------------------------------------------------------- /includes/abstracts/abstract-wc-gateway-ppec-client-credential.php: -------------------------------------------------------------------------------- 1 | _username; 37 | } 38 | 39 | /** 40 | * Get API password. 41 | * 42 | * @return string API password 43 | */ 44 | public function get_password() { 45 | return $this->_password; 46 | } 47 | 48 | /** 49 | * Get API subject. 50 | * 51 | * @return string API subject 52 | */ 53 | public function get_subject() { 54 | return $this->_subject; 55 | } 56 | 57 | /** 58 | * Retrieves the subdomain of the endpoint which should be used for this type 59 | * of credentials. 60 | * 61 | * @return string The appropriate endpoint, e.g. https://api.paypal.com/nvp 62 | * in this case the subdomain is 'api' 63 | */ 64 | abstract public function get_endpoint_subdomain(); 65 | 66 | /** 67 | * Retrieves a list of credentialing parameters that should be supplied to 68 | * PayPal. 69 | * 70 | * @return array An array of name-value pairs containing the API credentials 71 | * from this object. 72 | */ 73 | public function get_request_params() { 74 | $params = array( 75 | 'USER' => $this->_username, 76 | 'PWD' => $this->_password, 77 | ); 78 | 79 | if ( ! empty( $this->_subject ) ) { 80 | $params['SUBJECT'] = $this->_subject; 81 | } 82 | 83 | return $params; 84 | } 85 | 86 | /** 87 | * Allow certificate-based credential to configure cURL, especially 88 | * to set CURLOPT_SSLCERT and CURLOPT_SSLCERTPASSWD. 89 | * 90 | * @throws Exception 91 | * 92 | * @param resource &$handle The cURL handle returned by curl_init(). 93 | * @param array $r The HTTP request arguments. 94 | * @param string $url The request URL. 95 | * 96 | * @return void 97 | */ 98 | public function configure_curl( $handle, $r, $url ) { 99 | curl_setopt( $handle, CURLOPT_CAINFO, wc_gateway_ppec()->includes_path . 'pem/bundle.pem' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /includes/abstracts/abstract-wc-gateway-ppec-paypal-request-handler.php: -------------------------------------------------------------------------------- 1 | gateway = $gateway; 28 | } 29 | 30 | abstract protected function handle(); 31 | 32 | /** 33 | * Get the order from the PayPal 'Custom' variable. 34 | * 35 | * @param string $raw_custom JSON Data passed back by PayPal 36 | * @return bool|WC_Order Order object or false 37 | */ 38 | protected function get_paypal_order( $raw_custom ) { 39 | // We have the data in the correct format, so get the order. 40 | $custom = json_decode( $raw_custom ); 41 | if ( $custom && is_object( $custom ) ) { 42 | $order_id = $custom->order_id; 43 | $order_key = $custom->order_key; 44 | } else { 45 | wc_gateway_ppec_log( sprintf( '%s: %s', __FUNCTION__, 'Error: Order ID and key were not found in "custom".' ) ); 46 | return false; 47 | } 48 | 49 | $order = wc_get_order( $order_id ); 50 | if ( ! $order ) { 51 | // We have an invalid $order_id, probably because invoice_prefix has changed. 52 | $order_id = wc_get_order_id_by_order_key( $order_key ); 53 | $order = wc_get_order( $order_id ); 54 | } 55 | 56 | if ( $order ) { 57 | $order_key_from_order = version_compare( WC_VERSION, '3.0', '<' ) ? $order->order_key : $order->get_order_key(); 58 | } else { 59 | $order_key_from_order = ''; 60 | } 61 | 62 | if ( ! $order || $order_key_from_order !== $order_key ) { 63 | wc_gateway_ppec_log( sprintf( '%s: %s', __FUNCTION__, 'Error: Order Keys do not match.' ) ); 64 | return false; 65 | } 66 | return $order; 67 | } 68 | 69 | /** 70 | * Complete order, add transaction ID and note. 71 | * 72 | * @param WC_Order $order Order object 73 | * @param string $txn_id Transaction ID 74 | * @param string $note Order note 75 | */ 76 | protected function payment_complete( $order, $txn_id = '', $note = '' ) { 77 | $order->add_order_note( $note ); 78 | $order->payment_complete( $txn_id ); 79 | } 80 | 81 | /** 82 | * Hold order and add note. 83 | * 84 | * @param WC_Order $order Order object 85 | * @param string $reason On-hold reason 86 | */ 87 | protected function payment_on_hold( $order, $reason = '' ) { 88 | $order->update_status( 'on-hold', $reason ); 89 | if ( version_compare( WC_VERSION, '3.0', '<' ) ) { 90 | if ( ! get_post_meta( $order->id, '_order_stock_reduced', true ) ) { 91 | $order->reduce_order_stock(); 92 | } 93 | } else { 94 | wc_maybe_reduce_stock_levels( $order->get_id() ); 95 | } 96 | WC()->cart->empty_cart(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-admin-handler.php: -------------------------------------------------------------------------------- 1 | id : $order->get_id(); 50 | $payment_method = $old_wc ? $order->payment_method : $order->get_payment_method(); 51 | $paypal_status = $old_wc ? get_post_meta( $order_id, '_paypal_status', true ) : $order->get_meta( '_paypal_status', true ); 52 | 53 | // bail if the order wasn't paid for with this gateway 54 | if ( 'ppec_paypal' !== $payment_method || 'pending' !== $paypal_status ) { 55 | return $actions; 56 | } 57 | 58 | if ( ! is_array( $actions ) ) { 59 | $actions = array(); 60 | } 61 | 62 | $actions['ppec_capture_charge'] = esc_html__( 'Capture Charge', 'woocommerce-gateway-paypal-express-checkout' ); 63 | 64 | return $actions; 65 | } 66 | 67 | /** 68 | * Force zero decimal on specific currencies. 69 | */ 70 | public function force_zero_decimal() { 71 | $settings = wc_gateway_ppec()->settings; 72 | if ( $settings->currency_has_decimal_restriction() ) { 73 | update_option( 'woocommerce_price_num_decimals', 0 ); 74 | update_option( 'wc_gateway_ppce_display_decimal_msg', true ); 75 | } 76 | } 77 | 78 | /** 79 | * Show decimal warning. 80 | */ 81 | public function show_decimal_warning() { 82 | if ( get_option( 'wc_gateway_ppce_display_decimal_msg', false ) ) { 83 | ?> 84 |
    85 |

    86 | 87 |

    88 |
    89 | id : $order->get_id(); 145 | $this->capture_payment( $order_id ); 146 | 147 | return true; 148 | } 149 | 150 | /** 151 | * Capture payment when the order is changed from on-hold to complete or processing 152 | * 153 | * @param int $order_id 154 | */ 155 | public function capture_payment( $order_id ) { 156 | $order = wc_get_order( $order_id ); 157 | 158 | if ( ! $order ) { 159 | return; 160 | } 161 | 162 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 163 | $payment_method = $old_wc ? $order->payment_method : $order->get_payment_method(); 164 | $transaction_id = get_post_meta( $order_id, '_transaction_id', true ); 165 | 166 | if ( 'ppec_paypal' === $payment_method && $transaction_id ) { 167 | 168 | $trans_details = wc_gateway_ppec()->client->get_transaction_details( array( 'TRANSACTIONID' => $transaction_id ) ); 169 | 170 | if ( $this->is_authorized_only( $trans_details ) ) { 171 | $order_total = $old_wc ? $order->order_total : $order->get_total(); 172 | 173 | $params['AUTHORIZATIONID'] = $transaction_id; 174 | $params['AMT'] = floatval( $order_total ); 175 | $params['CURRENCYCODE'] = $old_wc ? $order->order_currency : $order->get_currency(); 176 | $params['COMPLETETYPE'] = 'Complete'; 177 | 178 | $result = wc_gateway_ppec()->client->do_express_checkout_capture( $params ); 179 | 180 | if ( is_wp_error( $result ) ) { 181 | $order->add_order_note( esc_html__( 'Unable to capture charge!', 'woocommerce-gateway-paypal-express-checkout' ) . ' ' . $result->get_error_message() ); 182 | } else { 183 | update_post_meta( $order_id, '_paypal_status', ! empty( $trans_details['PAYMENTSTATUS'] ) ? $trans_details['PAYMENTSTATUS'] : 'completed' ); 184 | 185 | if ( ! empty( $result['TRANSACTIONID'] ) ) { 186 | update_post_meta( $order_id, '_transaction_id', $result['TRANSACTIONID'] ); 187 | } 188 | 189 | // Translators: %s is a transaction ID. 190 | $order->add_order_note( sprintf( esc_html__( 'PayPal Checkout charge complete (Charge ID: %s)', 'woocommerce-gateway-paypal-express-checkout' ), $transaction_id ) ); 191 | } 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * Checks to see if the transaction can be captured 198 | * 199 | * @param array $trans_details 200 | */ 201 | public function is_authorized_only( $trans_details = array() ) { 202 | if ( ! is_wp_error( $trans_details ) && ! empty( $trans_details ) ) { 203 | if ( 'Pending' === $trans_details['PAYMENTSTATUS'] && 'authorization' === $trans_details['PENDINGREASON'] ) { 204 | return true; 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | 211 | /** 212 | * Cancel authorization (if one is present) 213 | * 214 | * @param int $order_id 215 | */ 216 | public function cancel_authorization( $order_id ) { 217 | $order = wc_get_order( $order_id ); 218 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 219 | $payment_method = $old_wc ? $order->payment_method : $order->get_payment_method(); 220 | 221 | if ( 'ppec_paypal' === $payment_method ) { 222 | 223 | $trans_id = get_post_meta( $order_id, '_transaction_id', true ); 224 | $trans_details = wc_gateway_ppec()->client->get_transaction_details( array( 'TRANSACTIONID' => $trans_id ) ); 225 | 226 | if ( $trans_id && $this->is_authorized_only( $trans_details ) ) { 227 | $params['AUTHORIZATIONID'] = $trans_id; 228 | 229 | $result = wc_gateway_ppec()->client->do_express_checkout_void( $params ); 230 | 231 | if ( is_wp_error( $result ) ) { 232 | $order->add_order_note( esc_html__( 'Unable to void charge!', 'woocommerce-gateway-paypal-express-checkout' ) . ' ' . $result->get_error_message() ); 233 | } else { 234 | // Translators: %s is a transaction ID. 235 | $order->add_order_note( sprintf( esc_html__( 'PayPal Checkout charge voided (Charge ID: %s)', 'woocommerce-gateway-paypal-express-checkout' ), $trans_id ) ); 236 | } 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Get admin URL for this gateway setting. 243 | * 244 | * @deprecated 245 | * 246 | * @return string URL 247 | */ 248 | public function gateway_admin_url( $gateway_class ) { 249 | _deprecated_function( 'WC_Gateway_PPEC_Admin_Handler::gateway_admin_url', '1.0.4', 'wc_gateway_ppec()->get_admin_setting_link' ); 250 | 251 | return wc_gateway_ppec()->get_admin_setting_link(); 252 | } 253 | 254 | /** 255 | * Maybe redirect to wc_gateway_ppec_with_paypal from PayPal standard 256 | * checkout settings. 257 | * 258 | * @return void 259 | */ 260 | public function maybe_redirect_to_ppec_settings() { 261 | if ( ! wc_gateway_ppec()->settings->enabled ) { 262 | return; 263 | } 264 | 265 | if ( empty( $_GET['tab'] ) || empty( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 266 | return; 267 | } 268 | 269 | if ( 'checkout' === $_GET['tab'] && 'wc_gateway_paypal' === $_GET['section'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 270 | $redirect = add_query_arg( array( 'section' => 'wc_gateway_ppec_with_paypal' ) ); 271 | wp_safe_redirect( $redirect ); 272 | } 273 | } 274 | 275 | /** 276 | * Reset API credentials if merchant clicked the reset credential link. 277 | * 278 | * When API credentials empty, the connect button will be displayed again, 279 | * allowing merchant to reconnect with other account. 280 | * 281 | * When WooCommerce Branding is active, this handler may not be invoked as 282 | * screen ID may evaluates to something else. 283 | * 284 | * @since 1.2.0 285 | */ 286 | public function maybe_reset_api_credentials() { 287 | if ( empty( $_GET['reset_ppec_api_credentials'] ) ) { 288 | return; 289 | } 290 | 291 | if ( empty( $_GET['reset_nonce'] ) || ! wp_verify_nonce( $_GET['reset_nonce'], 'reset_ppec_api_credentials' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 292 | return; 293 | } 294 | 295 | $settings = wc_gateway_ppec()->settings; 296 | $env = $settings->_environment; 297 | if ( ! empty( $_GET['environment'] ) ) { 298 | $env = $_GET['environment']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 299 | } 300 | $prefix = 'sandbox' === $env ? 'sandbox_' : ''; 301 | 302 | foreach ( array( 'api_username', 'api_password', 'api_signature', 'api_certificate' ) as $key ) { 303 | $key = $prefix . $key; 304 | $settings->{$key} = ''; 305 | } 306 | 307 | // Save environment too as when it switches to another env and merchant 308 | // click the reset they'd expect to save the environment too. 309 | $settings->environment = 'sandbox' === $env ? 'sandbox' : 'live'; 310 | 311 | $settings->save(); 312 | 313 | wp_safe_redirect( wc_gateway_ppec()->get_admin_setting_link() ); 314 | } 315 | 316 | /** 317 | * Displays the PayPal fee and the net total of the transaction without the PayPal charges 318 | * 319 | * @since 1.6.6 320 | * 321 | * @param int $order_id 322 | */ 323 | public function display_order_fee_and_payout( $order_id ) { 324 | $order = wc_get_order( $order_id ); 325 | 326 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 327 | $payment_method = $old_wc ? $order->payment_method : $order->get_payment_method(); 328 | $paypal_fee = wc_gateway_ppec_get_transaction_fee( $order ); 329 | $order_currency = $old_wc ? $order->order_currency : $order->get_currency(); 330 | $order_total = $old_wc ? $order->order_total : $order->get_total(); 331 | 332 | if ( 'ppec_paypal' !== $payment_method || ! is_numeric( $paypal_fee ) ) { 333 | return; 334 | } 335 | 336 | $net = $order_total - $paypal_fee; 337 | 338 | ?> 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | -  $order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | $order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 358 | 359 | 360 | 361 | =' ); 378 | $dismissed = isset( $_GET['wc_ppec_hide_3_0_notice'], $_GET['_wc_ppec_notice_nonce'] ) && wp_verify_nonce( $_GET['_wc_ppec_notice_nonce'], 'wc_ppec_hide_wc_notice_nonce' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 379 | 380 | if ( $wc_updated || $dismissed ) { 381 | delete_option( 'wc_ppec_display_wc_3_0_warning' ); 382 | return; 383 | } 384 | ?> 385 |
    386 | 387 |

    388 | tag, %2$ closing tag, %3$ WooCommerce version, %4$ tag linking to Plugins screen, %5$ closing tag. */ 392 | __( 393 | '%1$sWarning!%2$s PayPal Checkout will drop support for WooCommerce %3$s in a soon to be released update. To continue using PayPal Checkout please %4$supdate to %1$sWooCommerce 3.0%2$s or greater%5$s.', 394 | 'woocommerce-gateway-paypal-express-checkout' 395 | ), 396 | '', 397 | '', 398 | WC_VERSION, 399 | '', 400 | '' 401 | ); 402 | // phpcs:enable 403 | ?> 404 |

    405 |
    406 | error_code = $error_code; 15 | $this->short_message = $short_message; 16 | $this->long_message = $long_message; 17 | $this->severity_code = $severity_code; 18 | } 19 | 20 | public function mapToBuyerFriendlyError() { 21 | switch ( $this->error_code ) { 22 | // phpcs:disable PSR2.ControlStructures.SwitchDeclaration.BodyOnNextLineCASE 23 | case '-1': return esc_html__( 'Unable to communicate with PayPal. Please try your payment again.', 'woocommerce-gateway-paypal-express-checkout' ); 24 | case '10407': return esc_html__( 'PayPal rejected your email address because it is not valid. Please double-check your email address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 25 | case '10409': 26 | case '10421': 27 | case '10410': return esc_html__( 'Your PayPal checkout session is invalid. Please check out again.', 'woocommerce-gateway-paypal-express-checkout' ); 28 | case '10411': return esc_html__( 'Your PayPal checkout session has expired. Please check out again.', 'woocommerce-gateway-paypal-express-checkout' ); 29 | case '11607': 30 | case '10415': return esc_html__( 'Your PayPal payment has already been completed. Please contact the store owner for more information.', 'woocommerce-gateway-paypal-express-checkout' ); 31 | case '10416': return esc_html__( 'Your PayPal payment could not be processed. Please check out again or contact PayPal for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 32 | case '10417': return esc_html__( 'Your PayPal payment could not be processed. Please select an alternative method of payment or contact PayPal for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 33 | case '10486': 34 | case '10422': return esc_html__( 'Your PayPal payment could not be processed. Please return to PayPal and select a new method of payment.', 'woocommerce-gateway-paypal-express-checkout' ); 35 | case '10485': 36 | case '10435': return esc_html__( 'You have not approved this transaction on the PayPal website. Please check out again and be sure to complete all steps of the PayPal checkout process.', 'woocommerce-gateway-paypal-express-checkout' ); 37 | case '10474': return esc_html__( 'Your shipping address may not be in a different country than your country of residence. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 38 | case '10537': return esc_html__( 'This store does not accept transactions from buyers in your country. Please contact the store owner for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 39 | case '10538': return esc_html__( 'The transaction is over the threshold allowed by this store. Please contact the store owner for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 40 | case '11611': 41 | case '10539': return esc_html__( 'Your transaction was declined. Please contact the store owner for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 42 | case '10725': return esc_html__( 'The country in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 43 | case '10727': return esc_html__( 'The street address in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 44 | case '10728': return esc_html__( 'The city in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 45 | case '10729': return esc_html__( 'The state in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 46 | case '10730': return esc_html__( 'The ZIP code or postal code in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 47 | case '10731': return esc_html__( 'The country in your shipping address is not valid. Please double-check your shipping address and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 48 | case '10736': return esc_html__( 'PayPal rejected your shipping address because the city, state, and/or ZIP code are incorrect. Please double-check that they are all spelled correctly and try again.', 'woocommerce-gateway-paypal-express-checkout' ); 49 | case '13113': 50 | case '11084': return esc_html__( 'Your PayPal payment could not be processed. Please contact PayPal for assistance.', 'woocommerce-gateway-paypal-express-checkout' ); 51 | case '12126': 52 | case '12125': return esc_html__( 'The redemption code(s) you entered on PayPal cannot be used at this time. Please return to PayPal and remove them.', 'woocommerce-gateway-paypal-express-checkout' ); 53 | case '17203': 54 | case '17204': 55 | case '17200': return esc_html__( 'Your funding instrument is invalid. Please check out again and select a new funding source.', 'woocommerce-gateway-paypal-express-checkout' ); 56 | default: 57 | /* Translators: placeholder is an error code. */ 58 | return sprintf( esc_html__( 'An error (%s) occurred while processing your PayPal payment. Please contact the store owner for assistance.', 'woocommerce-gateway-paypal-express-checkout' ), $this->error_code ); 59 | // phpcs:enable 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-client-credential-certificate.php: -------------------------------------------------------------------------------- 1 | _username = $username; 26 | $this->_password = $password; 27 | $this->_certificate = $certificate; 28 | $this->_subject = $subject; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function get_endpoint_subdomain() { 35 | return 'api'; 36 | } 37 | 38 | /** 39 | * Get certificate. 40 | * 41 | * @return string 42 | */ 43 | public function get_certificate() { 44 | return $this->_certificate; 45 | } 46 | 47 | /** 48 | * Allow certificate-based credential to configure cURL, especially 49 | * to set CURLOPT_SSLCERT and CURLOPT_SSLCERTPASSWD. 50 | * 51 | * @throws Exception 52 | * 53 | * @param resource &$handle The cURL handle returned by curl_init(). 54 | * @param array $r The HTTP request arguments. 55 | * @param string $url The request URL. 56 | * 57 | * @return void 58 | */ 59 | public function configure_curl( $handle, $r, $url ) { 60 | parent::configure_curl( $handle, $r, $url ); 61 | 62 | $password = uniqid(); 63 | $certificate_file = $this->_maybe_create_certificate_file( $password ); 64 | 65 | if ( false === curl_setopt( $handle, CURLOPT_SSLCERT, $certificate_file ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt 66 | throw new Exception( esc_html__( 'Unable to accept certificate during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 67 | } 68 | 69 | if ( $this->_use_secure_transport() && false === curl_setopt( $handle, CURLOPT_SSLCERTPASSWD, $password ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt 70 | throw new Exception( esc_html__( 'Unable to accept certificate password during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 71 | } 72 | } 73 | 74 | /** 75 | * Dump the certificate out to a temporary file, because cURL can't accept 76 | * it any other way. 77 | * 78 | * @throws Exception 79 | * 80 | * @param string $password Password for certificate when using secure transport 81 | * 82 | * @return string Filepath of certificate file 83 | */ 84 | protected function _maybe_create_certificate_file( $password ) { 85 | $temp_file = tempnam( sys_get_temp_dir(), 'pptmp_' ); 86 | if ( ! $temp_file ) { 87 | // Translators: %s is a filepath. 88 | throw new Exception( sprintf( esc_html__( 'Unable to write certificate file %s during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), $temp_file ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 89 | } 90 | 91 | if ( $this->_use_secure_transport() ) { 92 | $this->_maybe_create_secure_certificate_file( $temp_file, $password ); 93 | } else { 94 | $this->_maybe_create_non_secure_certificate_file( $temp_file ); 95 | } 96 | 97 | return $temp_file; 98 | } 99 | 100 | /** 101 | * If we're using SecureTransport, we have to translate the certificate to 102 | * PKCS12 before passing it to cURL. 103 | * 104 | * @throws Exception 105 | * 106 | * @param string $temp_file Filepath to temporary certificate file 107 | * 108 | * @return void 109 | */ 110 | protected function _maybe_create_secure_certificate_file( $temp_file, $password ) { 111 | $private_key = openssl_pkey_get_private( $this->_certificate ); 112 | 113 | if ( false === $private_key ) { 114 | throw new Exception( esc_html__( 'Failed to retrieve private key during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 115 | } 116 | 117 | if ( ! openssl_pkcs12_export_to_file( $this->_certificate, $temp_file, $private_key, $password ) ) { 118 | throw new Exception( esc_html__( 'Failed to export PKCS12 file during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 119 | } 120 | } 121 | 122 | /** 123 | * Create non-password certificate file. Basically just dump the certificate 124 | * string to temporary file. 125 | * 126 | * @throws Exception 127 | * 128 | * @param string $temp_file Filepath to temporary certificate file 129 | * 130 | * @return void 131 | */ 132 | protected function _maybe_create_non_secure_certificate_file( $temp_file ) { 133 | if ( false === file_put_contents( $temp_file, $this->_certificate ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents 134 | // Translators: %s is a filepath. 135 | throw new Exception( sprintf( esc_html__( 'Unable to write certificate file %s during cURL configuration', 'woocommerce-gateway-paypal-express-checkout' ), $temp_file ), WC_Gateway_PPEC_Client::INVALID_ENVIRONMENT_ERROR ); 136 | } 137 | } 138 | 139 | /** 140 | * Returns true if secure transport is available in current cURL. 141 | * 142 | * @return bool 143 | */ 144 | protected function _use_secure_transport() { 145 | $curl_version = curl_version(); 146 | return false !== strpos( $curl_version['ssl_version'], 'SecureTransport' ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-client-credential-signature.php: -------------------------------------------------------------------------------- 1 | _username = $username; 26 | $this->_password = $password; 27 | $this->_signature = $signature; 28 | $this->_subject = $subject; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function get_request_params() { 35 | $params = parent::get_request_params(); 36 | $params['SIGNATURE'] = $this->_signature; 37 | 38 | return $params; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function get_endpoint_subdomain() { 45 | return 'api-3t'; 46 | } 47 | 48 | /** 49 | * Get signature. 50 | * 51 | * @return string 52 | */ 53 | public function get_signature() { 54 | return $this->_signature; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-gateway-loader.php: -------------------------------------------------------------------------------- 1 | includes_path; 17 | 18 | require_once $includes_path . 'class-wc-gateway-ppec-refund.php'; 19 | require_once $includes_path . 'abstracts/abstract-wc-gateway-ppec.php'; 20 | 21 | require_once $includes_path . 'class-wc-gateway-ppec-with-paypal.php'; 22 | require_once $includes_path . 'class-wc-gateway-ppec-with-paypal-credit.php'; 23 | require_once $includes_path . 'class-wc-gateway-ppec-with-paypal-addons.php'; 24 | require_once $includes_path . 'class-wc-gateway-ppec-with-spb.php'; 25 | require_once $includes_path . 'class-wc-gateway-ppec-with-spb-addons.php'; 26 | 27 | add_filter( 'woocommerce_payment_gateways', array( $this, 'payment_gateways' ) ); 28 | } 29 | 30 | /** 31 | * Register the PPEC payment methods. 32 | * 33 | * @param array $methods Payment methods. 34 | * 35 | * @return array Payment methods 36 | */ 37 | public function payment_gateways( $methods ) { 38 | $settings = wc_gateway_ppec()->settings; 39 | 40 | if ( 'yes' === $settings->use_spb ) { 41 | if ( $this->can_use_addons() ) { 42 | $methods[] = 'WC_Gateway_PPEC_With_SPB_Addons'; 43 | } else { 44 | $methods[] = 'WC_Gateway_PPEC_With_SPB'; 45 | } 46 | return $methods; 47 | } 48 | 49 | if ( $this->can_use_addons() ) { 50 | $methods[] = 'WC_Gateway_PPEC_With_PayPal_Addons'; 51 | } else { 52 | $methods[] = 'WC_Gateway_PPEC_With_PayPal'; 53 | } 54 | 55 | if ( $settings->is_credit_enabled() ) { 56 | $methods[] = 'WC_Gateway_PPEC_With_PayPal_Credit'; 57 | } 58 | 59 | return $methods; 60 | } 61 | 62 | /** 63 | * Checks whether gateway addons can be used. 64 | * 65 | * @since 1.2.0 66 | * 67 | * @return bool Returns true if gateway addons can be used 68 | */ 69 | public function can_use_addons() { 70 | return ( class_exists( 'WC_Subscriptions_Order' ) && function_exists( 'wcs_create_renewal_order' ) ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-ipn-handler.php: -------------------------------------------------------------------------------- 1 | is_valid_ipn_request( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 33 | wc_gateway_ppec_log( 'IPN request is valid according to PayPal.' ); 34 | do_action( 'woocommerce_paypal_express_checkout_valid_ipn_request', wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing 35 | exit; 36 | } else { 37 | wc_gateway_ppec_log( 'IPN request is NOT valid according to PayPal.' ); 38 | throw new Exception( esc_html__( 'Invalid IPN request.', 'woocommerce-gateway-paypal-express-checkout' ) ); 39 | } 40 | } catch ( Exception $e ) { 41 | wp_die( $e->getMessage(), esc_html__( 'PayPal IPN Request Failure', 'woocommerce-gateway-paypal-express-checkout' ), array( 'response' => 500 ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 42 | } 43 | } 44 | 45 | /** 46 | * Check with PayPal whether posted data is valid IPN request. 47 | * 48 | * @throws Exception 49 | * 50 | * @param array $posted_data Posted data 51 | * @return bool True if posted_data is valid IPN request 52 | */ 53 | public function is_valid_ipn_request( array $posted_data ) { 54 | wc_gateway_ppec_log( sprintf( '%s: %s', __FUNCTION__, 'Checking IPN request validity' ) ); 55 | 56 | $ipn_request = array( 57 | 'cmd' => '_notify-validate', 58 | ); 59 | $ipn_request += wp_unslash( $posted_data ); 60 | 61 | $params = array( 62 | 'body' => $ipn_request, 63 | 'timeout' => 60, 64 | 'httpversion' => '1.1', 65 | 'compress' => false, 66 | 'decompress' => false, 67 | 'user-agent' => get_class( $this->gateway ), 68 | ); 69 | 70 | // Post back to PayPal to check validity of IPN request. 71 | $response = wp_safe_remote_post( $this->get_validator_url(), $params ); 72 | 73 | wc_gateway_ppec_log( sprintf( '%s: %s: %s', __FUNCTION__, 'Verify IPN request', print_r( $params, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 74 | wc_gateway_ppec_log( sprintf( '%s: %s: %s', __FUNCTION__, 'Response for the IPN request', print_r( $response, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 75 | 76 | if ( is_wp_error( $response ) ) { 77 | throw new Exception( $response->get_error_message() ); 78 | } 79 | 80 | return ( 81 | $response['response']['code'] >= 200 82 | && 83 | $response['response']['code'] < 300 84 | && 85 | strstr( $response['body'], 'VERIFIED' ) 86 | ); 87 | } 88 | 89 | /** 90 | * Handle valid IPN request. 91 | * 92 | * @param array $posted_data Posted data 93 | */ 94 | public function handle_valid_ipn( $posted_data ) { 95 | if ( ! empty( $posted_data['custom'] ) && ( $order = $this->get_paypal_order( $posted_data['custom'] ) ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure,WordPress.CodeAnalysis.AssignmentInCondition.Found 96 | // Lowercase returned variables. 97 | $posted_data['payment_status'] = strtolower( $posted_data['payment_status'] ); 98 | 99 | // Sandbox fix. 100 | if ( ( empty( $posted_data['pending_reason'] ) || 'authorization' !== $posted_data['pending_reason'] ) && isset( $posted_data['test_ipn'] ) && 1 == $posted_data['test_ipn'] && 'pending' == $posted_data['payment_status'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison 101 | $posted_data['payment_status'] = 'completed'; 102 | } 103 | 104 | $order_id = version_compare( WC_VERSION, '3.0', '<' ) ? $order->id : $order->get_id(); 105 | wc_gateway_ppec_log( 'Found order #' . $order_id ); 106 | wc_gateway_ppec_log( 'Payment status: ' . $posted_data['payment_status'] ); 107 | 108 | if ( method_exists( $this, 'payment_status_' . $posted_data['payment_status'] ) ) { 109 | call_user_func( array( $this, 'payment_status_' . $posted_data['payment_status'] ), $order, $posted_data ); 110 | } 111 | } else { 112 | wc_gateway_ppec_log( sprintf( '%s: %s', __FUNCTION__, 'No order data being passed' ) ); 113 | } 114 | } 115 | 116 | /** 117 | * Check for a valid transaction type. 118 | * 119 | * @param string $txn_type Transaction type 120 | */ 121 | protected function validate_transaction_type( $txn_type ) { 122 | $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money' ); 123 | if ( ! in_array( strtolower( $txn_type ), $accepted_types, true ) ) { 124 | wc_gateway_ppec_log( 'Aborting, Invalid type:' . $txn_type ); 125 | exit; 126 | } 127 | } 128 | 129 | /** 130 | * Check currency from IPN matches the order. 131 | * 132 | * @param WC_Order $order Order object 133 | * @param string $currency Currency 134 | */ 135 | protected function validate_currency( $order, $currency ) { 136 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 137 | $order_currency = $old_wc ? $order->order_currency : $order->get_currency(); 138 | 139 | if ( $order_currency !== $currency ) { 140 | wc_gateway_ppec_log( 'Payment error: Currencies do not match (sent "' . $order_currency . '" | returned "' . $currency . '")' ); 141 | // Put this order on-hold for manual checking. 142 | // Translators: placeholder is a currency code. 143 | $order->update_status( 'on-hold', sprintf( esc_html__( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce-gateway-paypal-express-checkout' ), $currency ) ); 144 | exit; 145 | } 146 | } 147 | 148 | /** 149 | * Check payment amount from IPN matches the order. 150 | * 151 | * @param WC_Order $order Order object 152 | * @param int $amount Amount 153 | */ 154 | protected function validate_amount( $order, $amount ) { 155 | if ( number_format( $order->get_total(), 2, '.', '' ) != number_format( $amount, 2, '.', '' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison 156 | wc_gateway_ppec_log( 'Payment error: Amounts do not match (gross ' . $amount . ')' ); 157 | // Put this order on-hold for manual checking. 158 | // Translators: placeholder is an amount. 159 | $order->update_status( 'on-hold', sprintf( esc_html__( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce-gateway-paypal-express-checkout' ), $amount ) ); 160 | exit; 161 | } 162 | } 163 | 164 | /** 165 | * Handle a completed payment. 166 | * 167 | * @param WC_Order $order Order object 168 | * @param array $posted_data Posted data 169 | */ 170 | protected function payment_status_completed( $order, $posted_data ) { 171 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 172 | $order_id = $old_wc ? $order->id : $order->get_id(); 173 | 174 | if ( $order->has_status( array( 'completed' ) ) ) { 175 | wc_gateway_ppec_log( 'Aborting, Order #' . $order_id . ' is already complete.' ); 176 | exit; 177 | } 178 | 179 | $this->validate_transaction_type( $posted_data['txn_type'] ); 180 | $this->validate_currency( $order, $posted_data['mc_currency'] ); 181 | $this->validate_amount( $order, $posted_data['mc_gross'] ); 182 | $this->save_paypal_meta_data( $order, $posted_data ); 183 | 184 | if ( 'completed' === $posted_data['payment_status'] ) { 185 | $this->payment_complete( $order, ( ! empty( $posted_data['txn_id'] ) ? wc_clean( $posted_data['txn_id'] ) : '' ), esc_html__( 'IPN payment completed', 'woocommerce-gateway-paypal-express-checkout' ) ); 186 | if ( ! empty( $posted_data['mc_fee'] ) ) { 187 | // Log paypal transaction fee. 188 | wc_gateway_ppec_set_transaction_fee( $order, $posted_data['mc_fee'] ); 189 | } 190 | } else { 191 | if ( 'authorization' === $posted_data['pending_reason'] ) { 192 | $this->payment_on_hold( $order, esc_html__( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce-gateway-paypal-express-checkout' ) ); 193 | } else { 194 | // Translators: placeholder is the reason for the payment to be in pending status. 195 | $this->payment_on_hold( $order, sprintf( esc_html__( 'Payment pending (%s).', 'woocommerce-gateway-paypal-express-checkout' ), $posted_data['pending_reason'] ) ); 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Handle a pending payment. 202 | * 203 | * @param WC_Order $order Order object 204 | * @param array $posted_data Posted data 205 | */ 206 | protected function payment_status_pending( $order, $posted_data ) { 207 | $this->payment_status_completed( $order, $posted_data ); 208 | } 209 | 210 | /** 211 | * Handle a failed payment. 212 | * 213 | * @param WC_Order $order Order object 214 | * @param array $posted_data Posted data 215 | */ 216 | protected function payment_status_failed( $order, $posted_data ) { 217 | // Translators: placeholder is a payment status. 218 | $order->update_status( 'failed', sprintf( esc_html__( 'Payment %s via IPN.', 'woocommerce-gateway-paypal-express-checkout' ), wc_clean( $posted_data['payment_status'] ) ) ); 219 | } 220 | 221 | /** 222 | * Handle a denied payment. 223 | * 224 | * @param WC_Order $order Order object 225 | * @param array $posted_data Posted data 226 | */ 227 | protected function payment_status_denied( $order, $posted_data ) { 228 | $this->payment_status_failed( $order, $posted_data ); 229 | } 230 | 231 | /** 232 | * Handle an expired payment. 233 | * 234 | * @param WC_Order $order Order object 235 | * @param array $posted_data Posted data 236 | */ 237 | protected function payment_status_expired( $order, $posted_data ) { 238 | $this->payment_status_failed( $order, $posted_data ); 239 | } 240 | 241 | /** 242 | * Handle a voided payment. 243 | * 244 | * @param WC_Order $order Order object 245 | * @param array $posted_data Posted data 246 | */ 247 | protected function payment_status_voided( $order, $posted_data ) { 248 | $this->payment_status_failed( $order, $posted_data ); 249 | } 250 | 251 | /** 252 | * Handle a refunded order. 253 | * 254 | * @param WC_Order $order Order object 255 | * @param array $posted_data Posted data 256 | */ 257 | protected function payment_status_refunded( $order, $posted_data ) { 258 | // Only handle full refunds, not partial. 259 | $order_id = version_compare( WC_VERSION, '3.0', '<' ) ? $order->id : $order->get_id(); 260 | if ( $order->get_total() == ( $posted_data['mc_gross'] * -1 ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison 261 | // Mark order as refunded. 262 | // Translators: placeholder is a payment status. 263 | $order->update_status( 'refunded', sprintf( esc_html__( 'Payment %s via IPN.', 'woocommerce-gateway-paypal-express-checkout' ), strtolower( $posted_data['payment_status'] ) ) ); 264 | $this->send_ipn_email_notification( 265 | /* Translators: placeholder is an order number (linked to its details screen). */ 266 | sprintf( esc_html__( 'Payment for order %s refunded', 'woocommerce-gateway-paypal-express-checkout' ), '' . $order->get_order_number() . '' ), 267 | /* Translators: 1) is an order number, 2) is a PayPal reason code. */ 268 | sprintf( esc_html__( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce-gateway-paypal-express-checkout' ), $order->get_order_number(), $posted_data['reason_code'] ) 269 | ); 270 | } 271 | } 272 | 273 | /** 274 | * Handle a reveral. 275 | * 276 | * @param WC_Order $order Order object 277 | * @param array $posted_data Posted data 278 | */ 279 | protected function payment_status_reversed( $order, $posted_data ) { 280 | $order_id = version_compare( WC_VERSION, '3.0', '<' ) ? $order->id : $order->get_id(); 281 | // Translators: placeholder is a payment status. 282 | $order->update_status( 'on-hold', sprintf( esc_html__( 'Payment %s via IPN.', 'woocommerce-gateway-paypal-express-checkout' ), wc_clean( $posted_data['payment_status'] ) ) ); 283 | $this->send_ipn_email_notification( 284 | /* Translators: placeholder is an order number (linked to its details screen). */ 285 | sprintf( esc_html__( 'Payment for order %s reversed', 'woocommerce-gateway-paypal-express-checkout' ), '' . $order->get_order_number() . '' ), 286 | /* Translators: 1) is an order number, 2) is a PayPal reason code. */ 287 | sprintf( esc_html__( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'woocommerce-gateway-paypal-express-checkout' ), $order->get_order_number(), wc_clean( $posted_data['reason_code'] ) ) 288 | ); 289 | } 290 | 291 | /** 292 | * Handle a cancelled reveral. 293 | * 294 | * @param WC_Order $order Order object 295 | * @param array $posted_data Posted data 296 | */ 297 | protected function payment_status_canceled_reversal( $order, $posted_data ) { 298 | $order_id = version_compare( WC_VERSION, '3.0', '<' ) ? $order->id : $order->get_id(); 299 | $this->send_ipn_email_notification( 300 | /* Translators: placeholder is an order number. */ 301 | sprintf( esc_html__( 'Reversal cancelled for order #%s', 'woocommerce-gateway-paypal-express-checkout' ), $order->get_order_number() ), 302 | /* Translators: 1) is an order number, 2) is the URL of the order's edit screen. */ 303 | sprintf( __( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'woocommerce-gateway-paypal-express-checkout' ), $order->get_order_number(), esc_url( admin_url( 'post.php?post=' . $order_id . '&action=edit' ) ) ) 304 | ); 305 | } 306 | 307 | /** 308 | * Save important data from the IPN to the order. 309 | * 310 | * @param WC_Order $order Order object 311 | * @param array $posted_data Posted data 312 | */ 313 | protected function save_paypal_meta_data( $order, $posted_data ) { 314 | 315 | // A map of PayPal $POST keys to order meta keys 316 | $mapped_keys = array( 317 | 'payer_email' => 'Payer PayPal address', 318 | 'first_name' => 'Payer first name', 319 | 'last_name' => 'Payer last name', 320 | 'payment_type' => 'Payment type', 321 | 'payment_status' => '_paypal_status', 322 | ); 323 | 324 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 325 | foreach ( $mapped_keys as $post_key => $meta_key ) { 326 | if ( ! empty( $posted_data[ $post_key ] ) ) { 327 | $value = wc_clean( $posted_data[ $post_key ] ); 328 | if ( $old_wc ) { 329 | update_post_meta( $order->id, $meta_key, $value ); 330 | } else { 331 | $order->update_meta_data( $meta_key, $value ); 332 | } 333 | } 334 | } 335 | 336 | if ( ! $old_wc ) { 337 | $order->save_meta_data(); 338 | } 339 | 340 | if ( ! empty( $posted_data['txn_id'] ) ) { 341 | update_post_meta( $old_wc ? $order->id : $order->get_id(), '_transaction_id', wc_clean( $posted_data['txn_id'] ) ); 342 | } 343 | } 344 | 345 | /** 346 | * Send a notification to the user handling orders. 347 | * 348 | * @param string $subject Email subject 349 | * @param string $message Email message 350 | */ 351 | protected function send_ipn_email_notification( $subject, $message ) { 352 | $new_order_settings = get_option( 'woocommerce_new_order_settings', array() ); 353 | $mailer = WC()->mailer(); 354 | $message = $mailer->wrap_message( $subject, $message ); 355 | $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), wp_strip_all_tags( $subject ), $message ); 356 | } 357 | 358 | /** 359 | * Get IPN request validator URL. 360 | * 361 | * @return string PayPal IPN request validator URL 362 | */ 363 | public function get_validator_url() { 364 | $url = 'https://www.paypal.com/cgi-bin/webscr'; 365 | if ( 'sandbox' === $this->gateway->environment ) { 366 | $url = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; 367 | } 368 | 369 | return $url; 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-ips-handler.php: -------------------------------------------------------------------------------- 1 | $env, 58 | 'wc_ppec_ips_admin_nonce' => wp_create_nonce( 'wc_ppec_ips' ), 59 | ), 60 | wc_gateway_ppec()->get_admin_setting_link() 61 | ), 62 | null, 63 | 'db' 64 | ); 65 | } 66 | 67 | /** 68 | * Get login URL to WC middleware. 69 | * 70 | * @param string $env Environment 71 | * 72 | * @return string Signup URL 73 | */ 74 | public function get_middleware_login_url( $env ) { 75 | $service = 'ppe'; 76 | if ( 'sandbox' === $env ) { 77 | $service = 'ppesandbox'; 78 | } 79 | 80 | return self::MIDDLEWARE_BASE_URL . '/login/' . $service; 81 | } 82 | 83 | /** 84 | * Get signup URL to WC middleware. 85 | * 86 | * @param string $env Environment 87 | * 88 | * @return string Signup URL 89 | */ 90 | public function get_signup_url( $env ) { 91 | $query_args = array( 92 | 'redirect' => urlencode( $this->get_redirect_url( $env ) ), /* phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode */ 93 | 'countryCode' => WC()->countries->get_base_country(), 94 | 'merchantId' => md5( site_url( '/' ) . time() ), 95 | ); 96 | 97 | return esc_url( add_query_arg( $query_args, $this->get_middleware_login_url( $env ) ) ); 98 | } 99 | 100 | /** 101 | * Check if base location country supports IPS. 102 | * 103 | * @return bool Returns true of base country in supported countries 104 | */ 105 | public function is_supported() { 106 | return in_array( WC()->countries->get_base_country(), $this->_supported_countries, true ); 107 | } 108 | 109 | /** 110 | * Redirect with messages. 111 | * 112 | * @return void 113 | */ 114 | protected function _redirect_with_messages( $error_msg ) { 115 | if ( ! is_array( $error_msg ) ) { 116 | $error_msgs = array( array( 'error' => $error_msg ) ); 117 | } else { 118 | $error_msgs = $error_msg; 119 | } 120 | 121 | add_option( 'woo_pp_admin_error', $error_msgs ); 122 | wp_safe_redirect( wc_gateway_ppec()->get_admin_setting_link() ); 123 | exit; 124 | } 125 | 126 | /** 127 | * Maybe received credentials after successfully returned from IPS flow. 128 | * 129 | * @return mixed 130 | */ 131 | public function maybe_received_credentials() { 132 | if ( ! is_admin() || ! is_user_logged_in() ) { 133 | return false; 134 | } 135 | 136 | // Require the nonce. 137 | if ( empty( $_GET['wc_ppec_ips_admin_nonce'] ) || empty( $_GET['env'] ) ) { 138 | return false; 139 | } 140 | $env = in_array( $_GET['env'], array( 'live', 'sandbox' ), true ) ? $_GET['env'] : 'live'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 141 | 142 | // Verify the nonce. 143 | if ( ! wp_verify_nonce( $_GET['wc_ppec_ips_admin_nonce'], 'wc_ppec_ips' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 144 | wp_die( esc_html__( 'Invalid connection request', 'woocommerce-gateway-paypal-express-checkout' ) ); 145 | } 146 | 147 | wc_gateway_ppec_log( sprintf( '%s: returned back from IPS flow with parameters: %s', __METHOD__, print_r( $_GET, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 148 | 149 | // Check if error. 150 | if ( ! empty( $_GET['error'] ) ) { 151 | $error_message = ! empty( $_GET['error_message'] ) ? $_GET['error_message'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 152 | wc_gateway_ppec_log( sprintf( '%s: returned back from IPS flow with error: %s', __METHOD__, $error_message ) ); 153 | 154 | $this->_redirect_with_messages( esc_html__( 'Sorry, Easy Setup encountered an error. Please try again.', 'woocommerce-gateway-paypal-express-checkout' ) ); 155 | } 156 | 157 | // Make sure credentials present in query string. 158 | foreach ( array( 'api_style', 'api_username', 'api_password', 'signature' ) as $param ) { 159 | if ( empty( $_GET[ $param ] ) ) { 160 | wc_gateway_ppec_log( sprintf( '%s: returned back from IPS flow but missing parameter %s', __METHOD__, $param ) ); 161 | 162 | $this->_redirect_with_messages( esc_html__( 'Sorry, Easy Setup encountered an error. Please try again.', 'woocommerce-gateway-paypal-express-checkout' ) ); 163 | } 164 | } 165 | 166 | $creds = new WC_Gateway_PPEC_Client_Credential_Signature( $_GET['api_username'], $_GET['api_password'], $_GET['signature'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 167 | 168 | $error_msgs = array(); 169 | try { 170 | $payer_id = wc_gateway_ppec()->client->test_api_credentials( $creds, $env ); 171 | 172 | if ( ! $payer_id ) { 173 | $this->_redirect_with_messages( esc_html__( 'Easy Setup was able to obtain your API credentials, but was unable to verify that they work correctly. Please make sure your PayPal account is set up properly and try Easy Setup again.', 'woocommerce-gateway-paypal-express-checkout' ) ); 174 | } 175 | } catch ( PayPal_API_Exception $ex ) { 176 | $error_msgs[] = array( 177 | 'warning' => esc_html__( 'Easy Setup was able to obtain your API credentials, but an error occurred while trying to verify that they work correctly. Please try Easy Setup again.', 'woocommerce-gateway-paypal-express-checkout' ), 178 | ); 179 | } 180 | 181 | $error_msgs[] = array( 182 | 'success' => esc_html__( 'Success! Your PayPal account has been set up successfully.', 'woocommerce-gateway-paypal-express-checkout' ), 183 | ); 184 | 185 | if ( ! empty( $error_msgs ) ) { 186 | wc_gateway_ppec_log( sprintf( '%s: returned back from IPS flow: %s', __METHOD__, print_r( $error_msgs, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r 187 | } 188 | 189 | // Save credentials to settings API 190 | $settings_array = (array) get_option( 'woocommerce_ppec_paypal_settings', array() ); 191 | 192 | if ( 'live' === $env ) { 193 | $settings_array['environment'] = 'live'; 194 | $settings_array['api_username'] = $creds->get_username(); 195 | $settings_array['api_password'] = $creds->get_password(); 196 | $settings_array['api_signature'] = is_callable( array( $creds, 'get_signature' ) ) ? $creds->get_signature() : ''; 197 | $settings_array['api_certificate'] = is_callable( array( $creds, 'get_certificate' ) ) ? $creds->get_certificate() : ''; 198 | $settings_array['api_subject'] = $creds->get_subject(); 199 | } else { 200 | $settings_array['environment'] = 'sandbox'; 201 | $settings_array['sandbox_api_username'] = $creds->get_username(); 202 | $settings_array['sandbox_api_password'] = $creds->get_password(); 203 | $settings_array['sandbox_api_signature'] = is_callable( array( $creds, 'get_signature' ) ) ? $creds->get_signature() : ''; 204 | $settings_array['sandbox_api_certificate'] = is_callable( array( $creds, 'get_certificate' ) ) ? $creds->get_certificate() : ''; 205 | $settings_array['sandbox_api_subject'] = $creds->get_subject(); 206 | } 207 | 208 | update_option( 'woocommerce_ppec_paypal_settings', $settings_array ); 209 | 210 | $this->_redirect_with_messages( $error_msgs ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-payment-details.php: -------------------------------------------------------------------------------- 1 | 'token', 25 | 'BILLINGAGREEMENTID' => 'billing_agreement_id', 26 | 'REDIRECTREQUIRED' => 'redirect_required', 27 | 'SUCCESSPAGEREDIRECTREQUESTED' => 'redirect_requested', 28 | 'NOTE' => 'note', 29 | ); 30 | 31 | $max_payment_num = -1; 32 | 33 | foreach ( $doECResponse as $index => $value ) { 34 | if ( array_key_exists( $index, $map ) ) { 35 | $key = $map[ $index ]; 36 | $this->$key = $value; 37 | } 38 | // Figure out the highest payment number 39 | if ( preg_match( '/^PAYMENTINFO_(\d)_(TRANSACTIONID|EBAYITEMAUCTIONTXNID|PARENTTRANSACTIONID|RECEIPTID|TRANSACTIONTYPE|PAYMENTTYPE|EXPECTEDECHECKCLEARDATE|ORDERTIME|AMT|CURRENCYCODE|FEEAMT|SETTLEAMT|TAXAMT|EXCHANGERATE|PAYMENTSTATUS|PENDINGREASON|REASONCODE|HOLDDECISION|SHIPPINGMETHOD|PROTECTIONELIGIBILITY|PROTECTIONELIGIBILITYTYPE|RECEIPTREFERENCENUMBER|SHIPPINGAMT|HANDLINGAMT|PAYMENTREQUESTID|INSTRUMENTCATEGORY|INSTRUMENTID|OFFERCODE|OFFERTRACKINGID|SHORTMESSAGE|LONGMESSAGE|ERRORCODE|SEVERITYCODE|ACK|SELLERPAYPALACCOUNTID|SECUREMERCHANTACCOUNTID|SELLERID|SELLERUSERNAME|SELLERREGISTRATIONDATE)$/', $index, $matches ) ) { 40 | if ( $matches[1] > $max_payment_num ) { 41 | $max_payment_num = $matches[1]; 42 | } 43 | } 44 | } 45 | 46 | if ( $max_payment_num >= 0 ) { 47 | $this->payments = array(); 48 | for ( $i = 0; $i <= $max_payment_num; $i++ ) { 49 | $this->payments[ $i ] = new PayPal_Payment_Payment_Details(); 50 | $this->payments[ $i ]->loadFromDoECResponse( $doECResponse, $i ); 51 | } 52 | } 53 | 54 | $this->shipping_option_details = new PayPal_Payment_Shipping_Option_Details(); 55 | if ( ! $this->shipping_option_details->loadFromDoECResponse( $doECResponse ) ) { 56 | $this->shipping_option_details = false; 57 | } 58 | 59 | } 60 | } 61 | 62 | class PayPal_Payment_Payment_FMF_Details { 63 | public $filters = false; 64 | 65 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 66 | $max_filter_num = array( 67 | 'PENDING' => -1, 68 | 'REPORT' => -1, 69 | 'DENY' => -1, 70 | 'ACCEPT' => -1, 71 | ); 72 | 73 | $found_any = false; 74 | foreach ( $doECResponse as $index => $value ) { 75 | if ( preg_match( '/^L_PAYMENTINFO_' . $bucketNum . '_FMF(PENDING|REPORT|DENY|ACCEPT)(ID|NAME)(\d+)$/', $index, $matches ) ) { 76 | $found_any = true; 77 | if ( $matches[3] > $max_filter_num[ $matches[1] ] ) { 78 | $max_filter_num[ $matches[1] ] = $matches[3]; 79 | } 80 | } 81 | } 82 | 83 | // If we didn't find anything in the initial scan, bail out now. 84 | if ( ! $found_any ) { 85 | return false; 86 | } 87 | 88 | $this->filters = array(); 89 | foreach ( $max_filter_num as $index => $value ) { 90 | for ( $i = 0; $i <= $value; $i++ ) { 91 | $prefix = 'L_PAYMENTINFO_' . $bucketNum . '_FMF' . $index; 92 | if ( array_key_exists( $prefix . 'NAME' . $i, $doECResponse ) && array_key_exists( $prefix . 'ID' . $i, $doECResponse ) ) { 93 | $filters[] = new PayPal_Payment_Fraud_Management_Filter( $doECResponse[ $prefix . 'NAME' . $i ], $doECResponse[ $prefix . 'ID' . $i ], $index ); 94 | } 95 | } 96 | } 97 | 98 | return true; 99 | } 100 | } 101 | 102 | class PayPal_Payment_Fraud_Management_Filter { 103 | public $name; 104 | public $id; 105 | public $status; 106 | 107 | const FraudManagementFilterPending = 'PENDING'; 108 | const FraudManagementFilterReport = 'REPORT'; 109 | const FraudManagementFilterDeny = 'DENY'; 110 | const FraudManagementFilterAccept = 'ACCEPT'; 111 | 112 | public function __construct( $name, $id, $status ) { 113 | $this->name = $name; 114 | $this->id = $id; 115 | $this->status = $status; 116 | } 117 | } 118 | 119 | class PayPal_Payment_Shipping_Option_Details { 120 | public $calculation_mode = false; 121 | public $insurance_option_selected = false; 122 | public $shipping_option_is_default = false; 123 | public $shipping_option_amount = false; 124 | public $shipping_option_name = false; 125 | 126 | public function loadFromDoECResponse( $doECResponse ) { 127 | $map = array( 128 | 'SHIPPINGCALCULATIONMODE' => 'calculation_mode', 129 | 'INSURANCEOPTIONSELECTED' => 'insurance_option_selected', 130 | 'SHIPPINGOPTIONISDEFAULT' => 'shipping_option_is_default', 131 | 'SHIPPINGOPTIONAMOUNT' => 'shipping_option_amount', 132 | 'SHIPPINGOPTIONNAME' => 'shipping_option_name', 133 | ); 134 | 135 | $found_any = false; 136 | foreach ( $map as $index => $value ) { 137 | if ( array_key_exists( $index, $doECResponse ) ) { 138 | $this->$value = $doECResponse[ $index ]; 139 | $found_any = true; 140 | } 141 | } 142 | 143 | return $found_any; 144 | } 145 | } 146 | 147 | class PayPal_Payment_Payment_Details { 148 | public $transaction_id = false; 149 | public $ebay_item_auction_transaction_id = false; 150 | public $parent_transaction_id = false; 151 | public $receipt_id = false; 152 | public $transaction_type = false; 153 | 154 | const TransactionTypeCart = 'cart'; 155 | const TransactionTypeExpressCheckout = 'express-checkout'; 156 | 157 | public $payment_type = false; 158 | 159 | const PaymentTypeNone = 'none'; 160 | const PaymentTypeEcheck = 'echeck'; 161 | const PaymentTypeInstant = 'instant'; 162 | 163 | public $expected_echeck_clear_date = false; 164 | public $order_time = false; 165 | public $amount = false; 166 | public $currency_code = false; 167 | public $fee_amount = false; 168 | public $settlement_amount = false; 169 | public $tax_amount = false; 170 | public $exchange_rate = false; 171 | public $payment_status = false; 172 | 173 | const PaymentStatusNone = 'None'; 174 | const PaymentStatusCanceledReversal = 'Canceled-Reversal'; 175 | const PaymentStatusCompleted = 'Completed'; 176 | const PaymentStatusDenied = 'Denied'; 177 | const PaymentStatusExpired = 'Expired'; 178 | const PaymentStatusFailed = 'Failed'; 179 | const PaymentStatusInProgress = 'In-Progress'; 180 | const PaymentStatusPartiallyRefunded = 'Partially-Refunded'; 181 | const PaymentStatusPending = 'Pending'; 182 | const PaymentStatusRefunded = 'Refunded'; 183 | const PaymentStatusReversed = 'Reversed'; 184 | const PaymentStatusProcessed = 'Processed'; 185 | const PaymentStatusVoided = 'Voided'; 186 | const PaymentStatusCompletedFundsHeld = 'Completed-Funds-Held'; 187 | 188 | public $pending_reason = false; 189 | 190 | const PendingReasonNone = 'none'; 191 | const PendingReasonAddress = 'address'; 192 | const PendingReasonAuthorization = 'authorization'; 193 | const PendingReasonEcheck = 'echeck'; 194 | const PendingReasonInternational = 'intl'; 195 | const PendingReasonMultiCurrency = 'multi-currency'; 196 | const PendingReasonOrder = 'order'; 197 | const PendingReasonPaymentReview = 'payment-review'; 198 | const PendingReasonRegulatoryReview = 'regulatory-review'; 199 | const PendingReasonUnilateral = 'unilateral'; 200 | const PendingReasonVerify = 'verify'; 201 | const PendingReasonOther = 'other'; 202 | 203 | public $reason_code = false; 204 | 205 | const ReasonCodeNone = 'none'; 206 | const ReasonCodeChargeback = 'chargeback'; 207 | const ReasonCodeGuarantee = 'guarantee'; 208 | const ReasonCodeBuyerComplaint = 'buyer-complaint'; 209 | const ReasonCodeRefund = 'refund'; 210 | const ReasonCodeOther = 'other'; 211 | 212 | public $hold_decision = false; 213 | 214 | const HoldDecisionNewSellerPaymentHold = 'newsellerpaymenthold'; 215 | const HoldDecisionPaymentHold = 'paymenthold'; 216 | 217 | public $shipping_method = false; 218 | 219 | public $protection_eligibility_details = false; 220 | public $receipt_reference_number = false; 221 | public $shipping_amount = false; 222 | 223 | public $handling_amount = false; 224 | 225 | public $payment_request_id = false; 226 | public $instrument_details = false; 227 | 228 | public $offer_details = false; 229 | public $error_details = false; 230 | public $seller_details = false; 231 | public $fmf_details = false; 232 | 233 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 234 | $map = array( 235 | 'TRANSACTIONID' => 'transaction_id', 236 | 'EBAYITEMAUCTIONTXNID' => 'ebay_item_auction_transaction_id', 237 | 'PARENTTRANSACTIONID' => 'parent_transaction_id', 238 | 'RECEIPTID' => 'receipt_id', 239 | 'TRANSACTIONTYPE' => 'transaction_type', 240 | 'PAYMENTTYPE' => 'payment_type', 241 | 'EXPECTEDECHECKCLEARDATE' => 'expected_echeck_clear_date', 242 | 'ORDERTIME' => 'order_time', 243 | 'AMT' => 'amount', 244 | 'CURRENCYCODE' => 'currency_code', 245 | 'FEEAMT' => 'fee_amount', 246 | 'SETTLEAMT' => 'settlement_amount', 247 | 'TAXAMT' => 'tax_amount', 248 | 'EXCHANGERATE' => 'exchange_rate', 249 | 'PAYMENTSTATUS' => 'payment_status', 250 | 'PENDINGREASON' => 'pending_reason', 251 | 'REASONCODE' => 'reason_code', 252 | 'HOLDDECISION' => 'hold_decision', 253 | 'SHIPPINGMETHOD' => 'shipping_method', 254 | 'RECEIPTREFERENCENUMBER' => 'receipt_reference_number', 255 | 'SHIPPINGAMT' => 'shipping_amount', 256 | 'HANDLINGAMT' => 'handling_amount', 257 | 'PAYMENTREQUESTID' => 'payment_request_id', 258 | ); 259 | 260 | $found_any = false; 261 | foreach ( $map as $index => $value ) { 262 | $var_name = 'PAYMENTINFO_' . $bucketNum . '_' . $index; 263 | if ( array_key_exists( $var_name, $doECResponse ) ) { 264 | $this->$value = $doECResponse[ $var_name ]; 265 | $found_any = true; 266 | } 267 | } 268 | 269 | $this->protection_eligibility_details = new PayPal_Payment_Payment_Protection_Eligibility_Details(); 270 | if ( ! $this->protection_eligibility_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 271 | $this->protection_eligibility_details = false; 272 | } 273 | 274 | $this->instrument_details = new PayPal_Payment_Payment_Instrument_Details(); 275 | if ( ! $this->instrument_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 276 | $this->instrument_details = false; 277 | } 278 | 279 | $this->offer_details = new PayPal_Payment_Payment_Offer_Details(); 280 | if ( ! $this->offer_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 281 | $this->offer_details = false; 282 | } 283 | 284 | $this->error_details = new PayPal_Payment_Payment_Error_Details(); 285 | if ( ! $this->error_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 286 | $this->error_details = false; 287 | } 288 | 289 | $this->seller_details = new PayPal_Payment_Payment_Seller_Details(); 290 | if ( ! $this->seller_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 291 | $this->seller_details = false; 292 | } 293 | 294 | $this->fmf_details = new PayPal_Payment_Payment_FMF_Details(); 295 | if ( ! $this->fmf_details->loadFromDoECResponse( $doECResponse, $bucketNum ) ) { 296 | $this->fmf_details = false; 297 | } 298 | } 299 | 300 | } 301 | 302 | class PayPal_Payment_Payment_Protection_Eligibility_Details { 303 | public $protection_eligibility = false; 304 | 305 | const ProtectionEligibilityEligible = 'Eligible'; 306 | const ProtectionEligibilityPartiallyEligible = 'PartiallyEligible'; 307 | const ProtectionEligibilityIneligible = 'Ineligible'; 308 | 309 | public $protection_eligibility_type = false; 310 | 311 | const ProtectionEligibilityTypeItemNotReceivedEligible = 'ItemNotReceivedEligible'; 312 | const ProtectionEligibilityTypeUnauthorizedPaymentEligible = 'UnauthorizedPaymentEligible'; 313 | const ProtectionEligibilityTypeIneligible = 'Ineligible'; 314 | 315 | public function isItemNotReceivedEligible() { 316 | $types = explode( ',', $this->protection_eligibility_type ); 317 | foreach ( $types as $value ) { 318 | if ( self::ProtectionEligibilityTypeItemNotReceivedEligible == $value ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison 319 | return true; 320 | } 321 | } 322 | return false; 323 | } 324 | 325 | public function isUnauthorizedPaymentEligible() { 326 | $types = explode( ',', $this->protection_eligibility_type ); 327 | foreach ( $types as $value ) { 328 | if ( self::ProtectionEligibilityTypeUnauthorizedPaymentEligible == $value ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison 329 | return true; 330 | } 331 | } 332 | return false; 333 | } 334 | 335 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 336 | $map = array( 337 | 'PROTECTIONELIGIBILITY' => 'protection_eligibility', 338 | 'PROTECTIONELIGIBILITYTYPE' => 'protection_eligibility_type', 339 | ); 340 | 341 | $found_any = false; 342 | 343 | foreach ( $map as $index => $value ) { 344 | $var_name = 'PAYMENTINFO_' . $bucketNum . '_' . $index; 345 | if ( array_key_exists( $var_name, $doECResponse ) ) { 346 | $this->$value = $doECResponse[ $var_name ]; 347 | $found_any = true; 348 | } 349 | } 350 | 351 | return $found_any; 352 | } 353 | } 354 | 355 | class PayPal_Payment_Payment_Instrument_Details { 356 | public $instrument_category = false; 357 | 358 | const InstrumentCategoryPayPalCredit = '1'; 359 | const InstrumentCategoryPrivateCard = '2'; 360 | 361 | public $instrument_id = false; 362 | 363 | // Returns true to indicate that the getECResponse array contained variables that were pertinent to this object. 364 | // If not, it returns false to indicate that the caller can destroy this object. 365 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 366 | $map = array( 367 | 'INSTRUMENTCATEGORY' => 'instrument_category', 368 | 'INSTRUMENTID' => 'instrument_id', 369 | ); 370 | $found_any = false; 371 | 372 | foreach ( $map as $index => $value ) { 373 | $var_name = 'PAYMENTINFO_' . $bucketNum . '_' . $index; 374 | if ( array_key_exists( $var_name, $doECResponse ) ) { 375 | $this->$value = $doECResponse[ $var_name ]; 376 | $found_any = true; 377 | } 378 | } 379 | 380 | return $found_any; 381 | } 382 | } 383 | 384 | class PayPal_Payment_Payment_Offer_Details { 385 | public $offer_code = false; 386 | public $offer_tracking_id = false; 387 | 388 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 389 | $map = array( 390 | 'OFFERCODE' => 'offer_code', 391 | 'OFFERTRACKINGID' => 'offer_tracking_id', 392 | ); 393 | } 394 | } 395 | 396 | class PayPal_Payment_Payment_Error_Details { 397 | public $short_message = false; 398 | public $long_message = false; 399 | public $error_code = false; 400 | public $severity_code = false; 401 | public $ack = false; 402 | 403 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 404 | $map = array( 405 | 'SHORTMESSAGE' => 'short_message', 406 | 'LONGMESSAGE' => 'long_message', 407 | 'ERRORCODE' => 'error_code', 408 | 'SEVERITYCODE' => 'severity_code', 409 | 'ACK' => 'ack', 410 | ); 411 | 412 | $found_any = false; 413 | foreach ( $map as $index => $value ) { 414 | $var_name = 'PAYMENTINFO_' . $bucketNum . '_' . $index; 415 | if ( array_key_exists( $var_name, $doECResponse ) ) { 416 | $this->$value = $doECResponse[ $var_name ]; 417 | $found_any = true; 418 | } 419 | } 420 | 421 | return $found_any; 422 | } 423 | } 424 | 425 | class PayPal_Payment_Payment_Seller_Details { 426 | public $paypal_account_id = false; 427 | public $secure_merchant_account_id = false; 428 | public $seller_id = false; 429 | public $user_name = false; 430 | public $registration_date = false; 431 | 432 | public function loadFromDoECResponse( $doECResponse, $bucketNum ) { 433 | $map = array( 434 | 'SELLERPAYPALACCOUNTID' => 'paypal_account_id', 435 | 'SECUREMERCHANTACCOUNTID' => 'secure_merchant_account_id', 436 | 'SELLERID' => 'seller_id', 437 | 'SELLERUSERNAME' => 'user_name', 438 | 'SELLERREGISTRATIONDATE' => 'registration_date', 439 | ); 440 | 441 | $found_any = false; 442 | foreach ( $map as $index => $value ) { 443 | $var_name = 'PAYMENTINFO_' . $bucketNum . '_' . $index; 444 | if ( array_key_exists( $var_name, $doECResponse ) ) { 445 | $this->$value = $doECResponse[ $var_name ]; 446 | $found_any = true; 447 | } 448 | } 449 | 450 | return $found_any; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-privacy.php: -------------------------------------------------------------------------------- 1 | add_exporter( 'woocommerce-gateway-paypal-express-checkout-order-data', __( 'WooCommerce PPEC Order Data', 'woocommerce-gateway-paypal-express-checkout' ), array( $this, 'order_data_exporter' ) ); 15 | if ( class_exists( 'WC_Subscriptions' ) ) { 16 | $this->add_exporter( 'woocommerce-gateway-paypal-express-checkout-subscriptions-data', __( 'WooCommerce PPEC Subscriptions Data', 'woocommerce-gateway-paypal-express-checkout' ), array( $this, 'subscriptions_data_exporter' ) ); 17 | } 18 | 19 | $this->add_eraser( 'woocommerce-gateway-paypal-express-checkout-order-data', __( 'WooCommerce PPEC Data', 'woocommerce-gateway-paypal-express-checkout' ), array( $this, 'order_data_eraser' ) ); 20 | } 21 | 22 | /** 23 | * Returns a list of orders that are using one of PPEC's payment methods. 24 | * 25 | * @param string $email_address 26 | * @param int $page 27 | * 28 | * @return array WP_Post 29 | */ 30 | protected function get_ppec_orders( $email_address, $page ) { 31 | $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. 32 | 33 | $order_query = array( 34 | 'payment_method' => array( 'ppec_paypal' ), 35 | 'limit' => 10, 36 | 'page' => $page, 37 | ); 38 | 39 | if ( $user instanceof WP_User ) { 40 | $order_query['customer_id'] = (int) $user->ID; 41 | } else { 42 | $order_query['billing_email'] = $email_address; 43 | } 44 | 45 | return wc_get_orders( $order_query ); 46 | } 47 | 48 | /** 49 | * Gets the message of the privacy to display. 50 | * 51 | */ 52 | public function get_privacy_message() { 53 | return wpautop( 54 | sprintf( 55 | /* translators: 1: anchor tag 2: closing anchor tag */ 56 | esc_html__( 57 | 'By using this extension, you may be storing personal data or sharing data with an external service. %1$sLearn more about how this works, including what you may want to include in your privacy policy.%2$s', 'woocommerce-gateway-paypal-express-checkout' 58 | ), 59 | '', 60 | '' 61 | ) 62 | ); 63 | } 64 | 65 | /** 66 | * Handle exporting data for Orders. 67 | * 68 | * @param string $email_address E-mail address to export. 69 | * @param int $page Pagination of data. 70 | * 71 | * @return array 72 | */ 73 | public function order_data_exporter( $email_address, $page = 1 ) { 74 | $done = false; 75 | $data_to_export = array(); 76 | 77 | $orders = $this->get_ppec_orders( $email_address, (int) $page ); 78 | 79 | $done = true; 80 | 81 | if ( 0 < count( $orders ) ) { 82 | foreach ( $orders as $order ) { 83 | $data_to_export[] = array( 84 | 'group_id' => 'woocommerce_orders', 85 | 'group_label' => esc_attr__( 'Orders', 'woocommerce-gateway-paypal-express-checkout' ), 86 | 'item_id' => 'order-' . $order->get_id(), 87 | 'data' => array( 88 | array( 89 | 'name' => esc_attr__( 'PPEC Refundable transaction data', 'woocommerce-gateway-paypal-express-checkout' ), 90 | 'value' => wp_json_encode( get_post_meta( $order->get_id(), '_woo_pp_txnData', true ) ), 91 | ), 92 | array( 93 | 'name' => esc_attr__( 'PPEC Billing agreement id', 'woocommerce-gateway-paypal-express-checkout' ), 94 | 'value' => get_post_meta( $order->get_id(), '_ppec_billing_agreement_id', true ), 95 | ), 96 | ), 97 | ); 98 | } 99 | 100 | $done = 10 > count( $orders ); 101 | } 102 | 103 | return array( 104 | 'data' => $data_to_export, 105 | 'done' => $done, 106 | ); 107 | } 108 | 109 | /** 110 | * Handle exporting data for Subscriptions. 111 | * 112 | * @param string $email_address E-mail address to export. 113 | * @param int $page Pagination of data. 114 | * 115 | * @return array 116 | */ 117 | public function subscriptions_data_exporter( $email_address, $page = 1 ) { 118 | $done = false; 119 | $page = (int) $page; 120 | $data_to_export = array(); 121 | 122 | $meta_query = array( 123 | 'relation' => 'AND', 124 | array( 125 | 'key' => '_payment_method', 126 | 'value' => array( 'ppec_paypal' ), 127 | 'compare' => 'IN', 128 | ), 129 | array( 130 | 'key' => '_billing_email', 131 | 'value' => $email_address, 132 | 'compare' => '=', 133 | ), 134 | ); 135 | 136 | $subscription_query = array( 137 | 'posts_per_page' => 10, 138 | 'page' => $page, 139 | 'meta_query' => $meta_query, /* phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query */ 140 | ); 141 | 142 | $subscriptions = wcs_get_subscriptions( $subscription_query ); 143 | 144 | $done = true; 145 | 146 | if ( 0 < count( $subscriptions ) ) { 147 | foreach ( $subscriptions as $subscription ) { 148 | $data_to_export[] = array( 149 | 'group_id' => 'woocommerce_subscriptions', 150 | 'group_label' => esc_attr__( 'Subscriptions', 'woocommerce-gateway-paypal-express-checkout' ), 151 | 'item_id' => 'subscription-' . $subscription->get_id(), 152 | 'data' => array( 153 | array( 154 | 'name' => esc_attr__( 'PPEC Refundable transaction data', 'woocommerce-gateway-paypal-express-checkout' ), 155 | 'value' => wp_json_encode( get_post_meta( $subscription->get_id(), '_woo_pp_txnData', true ) ), 156 | ), 157 | array( 158 | 'name' => esc_attr__( 'PPEC Billing agreement id', 'woocommerce-gateway-paypal-express-checkout' ), 159 | 'value' => get_post_meta( $subscription->get_id(), '_ppec_billing_agreement_id', true ), 160 | ), 161 | ), 162 | ); 163 | } 164 | 165 | $done = 10 > count( $subscriptions ); 166 | } 167 | 168 | return array( 169 | 'data' => $data_to_export, 170 | 'done' => $done, 171 | ); 172 | } 173 | 174 | /** 175 | * Finds and erases order data by email address. 176 | * 177 | * @since 3.4.0 178 | * @param string $email_address The user email address. 179 | * @param int $page Page. 180 | * @return array An array of personal data in name value pairs 181 | */ 182 | public function order_data_eraser( $email_address, $page ) { 183 | $orders = $this->get_ppec_orders( $email_address, (int) $page ); 184 | 185 | $items_removed = false; 186 | $items_retained = false; 187 | $messages = array(); 188 | 189 | foreach ( (array) $orders as $order ) { 190 | $order = wc_get_order( $order->get_id() ); 191 | 192 | list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); 193 | 194 | $items_removed |= $removed; 195 | $items_retained |= $retained; 196 | $messages = array_merge( $messages, $msgs ); 197 | 198 | list( $removed, $retained, $msgs ) = $this->maybe_handle_subscription( $order ); 199 | 200 | $items_removed |= $removed; 201 | $items_retained |= $retained; 202 | $messages = array_merge( $messages, $msgs ); 203 | } 204 | 205 | // Tell core if we have more orders to work on still 206 | $done = count( $orders ) < 10; 207 | 208 | return array( 209 | 'items_removed' => $items_removed, 210 | 'items_retained' => $items_retained, 211 | 'messages' => $messages, 212 | 'done' => $done, 213 | ); 214 | } 215 | 216 | /** 217 | * Handle eraser of data tied to Subscriptions 218 | * 219 | * @param WC_Order $order 220 | * @return array 221 | */ 222 | protected function maybe_handle_subscription( $order ) { 223 | if ( ! class_exists( 'WC_Subscriptions' ) ) { 224 | return array( false, false, array() ); 225 | } 226 | 227 | if ( ! wcs_order_contains_subscription( $order ) ) { 228 | return array( false, false, array() ); 229 | } 230 | 231 | $subscription = current( wcs_get_subscriptions_for_order( $order->get_id() ) ); 232 | $subscription_id = $subscription->get_id(); 233 | 234 | $ppec_billing = get_post_meta( $subscription_id, '_ppec_billing_agreement_id', true ); 235 | 236 | if ( empty( $ppec_billing ) ) { 237 | return array( false, false, array() ); 238 | } 239 | 240 | if ( $subscription->has_status( apply_filters( 'woocommerce_paypal_express_checkout_privacy_eraser_subs_statuses', array( 'on-hold', 'active' ) ) ) ) { 241 | // Translators: placeholder is an order number. 242 | return array( false, true, array( sprintf( esc_attr__( 'Order ID %d contains an active Subscription', 'woocommerce-gateway-paypal-express-checkout' ), $order->get_id() ) ) ); 243 | } 244 | 245 | $renewal_orders = WC_Subscriptions_Renewal_Order::get_renewal_orders( $order->get_id() ); 246 | 247 | foreach ( $renewal_orders as $renewal_order_id ) { 248 | delete_post_meta( $renewal_order_id, '_woo_pp_txnData' ); 249 | delete_post_meta( $renewal_order_id, '_ppec_billing_agreement_id' ); 250 | delete_post_meta( $renewal_order_id, '_paypal_status' ); 251 | } 252 | 253 | delete_post_meta( $subscription_id, '_woo_pp_txnData' ); 254 | delete_post_meta( $subscription_id, '_ppec_billing_agreement_id' ); 255 | delete_post_meta( $subscription_id, '_paypal_status' ); 256 | 257 | return array( true, false, array( esc_attr__( 'PayPal Checkout Subscriptions Data Erased.', 'woocommerce-gateway-paypal-express-checkout' ) ) ); 258 | } 259 | 260 | /** 261 | * Handle eraser of data tied to Orders 262 | * 263 | * @param WC_Order $order 264 | * @return array 265 | */ 266 | protected function maybe_handle_order( $order ) { 267 | $order_id = $order->get_id(); 268 | $ppec_txn_data = get_post_meta( $order_id, '_woo_pp_txnData', true ); 269 | $ppec_billing = get_post_meta( $order_id, '_ppec_billing_agreement_id', true ); 270 | $ppec_status = get_post_meta( $order_id, '_paypal_status', true ); 271 | 272 | if ( empty( $ppec_txn_data ) && empty( $ppec_billing ) && empty( $ppec_status ) ) { 273 | return array( false, false, array() ); 274 | } 275 | 276 | delete_post_meta( $order_id, '_woo_pp_txnData' ); 277 | delete_post_meta( $order_id, '_ppec_billing_agreement_id' ); 278 | delete_post_meta( $order_id, '_paypal_status' ); 279 | 280 | return array( true, false, array( esc_attr__( 'PayPal Checkout Order Data Erased.', 'woocommerce-gateway-paypal-express-checkout' ) ) ); 281 | } 282 | } 283 | 284 | new WC_Gateway_PPEC_Privacy(); 285 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-refund.php: -------------------------------------------------------------------------------- 1 | get_transaction_id(); 27 | $params['REFUNDTYPE'] = $refundType; 28 | $params['AMT'] = $amount; 29 | $params['CURRENCYCODE'] = $currency; 30 | $params['NOTE'] = $reason; 31 | 32 | // do API call 33 | $response = wc_gateway_ppec()->client->refund_transaction( $params ); 34 | 35 | // look at ACK to see if success or failure 36 | // if success return the transaction ID of the refund 37 | // if failure then do 'throw new PayPal_API_Exception( $response );' 38 | 39 | if ( 'Success' === $response['ACK'] || 'SuccessWithWarning' === $response['ACK'] ) { 40 | return $response['REFUNDTRANSACTIONID']; 41 | } else { 42 | throw new PayPal_API_Exception( $response ); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-session-data.php: -------------------------------------------------------------------------------- 1 | '', 85 | 'source' => 'cart', 86 | 'order_id' => false, 87 | 'expires_in' => 10800, 88 | 'use_paypal_credit' => false, 89 | 'cancel_url' => '', 90 | ) 91 | ); 92 | 93 | $this->token = $args['token']; 94 | $this->source = $args['source']; 95 | $this->expiry_time = time() + $args['expires_in']; 96 | $this->use_paypal_credit = $args['use_paypal_credit']; 97 | 98 | if ( 'order' === $this->source ) { 99 | $this->order_id = $args['order_id']; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-settings.php: -------------------------------------------------------------------------------- 1 | 'ar_EG', 70 | 'arq' => 'ar_EG', 71 | 'ary' => 'ar_EG', 72 | 'de_AT' => 'de_DE', 73 | 'de_CH' => 'de_DE', 74 | 'de_CH_informal' => 'de_DE', 75 | 'de_DE_formal' => 'de_DE', 76 | 'el' => 'el_GR', 77 | 'es_AR' => 'es_ES', 78 | 'es_CL' => 'es_ES', 79 | 'es_CO' => 'es_ES', 80 | 'es_CR' => 'es_ES', 81 | 'es_DO' => 'es_ES', 82 | 'es_GT' => 'es_ES', 83 | 'es_HN' => 'es_ES', 84 | 'es_MX' => 'es_ES', 85 | 'es_PE' => 'es_ES', 86 | 'es_PR' => 'es_ES', 87 | 'es_ES' => 'es_ES', 88 | 'es_UY' => 'es_ES', 89 | 'es_VE' => 'es_ES', 90 | 'fi' => 'fi_FI', 91 | 'fr_BE' => 'fr_FR', 92 | 'ja' => 'ja_JP', 93 | 'nb_NO' => 'no_NO', 94 | 'nn_NO' => 'no_NO', 95 | 'nl_BE' => 'nl_NL', 96 | 'nl_NL_formal' => 'nl_NL', 97 | 'pt_AO' => 'pt_PT', 98 | 'pt_PT_ao90' => 'pt_PT', 99 | 'th' => 'th_TH', 100 | 'zh_SG' => 'zh_CN', 101 | ); 102 | 103 | /** 104 | * Flag to indicate setting has been loaded from DB. 105 | * 106 | * @var bool 107 | */ 108 | private $_is_setting_loaded = false; 109 | 110 | public function __set( $key, $value ) { 111 | if ( array_key_exists( $key, $this->_settings ) ) { 112 | $this->_settings[ $key ] = $value; 113 | } 114 | } 115 | 116 | public function __get( $key ) { 117 | if ( array_key_exists( $key, $this->_settings ) ) { 118 | return $this->_settings[ $key ]; 119 | } 120 | return null; 121 | } 122 | 123 | public function __isset( $key ) { 124 | return array_key_exists( $key, $this->_settings ); 125 | } 126 | 127 | public function __construct() { 128 | $this->load(); 129 | } 130 | 131 | /** 132 | * Load settings from DB. 133 | * 134 | * @since 1.2.0 135 | * 136 | * @param bool $force_reload Force reload settings 137 | * 138 | * @return WC_Gateway_PPEC_Settings Instance of WC_Gateway_PPEC_Settings 139 | */ 140 | public function load( $force_reload = false ) { 141 | if ( $this->_is_setting_loaded && ! $force_reload ) { 142 | return $this; 143 | } 144 | $this->_settings = (array) get_option( 'woocommerce_ppec_paypal_settings', array() ); 145 | $this->_settings['use_spb'] = ! apply_filters( 'woocommerce_paypal_express_checkout_disable_smart_payment_buttons', false, $this ) ? 'yes' : 'no'; 146 | $this->_is_setting_loaded = true; 147 | return $this; 148 | } 149 | 150 | /** 151 | * Load settings from DB. 152 | * 153 | * @deprecated 154 | */ 155 | public function load_settings( $force_reload = false ) { 156 | _deprecated_function( __METHOD__, '1.2.0', 'WC_Gateway_PPEC_Settings::load' ); 157 | return $this->load( $force_reload ); 158 | } 159 | 160 | /** 161 | * Save current settings. 162 | * 163 | * @since 1.2.0 164 | */ 165 | public function save() { 166 | update_option( 'woocommerce_ppec_paypal_settings', $this->_settings ); 167 | } 168 | 169 | /** 170 | * Get API credentials for live envionment. 171 | * 172 | * @return WC_Gateway_PPEC_Client_Credential_Signature|WC_Gateway_PPEC_Client_Credential_Certificate 173 | */ 174 | public function get_live_api_credentials() { 175 | if ( $this->api_certificate ) { 176 | return new WC_Gateway_PPEC_Client_Credential_Certificate( $this->api_username, $this->api_password, base64_decode( $this->api_certificate ), $this->api_subject ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode 177 | } 178 | 179 | return new WC_Gateway_PPEC_Client_Credential_Signature( $this->api_username, $this->api_password, $this->api_signature, $this->api_subject ); 180 | } 181 | 182 | /** 183 | * Get API credentials for sandbox envionment. 184 | * 185 | * @return WC_Gateway_PPEC_Client_Credential_Signature|WC_Gateway_PPEC_Client_Credential_Certificate 186 | */ 187 | public function get_sandbox_api_credentials() { 188 | if ( $this->sandbox_api_certificate ) { 189 | return new WC_Gateway_PPEC_Client_Credential_Certificate( $this->sandbox_api_username, $this->sandbox_api_password, base64_decode( $this->sandbox_api_certificate ), $this->sandbox_api_subject ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode 190 | } 191 | 192 | return new WC_Gateway_PPEC_Client_Credential_Signature( $this->sandbox_api_username, $this->sandbox_api_password, $this->sandbox_api_signature, $this->sandbox_api_subject ); 193 | } 194 | 195 | /** 196 | * Get API credentials for the current envionment. 197 | * 198 | * @return object 199 | */ 200 | public function get_active_api_credentials() { 201 | return 'live' === $this->get_environment() ? $this->get_live_api_credentials() : $this->get_sandbox_api_credentials(); 202 | } 203 | 204 | /** 205 | * Get the REST Client ID for a live environment. 206 | * 207 | * @since 2.0 208 | * @return string 209 | */ 210 | public function get_live_rest_client_id() { 211 | return 'AQbghYd-7mRPyimEriYScIgTnYUsLnr5wVnPnmfPaSzwKrUe3qNzfEc5hXr9Ucf_JG_HFAZpJMJYXMuk'; 212 | } 213 | 214 | /** 215 | * Get the REST Client ID for current environment. 216 | * 217 | * @since 2.0 218 | * @return string 219 | */ 220 | public function get_active_rest_client_id() { 221 | return 'live' === $this->get_environment() ? $this->get_live_rest_client_id() : 'sb'; 222 | } 223 | 224 | /** 225 | * Get PayPal redirect URL. 226 | * 227 | * @param string $token Token 228 | * @param bool $commit If set to true, 'useraction' parameter will be set 229 | * to 'commit' which makes PayPal sets the button text 230 | * to **Pay Now** ont the PayPal _Review your information_ 231 | * page. 232 | * @param bool $ppc Whether to use PayPal credit. 233 | * 234 | * @return string PayPal redirect URL 235 | */ 236 | public function get_paypal_redirect_url( $token, $commit = false, $ppc = false ) { 237 | $url = 'https://www.'; 238 | 239 | if ( 'live' !== $this->environment ) { 240 | $url .= 'sandbox.'; 241 | } 242 | 243 | $url .= 'paypal.com/checkoutnow?token=' . urlencode( $token ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode 244 | 245 | if ( $commit ) { 246 | $url .= '&useraction=commit'; 247 | } 248 | 249 | if ( $ppc ) { 250 | $url .= '#/checkout/chooseCreditOffer'; 251 | } 252 | 253 | return $url; 254 | } 255 | 256 | public function get_set_express_checkout_shortcut_params( $buckets = 1 ) { 257 | _deprecated_function( __METHOD__, '1.2.0', 'WC_Gateway_PPEC_Client::get_set_express_checkout_params' ); 258 | 259 | return wc_gateway_ppec()->client->get_set_express_checkout_params( array( 'skip_checkout' => true ) ); 260 | } 261 | 262 | public function get_set_express_checkout_mark_params( $buckets = 1 ) { 263 | _deprecated_function( __METHOD__, '1.2.0', 'WC_Gateway_PPEC_Client::get_set_express_checkout_params' ); 264 | 265 | // Still missing order_id in args. 266 | return wc_gateway_ppec()->client->get_set_express_checkout_params( 267 | array( 268 | 'skip_checkout' => false, 269 | ) 270 | ); 271 | } 272 | 273 | /** 274 | * Get base parameters, based on settings instance, for DoExpressCheckoutCheckout NVP call. 275 | * 276 | * @see https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/ 277 | * 278 | * @param WC_Order $order Order object 279 | * @param int|array $buckets Number of buckets or list of bucket 280 | * 281 | * @return array DoExpressCheckoutPayment parameters 282 | */ 283 | public function get_do_express_checkout_params( WC_Order $order, $buckets = 1 ) { 284 | $params = array(); 285 | if ( ! is_array( $buckets ) ) { 286 | $num_buckets = $buckets; 287 | $buckets = array(); 288 | for ( $i = 0; $i < $num_buckets; $i++ ) { 289 | $buckets[] = $i; 290 | } 291 | } 292 | 293 | foreach ( $buckets as $bucket_num ) { 294 | $params[ 'PAYMENTREQUEST_' . $bucket_num . '_NOTIFYURL' ] = WC()->api_request_url( 'WC_Gateway_PPEC' ); 295 | $params[ 'PAYMENTREQUEST_' . $bucket_num . '_PAYMENTACTION' ] = $this->get_paymentaction(); 296 | $params[ 'PAYMENTREQUEST_' . $bucket_num . '_INVNUM' ] = $this->invoice_prefix . $order->get_order_number(); 297 | $params[ 'PAYMENTREQUEST_' . $bucket_num . '_CUSTOM' ] = wp_json_encode( 298 | array( 299 | 'order_id' => $order->id, 300 | 'order_key' => $order->order_key, 301 | ) 302 | ); 303 | } 304 | 305 | return $params; 306 | } 307 | 308 | /** 309 | * Is PPEC enabled. 310 | * 311 | * @return bool 312 | */ 313 | public function is_enabled() { 314 | return 'yes' === $this->enabled; 315 | } 316 | 317 | /** 318 | * Is logging enabled. 319 | * 320 | * @return bool 321 | */ 322 | public function is_logging_enabled() { 323 | return 'yes' === $this->debug; 324 | } 325 | 326 | /** 327 | * Get payment action from setting. 328 | * 329 | * @return string 330 | */ 331 | public function get_paymentaction() { 332 | return 'authorization' === $this->paymentaction ? 'authorization' : 'sale'; 333 | } 334 | 335 | /** 336 | * Get active environment from setting. 337 | * 338 | * @return string 339 | */ 340 | public function get_environment() { 341 | return 'sandbox' === $this->environment ? 'sandbox' : 'live'; 342 | } 343 | 344 | /** 345 | * Subtotal mismatches. 346 | * 347 | * @return string 348 | */ 349 | public function get_subtotal_mismatch_behavior() { 350 | return 'drop' === $this->subtotal_mismatch_behavior ? 'drop' : 'add'; 351 | } 352 | 353 | /** 354 | * Get session length. 355 | * 356 | * @todo Map this to a merchant-configurable setting 357 | * 358 | * @return int 359 | */ 360 | public function get_token_session_length() { 361 | return 10800; // 3h 362 | } 363 | 364 | /** 365 | * Whether currency has decimal restriction for PPCE to functions? 366 | * 367 | * @return bool True if it has restriction otherwise false 368 | */ 369 | public function currency_has_decimal_restriction() { 370 | return ( 371 | 'yes' === $this->enabled 372 | && 373 | in_array( get_woocommerce_currency(), array( 'HUF', 'TWD', 'JPY' ), true ) 374 | && 375 | 0 !== absint( get_option( 'woocommerce_price_num_decimals', 2 ) ) 376 | ); 377 | } 378 | 379 | /** 380 | * Get locale for PayPal. 381 | * 382 | * @return string 383 | */ 384 | public function get_paypal_locale() { 385 | $locale = get_locale(); 386 | 387 | // For stores based in the US, we need to do some special mapping so PayPal Credit is allowed. 388 | if ( wc_gateway_ppec_is_US_based_store() ) { 389 | // PayPal has support for French, Spanish and Chinese languages based in the US. See https://developer.paypal.com/docs/archive/checkout/reference/supported-locales/ 390 | preg_match( '/^(fr|es|zh)_/', $locale, $language_code ); 391 | 392 | if ( ! empty( $language_code ) ) { 393 | $locale = $language_code[0] . 'US'; 394 | } else { 395 | $locale = 'en_US'; 396 | } 397 | } elseif ( ! in_array( $locale, $this->_supported_locales, true ) ) { 398 | // Mapping some WP locales to PayPal locales. 399 | if ( isset( $this->_locales_mapping[ $locale ] ) ) { 400 | $locale = $this->_locales_mapping[ $locale ]; 401 | } else { 402 | $locale = 'en_US'; 403 | } 404 | } 405 | 406 | return apply_filters( 'woocommerce_paypal_express_checkout_paypal_locale', $locale ); 407 | } 408 | 409 | /** 410 | * Get brand name form settings. 411 | * 412 | * Default to site's name if brand_name in settings empty. 413 | * 414 | * @since 1.2.0 415 | * 416 | * @return string 417 | */ 418 | public function get_brand_name() { 419 | $brand_name = $this->brand_name ? $this->brand_name : get_bloginfo( 'name', 'display' ); 420 | 421 | /** 422 | * Character length and limitations for this parameter is 127 single-byte 423 | * alphanumeric characters. 424 | * 425 | * @see https://developer.paypal.com/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/ 426 | */ 427 | if ( ! empty( $brand_name ) ) { 428 | $brand_name = substr( $brand_name, 0, 127 ); 429 | } 430 | 431 | /** 432 | * Filters the brand name in PayPal hosted checkout pages. 433 | * 434 | * @since 1.2.0 435 | * 436 | * @param string Brand name 437 | */ 438 | return apply_filters( 'woocommerce_paypal_express_checkout_get_brand_name', $brand_name ); 439 | } 440 | 441 | /** 442 | * Checks whether PayPal Credit is enabled. 443 | * 444 | * @since 1.2.0 445 | * 446 | * @return bool Returns true if PayPal Credit is enabled and supported 447 | */ 448 | public function is_credit_enabled() { 449 | return 'yes' === $this->credit_enabled && wc_gateway_ppec_is_credit_supported(); 450 | } 451 | 452 | /** 453 | * Checks if currency in setting supports 0 decimal places. 454 | * 455 | * @since 1.2.0 456 | * 457 | * @return bool Returns true if currency supports 0 decimal places 458 | */ 459 | public function is_currency_supports_zero_decimal() { 460 | return in_array( get_woocommerce_currency(), array( 'HUF', 'JPY', 'TWD' ), true ); 461 | } 462 | 463 | /** 464 | * Get number of digits after the decimal point. 465 | * 466 | * @since 1.2.0 467 | * 468 | * @return int Number of digits after the decimal point. Either 2 or 0 469 | */ 470 | public function get_number_of_decimal_digits() { 471 | return $this->is_currency_supports_zero_decimal() ? 0 : 2; 472 | } 473 | 474 | /** 475 | * Whether to use checkout.js or the latest available SDK. 476 | * 477 | * @since 1.7.0 478 | * 479 | * @return bool 480 | */ 481 | public function use_legacy_checkout_js() { 482 | return (bool) apply_filters( 'woocommerce_paypal_express_checkout_use_legacy_checkout_js', false ); 483 | } 484 | 485 | } 486 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-with-paypal-addons.php: -------------------------------------------------------------------------------- 1 | supports = array_merge( 13 | $this->supports, 14 | array( 15 | 'subscriptions', 16 | 'subscription_cancellation', 17 | 'subscription_reactivation', 18 | 'subscription_suspension', 19 | 'multiple_subscriptions', 20 | 'subscription_payment_method_change_customer', 21 | 'subscription_payment_method_change_admin', 22 | 'subscription_amount_changes', 23 | 'subscription_date_changes', 24 | ) 25 | ); 26 | 27 | $this->_maybe_register_callback_in_subscriptions(); 28 | } 29 | 30 | /** 31 | * Maybe register callback in WooCommerce Subscription hooks. 32 | * 33 | * @since 1.2.0 34 | */ 35 | protected function _maybe_register_callback_in_subscriptions() { 36 | if ( ! class_exists( 'WC_Subscriptions_Order' ) ) { 37 | return; 38 | } 39 | 40 | add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 ); 41 | add_action( 'woocommerce_subscription_failing_payment_method_' . $this->id, array( $this, 'update_failing_payment_method' ) ); 42 | 43 | // When changing the payment method for a WooCommerce Subscription to PayPal Checkout, let WooCommerce Subscription 44 | // know that the payment method for that subscription should not be changed immediately. Instead, it should 45 | // wait for the IPN notification, after the user confirmed the payment method change with PayPal. 46 | add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', array( $this, 'indicate_async_payment_method_update' ), 10, 2 ); 47 | 48 | // Add extra parameter when updating the subscription payment method to PayPal. 49 | add_filter( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_return_url', array( $this, 'add_query_param_to_url_subscription_payment_method_change' ), 10, 2 ); 50 | add_filter( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_cancel_url', array( $this, 'add_query_param_to_url_subscription_payment_method_change' ), 10, 2 ); 51 | } 52 | 53 | /** 54 | * Checks whether order is part of subscription. 55 | * 56 | * @since 1.2.0 57 | * 58 | * @param int $order_id Order ID 59 | * 60 | * @return bool Returns true if order is part of subscription 61 | */ 62 | public function is_subscription( $order_id ) { 63 | return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) ); 64 | } 65 | 66 | /** 67 | * Checks whether the order associated with the given order_id is 68 | * for changing a payment method for a WooCommerce Subscription. 69 | * 70 | * @since 1.7.0 71 | * 72 | * @param int $order_id Order ID. 73 | * 74 | * @return bool Returns true if the order is changing the payment method for a subscription. 75 | */ 76 | private function is_order_changing_payment_method_for_subscription( $order_id ) { 77 | $order = wc_get_order( $order_id ); 78 | return ( 79 | is_callable( array( $order, 'get_type' ) ) 80 | && 'shop_subscription' === $order->get_type() 81 | && isset( $_POST['_wcsnonce'] ) 82 | && wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_change_payment_method' ) 83 | && isset( $_POST['woocommerce_change_payment'] ) 84 | && $order->get_id() === absint( $_POST['woocommerce_change_payment'] ) 85 | && isset( $_GET['key'] ) 86 | && $order->get_order_key() === $_GET['key'] 87 | && 0 === $order->get_total() // WooCommerce Subscriptions uses $0 orders to update payment method for the subscription. 88 | ); 89 | } 90 | 91 | /** 92 | * Process payment. 93 | * 94 | * @since 1.2.0 95 | * 96 | * @param int $order_id Order ID 97 | * 98 | * @return array 99 | */ 100 | public function process_payment( $order_id ) { 101 | if ( $this->is_subscription( $order_id ) ) { 102 | // Is this a subscription payment method change? 103 | if ( $this->is_order_changing_payment_method_for_subscription( $order_id ) ) { 104 | return $this->change_subscription_payment_method( $order_id ); 105 | } 106 | // Otherwise, it's a subscription payment. 107 | return $this->process_subscription( $order_id ); 108 | } 109 | 110 | return parent::process_payment( $order_id ); 111 | } 112 | 113 | /** 114 | * Process initial subscription. 115 | * 116 | * @since 1.2.0 117 | * 118 | * @param int $order_id Order ID 119 | * 120 | * @return array 121 | */ 122 | public function process_subscription( $order_id ) { 123 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 124 | $resp = parent::process_payment( $order_id ); 125 | $order = wc_get_order( $order_id ); 126 | 127 | $subscriptions = array(); 128 | if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) { 129 | $subscriptions = wcs_get_subscriptions_for_order( $order_id ); 130 | } elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) { 131 | $subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id ); 132 | } 133 | 134 | $billing_agreement_id = $old_wc ? get_post_meta( $order_id, '_ppec_billing_agreement_id', true ) : $order->get_meta( '_ppec_billing_agreement_id', true ); 135 | 136 | // Shipping / billing addresses and billing agreement were not copied 137 | // because it's not available during subscription creation. 138 | foreach ( $subscriptions as $subscription ) { 139 | wcs_copy_order_address( $order, $subscription ); 140 | update_post_meta( is_callable( array( $subscription, 'get_id' ) ) ? $subscription->get_id() : $subscription->id, '_ppec_billing_agreement_id', $billing_agreement_id ); 141 | } 142 | 143 | return $resp; 144 | } 145 | 146 | /** 147 | * Process scheduled subscription payment. 148 | * 149 | * @since 1.2.0 150 | * 151 | * @param float $amount Subscription amount 152 | * @param int|WC_Order $order Order ID or order object 153 | */ 154 | public function scheduled_subscription_payment( $amount, $order ) { 155 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 156 | $order = wc_get_order( $order ); 157 | $order_id = $old_wc ? $order->id : $order->get_id(); 158 | $billing_agreement_id = $old_wc ? get_post_meta( $order_id, '_ppec_billing_agreement_id', true ) : $order->get_meta( '_ppec_billing_agreement_id', true ); 159 | 160 | if ( empty( $billing_agreement_id ) ) { 161 | wc_gateway_ppec_log( sprintf( '%s: Could not found billing agreement. Skip reference transaction', __METHOD__ ) ); 162 | return; 163 | } 164 | 165 | if ( 0.0 === (float) $amount ) { 166 | $order->payment_complete(); 167 | return; 168 | } 169 | 170 | $client = wc_gateway_ppec()->client; 171 | $params = $client->get_do_reference_transaction_params( 172 | array( 173 | 'reference_id' => $billing_agreement_id, 174 | 'amount' => $amount, 175 | 'order_id' => $order_id, 176 | ) 177 | ); 178 | 179 | $resp = $client->do_reference_transaction( $params ); 180 | 181 | $this->_process_reference_transaction_response( $order, $resp ); 182 | } 183 | 184 | /** 185 | * Process reference transaction response used when creating payment for 186 | * scheduled subscription. 187 | * 188 | * @since 1.2.0 189 | * 190 | * @param WC_Order $order Order object 191 | * @param array $response Response from DoReferenceTransaction 192 | */ 193 | protected function _process_reference_transaction_response( $order, $response ) { 194 | $client = wc_gateway_ppec()->client; 195 | 196 | try { 197 | if ( ! $client->response_has_success_status( $response ) ) { 198 | throw new Exception( esc_html__( 'PayPal API error', 'woocommerce-gateway-paypal-express-checkout' ) ); 199 | } 200 | 201 | wc_gateway_ppec_save_transaction_data( $order, $response ); 202 | 203 | $status = ! empty( $response['PAYMENTSTATUS'] ) ? $response['PAYMENTSTATUS'] : ''; 204 | 205 | switch ( $status ) { 206 | case 'Pending': 207 | /* translators: placeholder is pending reason from PayPal API. */ 208 | $order_note = sprintf( esc_html__( 'PayPal transaction held: %s', 'woocommerce-gateway-paypal-express-checkout' ), $response['PENDINGREASON'] ); 209 | if ( ! $order->has_status( 'on-hold' ) ) { 210 | $order->update_status( 'on-hold', $order_note ); 211 | } else { 212 | $order->add_order_note( $order_note ); 213 | } 214 | break; 215 | case 'Completed': 216 | case 'Processed': 217 | case 'In-Progress': 218 | $transaction_id = $response['TRANSACTIONID']; 219 | // Translators: %s is a transaction ID. 220 | $order->add_order_note( sprintf( esc_html__( 'PayPal payment approved (ID: %s)', 'woocommerce-gateway-paypal-express-checkout' ), $transaction_id ) ); 221 | $order->payment_complete( $transaction_id ); 222 | break; 223 | default: 224 | throw new Exception( esc_html__( 'PayPal payment declined', 'woocommerce-gateway-paypal-express-checkout' ) ); 225 | } 226 | } catch ( Exception $e ) { 227 | $order->update_status( 'failed', $e->getMessage() ); 228 | } 229 | } 230 | 231 | /** 232 | * Update billing agreement ID for a subscription after using PPEC to complete 233 | * a payment to make up for an automatic renewal payment which previously 234 | * failed. 235 | * 236 | * @since 1.2.0 237 | * 238 | * @param WC_Subscription $subscription The subscription for which the failing 239 | * payment method relates 240 | * @param WC_Order $renewal_order The order which recorded the successful 241 | * payment (to make up for the failed 242 | * automatic payment) 243 | */ 244 | public function update_failing_payment_method( $subscription, $renewal_order ) { 245 | update_post_meta( is_callable( array( $subscription, 'get_id' ) ) ? $subscription->get_id() : $subscription->id, '_ppec_billing_agreement_id', $renewal_order->ppec_billing_agreement_id ); 246 | } 247 | 248 | /** 249 | * Indicate to WooCommerce Subscriptions that the payment method change for PayPal Checkout 250 | * should be asynchronous. 251 | * 252 | * WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode uses the 253 | * result to decide whether or not to change the payment method information on the subscription 254 | * right away or not. 255 | * 256 | * In our case, the payment method will not be updated until after the user confirms the 257 | * payment method change with PayPal. Once that's done, we'll take care of finishing 258 | * the payment method update with the subscription. 259 | * 260 | * @since 1.7.0 261 | * 262 | * @param bool $should_update Current value of whether the payment method should be updated immediately. 263 | * @param string $new_payment_method The new payment method name. 264 | * 265 | * @return bool Whether the subscription's payment method should be updated on checkout or async when a response is returned. 266 | */ 267 | public function indicate_async_payment_method_update( $should_update, $new_payment_method ) { 268 | if ( 'ppec_paypal' === $new_payment_method ) { 269 | $should_update = false; 270 | } 271 | return $should_update; 272 | } 273 | 274 | /** 275 | * Start the process to update the payment method for a WooCommerce Subscriptions. 276 | * 277 | * This function is called by `process_payment` when changing a payment method for WooCommerce Subscriptions. 278 | * When it's successful, `WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode` will 279 | * redirect to the redirect URL provided and the user will be prompted to confirm the payment update. 280 | * 281 | * @since 1.7.0 282 | * 283 | * @param int $order_id Order ID. 284 | * 285 | * @return array Array used by WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode. 286 | */ 287 | public function change_subscription_payment_method( $order_id ) { 288 | try { 289 | return array( 290 | 'result' => 'success', 291 | 'redirect' => wc_gateway_ppec()->checkout->start_checkout_from_order( $order_id, false ), 292 | ); 293 | } catch ( PayPal_API_Exception $e ) { 294 | wc_add_notice( $e->getMessage(), 'error' ); 295 | return array( 296 | 'result' => 'failure', 297 | ); 298 | } 299 | } 300 | 301 | /** 302 | * Add query param to return and cancel URLs when making a payment change for 303 | * a WooCommerce Subscription. 304 | * 305 | * @since 1.7.0 306 | * 307 | * @param string $url The original URL. 308 | * @param int $order_id Order ID. 309 | * 310 | * @return string The new URL. 311 | */ 312 | public function add_query_param_to_url_subscription_payment_method_change( $url, $order_id ) { 313 | if ( $this->is_order_changing_payment_method_for_subscription( $order_id ) ) { 314 | return esc_url( add_query_arg( 'update_subscription_payment_method', 'true', $url ) ); 315 | } 316 | return $url; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-with-paypal-credit.php: -------------------------------------------------------------------------------- 1 | icon = 'https://www.paypalobjects.com/webstatic/en_US/i/buttons/ppc-acceptance-small.png'; 10 | 11 | parent::__construct(); 12 | 13 | if ( ! is_admin() ) { 14 | if ( wc_gateway_ppec()->checkout->is_started_from_checkout_page() ) { 15 | $this->title = __( 'PayPal Credit', 'woocommerce-gateway-paypal-express-checkout' ); 16 | } 17 | } 18 | 19 | $this->use_ppc = true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-with-paypal.php: -------------------------------------------------------------------------------- 1 | id = 'ppec_paypal'; 10 | $this->icon = 'https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png'; 11 | 12 | parent::__construct(); 13 | 14 | if ( $this->is_available() ) { 15 | $ipn_handler = new WC_Gateway_PPEC_IPN_Handler( $this ); 16 | $ipn_handler->handle(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-with-spb-addons.php: -------------------------------------------------------------------------------- 1 | 24 |
    25 | checkout->has_active_session() ) { 35 | wp_enqueue_script( 'wc-gateway-ppec-order-review', wc_gateway_ppec()->plugin_url . 'assets/js/wc-gateway-ppec-order-review.js', array( 'jquery' ), wc_gateway_ppec()->version, true ); 36 | } 37 | } 38 | 39 | /** 40 | * Save data necessary for authorizing payment to session, in order to 41 | * go ahead with processing payment and bypass redirecting to PayPal. 42 | * 43 | * @param int $order_id Order ID 44 | * 45 | * @return array 46 | */ 47 | public function process_payment( $order_id ) { 48 | if ( isset( $_POST['payerID'] ) && isset( $_POST['paymentToken'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 49 | $session = WC()->session->get( 'paypal', new stdClass() ); 50 | 51 | $session->checkout_completed = true; 52 | $session->payer_id = $_POST['payerID']; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 53 | $session->token = $_POST['paymentToken']; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 54 | 55 | WC()->session->set( 'paypal', $session ); 56 | } 57 | 58 | return parent::process_payment( $order_id ); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /includes/class-wc-gateway-ppec-with-spb.php: -------------------------------------------------------------------------------- 1 | 24 |
    25 | checkout->has_active_session() ) { 35 | wp_enqueue_script( 'wc-gateway-ppec-order-review', wc_gateway_ppec()->plugin_url . 'assets/js/wc-gateway-ppec-order-review.js', array( 'jquery' ), wc_gateway_ppec()->version, true ); 36 | } 37 | } 38 | 39 | /** 40 | * Save data necessary for authorizing payment to session, in order to 41 | * go ahead with processing payment and bypass redirecting to PayPal. 42 | * 43 | * @param int $order_id Order ID 44 | * 45 | * @return array 46 | */ 47 | public function process_payment( $order_id ) { 48 | if ( isset( $_POST['payerID'] ) && isset( $_POST['paymentToken'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 49 | $session = WC()->session->get( 'paypal', new stdClass() ); 50 | 51 | $session->checkout_completed = true; 52 | $session->payer_id = $_POST['payerID']; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 53 | $session->token = $_POST['paymentToken']; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash 54 | 55 | WC()->session->set( 'paypal', $session ); 56 | } 57 | 58 | return parent::process_payment( $order_id ); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /includes/exceptions/class-wc-gateway-ppec-api-exception.php: -------------------------------------------------------------------------------- 1 | $value ) { 44 | if ( preg_match( '/^L_ERRORCODE(\d+)$/', $index, $matches ) ) { 45 | $errors[ $matches[1] ]['code'] = $value; 46 | } elseif ( preg_match( '/^L_SHORTMESSAGE(\d+)$/', $index, $matches ) ) { 47 | $errors[ $matches[1] ]['message'] = $value; 48 | } elseif ( preg_match( '/^L_LONGMESSAGE(\d+)$/', $index, $matches ) ) { 49 | $errors[ $matches[1] ]['long'] = $value; 50 | } elseif ( preg_match( '/^L_SEVERITYCODE(\d+)$/', $index, $matches ) ) { 51 | $errors[ $matches[1] ]['severity'] = $value; 52 | } elseif ( 'CORRELATIONID' === $index ) { 53 | $this->correlation_id = $value; 54 | } 55 | } 56 | 57 | $this->errors = array(); 58 | $error_messages = array(); 59 | foreach ( $errors as $value ) { 60 | $error = new PayPal_API_Error( $value['code'], $value['message'], $value['long'], $value['severity'] ); 61 | $this->errors[] = $error; 62 | 63 | /* translators: placeholders are error code and message from PayPal */ 64 | $error_messages[] = sprintf( esc_html__( 'PayPal error (%1$s): %2$s', 'woocommerce-gateway-paypal-express-checkout' ), $error->error_code, $error->maptoBuyerFriendlyError() ); 65 | } 66 | 67 | if ( empty( $error_messages ) ) { 68 | $error_messages[] = esc_html__( 'An error occurred while calling the PayPal API.', 'woocommerce-gateway-paypal-express-checkout' ); 69 | } 70 | 71 | $this->message = implode( PHP_EOL, $error_messages ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /includes/exceptions/class-wc-gateway-ppec-missing-session-exception.php: -------------------------------------------------------------------------------- 1 | checkout; 5 | 6 | try { 7 | $redirect_url = $checkout->start_checkout_from_cart(); 8 | wp_safe_redirect( $redirect_url ); 9 | exit; 10 | } catch ( PayPal_API_Exception $e ) { 11 | wc_add_notice( $e->getMessage(), 'error' ); 12 | 13 | $redirect_url = wc_get_cart_url(); 14 | $settings = wc_gateway_ppec()->settings; 15 | $client = wc_gateway_ppec()->client; 16 | 17 | if ( $settings->is_enabled() && $client->get_payer_id() ) { 18 | ob_end_clean(); 19 | ?> 20 | 30 | settings->is_logging_enabled() ) { 56 | return false; 57 | } 58 | 59 | if ( ! isset( $wc_ppec_logger ) ) { 60 | $wc_ppec_logger = new WC_Logger(); 61 | } 62 | 63 | $wc_ppec_logger->add( 'wc_gateway_ppec', $message ); 64 | 65 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 66 | error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 67 | } 68 | } 69 | 70 | /** 71 | * Whether PayPal credit is supported. 72 | * 73 | * @since 1.5.0 74 | * 75 | * @return bool Returns true if PayPal credit is supported 76 | */ 77 | function wc_gateway_ppec_is_credit_supported() { 78 | return wc_gateway_ppec_is_US_based_store() && 'USD' === get_woocommerce_currency(); 79 | } 80 | 81 | /** 82 | * Checks whether buyer is checking out with PayPal Credit. 83 | * 84 | * @since 1.2.0 85 | * 86 | * @return bool Returns true if buyer is checking out with PayPal Credit 87 | */ 88 | function wc_gateway_ppec_is_using_credit() { 89 | return ! empty( $_GET['use-ppc'] ) && 'true' === $_GET['use-ppc']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 90 | } 91 | 92 | const PPEC_FEE_META_NAME_OLD = 'PayPal Transaction Fee'; 93 | const PPEC_FEE_META_NAME_NEW = '_paypal_transaction_fee'; 94 | 95 | /** 96 | * Sets the PayPal Fee in the order metadata 97 | * 98 | * @since 1.6.6 99 | * 100 | * @param object $order Order to modify 101 | * @param string $fee Fee to save 102 | */ 103 | function wc_gateway_ppec_set_transaction_fee( $order, $fee ) { 104 | if ( empty( $fee ) ) { 105 | return; 106 | } 107 | $fee = wc_clean( $fee ); 108 | if ( version_compare( WC_VERSION, '3.0', '<' ) ) { 109 | update_post_meta( $order->id, PPEC_FEE_META_NAME_NEW, $fee ); 110 | } else { 111 | $order->update_meta_data( PPEC_FEE_META_NAME_NEW, $fee ); 112 | $order->save_meta_data(); 113 | } 114 | } 115 | 116 | /** 117 | * Gets the PayPal Fee from the order metadata, migrates if the fee was saved under a legacy key 118 | * 119 | * @since 1.6.6 120 | * 121 | * @param object $order Order to read 122 | * @return string Returns the fee or an empty string if the fee has not been set on the order 123 | */ 124 | function wc_gateway_ppec_get_transaction_fee( $order ) { 125 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 126 | 127 | //retrieve the fee using the new key 128 | if ( $old_wc ) { 129 | $fee = get_post_meta( $order->id, PPEC_FEE_META_NAME_NEW, true ); 130 | } else { 131 | $fee = $order->get_meta( PPEC_FEE_META_NAME_NEW, true ); 132 | } 133 | 134 | //if the fee was found, return 135 | if ( is_numeric( $fee ) ) { 136 | return $fee; 137 | } 138 | 139 | //attempt to retrieve the old meta, delete its old key, and migrate it to the new one 140 | if ( $old_wc ) { 141 | $fee = get_post_meta( $order->id, PPEC_FEE_META_NAME_OLD, true ); 142 | delete_post_meta( $order->id, PPEC_FEE_META_NAME_OLD ); 143 | } else { 144 | $fee = $order->get_meta( PPEC_FEE_META_NAME_OLD, true ); 145 | $order->delete_meta_data( PPEC_FEE_META_NAME_OLD ); 146 | $order->save_meta_data(); 147 | } 148 | 149 | if ( is_numeric( $fee ) ) { 150 | wc_gateway_ppec_set_transaction_fee( $order, $fee ); 151 | } 152 | 153 | return $fee; 154 | } 155 | 156 | /** 157 | * Checks whether the store is based in the US. 158 | * 159 | * Stores with a base location in the US, Puerto Rico, Guam, US Virgin Islands, American Samoa, or Northern Mariana Islands are considered US based stores. 160 | * 161 | * @return bool True if the store is located in the US or US Territory, otherwise false. 162 | */ 163 | function wc_gateway_ppec_is_US_based_store() { 164 | $base_location = wc_get_base_location(); 165 | return in_array( $base_location['country'], array( 'US', 'PR', 'GU', 'VI', 'AS', 'MP' ), true ); 166 | } 167 | 168 | /** 169 | * Saves the transaction details from the transaction response into a post meta. 170 | * 171 | * @since 2.0 172 | * 173 | * @param object $order Order for which the transaction was made 174 | * @param object $transaction_response Response from a transaction, which contains the transaction details 175 | * @param object $prefix A prefix string which is empty for Reference Transactions and is 'PAYMENTINFO_0_' for Express Checkout 176 | * @return void 177 | */ 178 | function wc_gateway_ppec_save_transaction_data( $order, $transaction_response, $prefix = '' ) { 179 | 180 | $settings = wc_gateway_ppec()->settings; 181 | $old_wc = version_compare( WC_VERSION, '3.0', '<' ); 182 | $order_id = $old_wc ? $order->id : $order->get_id(); 183 | $meta = $old_wc ? get_post_meta( $order_id, '_woo_pp_txnData', true ) : $order->get_meta( '_woo_pp_txnData', true ); 184 | 185 | if ( ! empty( $meta ) ) { 186 | $txnData = $meta; 187 | } else { 188 | $txnData = array( 'refundable_txns' => array() ); 189 | } 190 | 191 | $txn = array( 192 | 'txnID' => $transaction_response[ $prefix . 'TRANSACTIONID' ], 193 | 'amount' => $transaction_response[ $prefix . 'AMT' ], 194 | 'refunded_amount' => 0, 195 | ); 196 | 197 | $status = ! empty( $transaction_response[ $prefix . 'PAYMENTSTATUS' ] ) ? $transaction_response[ $prefix . 'PAYMENTSTATUS' ] : ''; 198 | 199 | if ( 'Completed' === $status ) { 200 | $txn['status'] = 'Completed'; 201 | } else { 202 | $txn['status'] = $status . '_' . $transaction_response[ $prefix . 'REASONCODE' ]; 203 | } 204 | $txnData['refundable_txns'][] = $txn; 205 | 206 | $paymentAction = $settings->get_paymentaction(); 207 | 208 | if ( 'authorization' === $paymentAction ) { 209 | $txnData['auth_status'] = 'NotCompleted'; 210 | } 211 | 212 | $txnData['txn_type'] = $paymentAction; 213 | 214 | if ( $old_wc ) { 215 | update_post_meta( $order_id, '_woo_pp_txnData', $txnData ); 216 | } else { 217 | $order->update_meta_data( '_woo_pp_txnData', $txnData ); 218 | $order->save(); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /includes/pem/bundle.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIG+zCCBeOgAwIBAgIQBhHe9dDxbRmioOMIJMXxtDANBgkqhkiG9w0BAQsFADBw 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz 5 | dXJhbmNlIFNlcnZlciBDQTAeFw0yMDA3MjcwMDAwMDBaFw0yMjA4MDExMjAwMDBa 6 | MIGMMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMI 7 | U2FuIEpvc2UxFTATBgNVBAoTDFBheVBhbCwgSW5jLjEaMBgGA1UECxMRUGF5UGFs 8 | IFByb2R1Y3Rpb24xIjAgBgNVBAMTGWFwaS0zdC5zYW5kYm94LnBheXBhbC5jb20w 9 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5zogjJyNtrySj8Rab20FG 10 | d406BAqFGCzXZX+Xvt6dALagOtDiWXGnnia4QvKQqi9JuYezh6TYUAwo7DYWmGfG 11 | +xPx+mrYFPkq3Efa5+IxL++Yuz5zVsUbOkPO+RgBg5zbZdFe7Nr9ZG7wBqeCbySM 12 | xf2BhvqEYVurelr9EjJMSgL4cjKQtmxNRH80vY0WMFfHmsm37KG5gsaJbG12DmwF 13 | /9Cmn5aP0kw0kMsdpUgzh0y2lAKzpGfy5veZ5+rlT0f8LEFzL/CIdPXzfgB3l2qI 14 | CXaGyMTAUgiocudnkoVrsvBqWXaBrPhRpPzp6AEZI7BDCcUUpqhwxOL8+KCG7Vv7 15 | AgMBAAGjggNyMIIDbjAfBgNVHSMEGDAWgBRRaP+QrwIHdTzM2WVkYqISuFlyOzAd 16 | BgNVHQ4EFgQUCM4YWfbdBgs0+0yJ+IyFtVAPVU0wJAYDVR0RBB0wG4IZYXBpLTN0 17 | LnNhbmRib3gucGF5cGFsLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI 18 | KwYBBQUHAwEGCCsGAQUFBwMCMHUGA1UdHwRuMGwwNKAyoDCGLmh0dHA6Ly9jcmwz 19 | LmRpZ2ljZXJ0LmNvbS9zaGEyLWhhLXNlcnZlci1nNi5jcmwwNKAyoDCGLmh0dHA6 20 | Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWhhLXNlcnZlci1nNi5jcmwwTAYDVR0g 21 | BEUwQzA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGln 22 | aWNlcnQuY29tL0NQUzAIBgZngQwBAgIwgYMGCCsGAQUFBwEBBHcwdTAkBggrBgEF 23 | BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME0GCCsGAQUFBzAChkFodHRw 24 | Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEySGlnaEFzc3VyYW5j 25 | ZVNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMIIBfAYKKwYBBAHWeQIEAgSCAWwE 26 | ggFoAWYAdQApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAXORx2s0 27 | AAAEAwBGMEQCIE1Sw9KFhldw0W5DNSdbpFRcrYP3o/Glvyp5wgH4u3Y9AiAcNAbo 28 | twfhCInMId97ZrgwM5Ibxhz+NQO6CtGCtVjPlAB2ACJFRQdZVSRWlj+hL/H3bYbg 29 | IyZjrcBLf13Gg1xu4g8CAAABc5HHa2QAAAQDAEcwRQIhAKqPEf95m0ZS/mgVY6oB 30 | x/B1LbYslSj87VViGqrsTKBtAiA2ehLtN3P6vzvdtQyo8QhVWAlUcAXh2ejoDL7k 31 | 8sn8bAB1AEHIyrHfIkZKEMahOglCh15OMYsbA+vrS8do8JBilgb2AAABc5HHavUA 32 | AAQDAEYwRAIgcajVwoUc4pLL0SRxLUnpEo/SoUKkJJbeOwdDuU/su6wCIEa4z8HP 33 | 0rF316ItKQPwJBvJ5QKa4WyRpvNJHEiCB34tMA0GCSqGSIb3DQEBCwUAA4IBAQBA 34 | BT7Qag+BbvD5NFZXfRoWjFjKfMSrklszjs7jWJ/H57Lt0EuyWod3m88Zk0Ueh+Qq 35 | destepdzXuxaiKZEfx8wg+hqC67mBuMjJSAccUcKXiiQi+WjMapJMYqKBOnNIkfy 36 | FPEM/bTkqgwvqVlVgjNoQroxNSTY4aRUl51HXOEgXyUhdzvms5O1QpMpT/7qYNT6 37 | jgY90PuOAST++cpfEf7ESs2DbdzVcB/5cyEapSTYKM7sP67Vz+24KE0K5yKln1In 38 | GhrfMk3xOIljR5zGrlNwubXRzkpH8zcnK39VIdgufvJvnXuHbO9xzFJU2TxDh4AC 39 | QlhNInKQ1vfMtAAy2HUK 40 | -----END CERTIFICATE----- 41 | -----BEGIN CERTIFICATE----- 42 | MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs 43 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 44 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 45 | ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL 46 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 47 | LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy 48 | YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2 49 | 4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC 50 | Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1 51 | itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn 52 | 4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X 53 | sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft 54 | bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA 55 | MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw 56 | NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy 57 | dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t 58 | L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG 59 | BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ 60 | UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D 61 | aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd 62 | aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH 63 | E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly 64 | /D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu 65 | xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF 66 | 0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae 67 | cPUeybQ= 68 | -----END CERTIFICATE----- 69 | -----BEGIN CERTIFICATE----- 70 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs 71 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 72 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 73 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL 74 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 75 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug 76 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm 77 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW 78 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM 79 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB 80 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 81 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg 82 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF 83 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA 84 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec 85 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z 86 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF 87 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 88 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe 89 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep 90 | +OkuE6N36B9K 91 | -----END CERTIFICATE----- 92 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WooCommerce PayPal Checkout Payment Gateway === 2 | Contributors: woocommerce, automattic, woothemes, akeda, dwainm, royho, allendav, slash1andy, woosteve, spraveenitpro, mikedmoore, fernashes, shellbeezy, danieldudzic, mikaey, fullysupportedphil, dsmithweb, corsonr, bor0, zandyring, pauldechov, robobot3000, jorgeatorres, mattdallan, menakas, chickenn00dle, jamesgallan, achyuthajoy, codestor 3 | Tags: ecommerce, e-commerce, commerce, woothemes, wordpress ecommerce, store, sales, sell, shop, shopping, cart, checkout, configurable, paypal 4 | Requires at least: 4.4 5 | Tested up to: 5.8 6 | Requires PHP: 5.5 7 | Stable tag: 2.1.3 8 | License: GPLv3 9 | License URI: http://www.gnu.org/licenses/gpl-3.0.html 10 | 11 | Accept PayPal, Credit Cards and Debit Cards on your WooCommerce store. 12 | 13 | == Description == 14 | 15 | ⚠️ **Support for PayPal Checkout will discontinue from *1 Mar 2022* and updates to the plugin have stopped as of *1 Sept 2021*. We recommend [switching to PayPal Payments](https://docs.woocommerce.com/document/woocommerce-paypal-payments/paypal-payments-upgrade-guide/).** 16 | 17 | This is a PayPal Checkout Payment Gateway for WooCommerce. 18 | 19 | PayPal Checkout allows you to securely sell your products and subscriptions online using In-Context Checkout to help you meet security requirements without causing your theme to suffer. In-Context Checkout uses a modal window, hosted on PayPal's servers, that overlays the checkout form and provides a secure means for your customers to enter their account information. 20 | 21 | Also, with Integrated PayPal Setup (Easy Setup), connecting to PayPal is as simple as clicking a button - no complicated API keys to cut and paste. 22 | 23 | == Installation == 24 | 25 | = Minimum Requirements = 26 | 27 | * WordPress 4.4 or greater 28 | 29 | = Automatic installation = 30 | 31 | Automatic installation is the easiest option as WordPress handles the file transfers itself and you don’t need to leave your web browser. To do an automatic install of, log in to your WordPress dashboard, navigate to the Plugins menu and click Add New. 32 | 33 | In the search field type "WooCommerce PayPal Checkout" and click Search Plugins. Once you’ve found our plugin you can view details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking “Install Now”. 34 | 35 | = Manual installation = 36 | 37 | The manual installation method involves downloading our plugin and uploading it to your webserver via your favorite FTP application. The 38 | WordPress codex contains [instructions on how to do this here](http://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation). 39 | 40 | = Updating = 41 | 42 | Automatic updates should work like a charm; as always though, ensure you backup your site just in case. 43 | 44 | If on the off-chance you do encounter issues with the shop/category pages after an update you simply need to flush the permalinks by going to WordPress > Settings > Permalinks and hitting 'save'. That should return things to normal. 45 | 46 | == Frequently Asked Questions == 47 | 48 | = Does this plugin work with credit cards or just PayPal? = 49 | 50 | This plugin supports payments using both credit and debit cards as well as PayPal. The new Smart Payment Buttons feature dynamically displays PayPal, Venmo (US Only), PayPal Credit, or other local payment options* in a single stack—without needing to leave the merchant's website. 51 | 52 | *PayPal Checkout features may not be available in all countries. 53 | 54 | = Does this support Checkout with PayPal from the cart view? = 55 | 56 | Yes! 57 | 58 | = Does this support both production mode and sandbox mode for testing? = 59 | 60 | Yes it does - production and sandbox mode is driven by how you connect. You may choose to connect in either mode, and disconnect and reconnect in the other mode whenever you want. 61 | 62 | = Where can I find documentation? = 63 | 64 | For help setting up and configuring, please refer to our [user guide](https://docs.woocommerce.com/document/paypal-express-checkout/) 65 | 66 | = Where can I get support or talk to other users? = 67 | 68 | If you get stuck, you can ask for help in the Plugin Forum. 69 | 70 | = Will this plugin work with my theme? = 71 | 72 | Yes, this plugin will work with any theme, but may require some styling to make it match nicely. If you're 73 | looking for a theme with built in WooCommerce integration we recommend [Storefront](http://www.woothemes.com/storefront/). 74 | 75 | = Where can I request new features or report bugs? = 76 | 77 | New feature requests and bugs reports can be made in the plugin forum. 78 | 79 | = How to remove 'Proceed to Checkout' button from cart page? = 80 | 81 | If PayPal Checkout is the only enabled payment gateway and you want to remove the 'Proceed to Checkout' button from the cart, you can use this snippet: 82 | 83 | https://gist.github.com/mikejolley/ad2ecc286c9ad6cefbb7065ba6dfef48 84 | 85 | = Where can I contribute? = 86 | 87 | The GitHub repository for PayPal Checkout is here: 88 | 89 | https://github.com/woocommerce/woocommerce-gateway-paypal-express-checkout 90 | 91 | Please use this to inform us about bugs, or make contributions via PRs. 92 | 93 | == Screenshots == 94 | 95 | 1. Click the "Setup or link an existing PayPal account" button. If you want to test before going live, you can switch the Environment, above the button, to Sandbox. 96 | 2. API credentials will be set after linking, or you can set them manually. 97 | 3. See PayPal button settings below. 98 | 4. Checkout with PayPal directly from the Cart. 99 | 5. And without leaving the store. 100 | 6. Confirm details after clicking "Continue". 101 | 7. Choose PayPal from regular checkout page. 102 | 8. Choose PayPal from single product page. 103 | 9. Initiate checkout from mini-cart. 104 | 105 | == Changelog == 106 | 107 | = 2.1.3 - 2021-09-16 = 108 | * Tweak - Remove broken URL from plugin headers. PR#887 109 | * Tweak - Update notice on plugins page about support and EOL. PR#886 110 | 111 | = 2.1.2 - 2021-06-28 = 112 | * Fix - Prevent fatal error when a line item isn't a WC_Product instance. PR#872 113 | * Fix - [WC Subscriptions] Update the shipping packages address using a dynamic key rather than assuming a 0 index. PR#871 114 | * New - Allow hiding of funding methods MercadoPago and BLIK. PR#870 115 | * Tweak - Make labels/descriptions more consistent on the settings screen. PR#771 116 | * Tweak - Make WooCommerce 3.2.0 explicit. PR#868 117 | * Dev - Add hooks to alter names and descriptions of line items sent to PayPal. PR#869 118 | * Fix - Create session cookie only when needed. PR#793, PR#845. 119 | * Tweak - Mark as compatible with latest WordPress and WooCommerce. PR#867 120 | * Fix - Replace jQuery 3.x deprecated functions. PR#852 121 | * Fix - Honor shape settings when rendering buttons for alternative funding sources. PR#844 122 | * New - Add notice on plugins page to upgrade to PayPal Payments. PR#866 123 | 124 | = 2.1.1 - 2020-11-24 = 125 | * Fix - Update the bundle.pem file to use the certificates from PayPal. PR#822 126 | * Tweak - PHP 8.0 compatibility. PR#837 127 | 128 | = 2.1.0 - 2020-10-06 = 129 | * New - Add support for PayPal Credit messaging. PR#810 130 | * Fix - Hide the "Pay Later" funding method when "PayPal Credit" is disabled. PR#811 131 | * Fix - Display correct image size in the PayPal Checkout window. PR#779 132 | 133 | = 2.0.3 - 2020-07-01 = 134 | * Fix - Records the proper refunded_amount to _woo_pp_txnData in the database PR#764 135 | * Fix - Redirect customers back to the original page they left on after closing PayPal modal PR#765 136 | * Fix - Preserve horizontal layout style setting when using standalone buttons PR#774 137 | * Fix - Smart payment buttons compatibility with older browsers PR#778 138 | * Tweak - Update the Require Phone Number field description PR#772 139 | * Dev - Make the SDK script args filterable PR#763 140 | 141 | = 2.0.2 - 2020-05-28 = 142 | * Fix - Javascript errors during checkout when the Payment Action is set to Authorize. PR#754 143 | * Fix - Style the Smart Payment Buttons according to the chosen button size setting. PR#753 144 | * Tweak - Change the "or" separator used on the cart page to be consistent with other payment gateways (uppercase and 100% opacity). PR#755 145 | 146 | = 2.0.1 - 2020-05-26 = 147 | * Fix - PayPal buttons not loading on the page, accompanied with the javascript/console error: "paypal.getFundingSources (or paypal.Buttons) is not a function". PR#740 148 | 149 | = 2.0.0 - 2020-05-25 = 150 | * New - Upgrade to the latest PayPal Checkout Javascript SDK. PR#668 151 | * Add - New setting found under Button Styles for choosing a Smart Payment Button label. PR#666 152 | * Add - Support for more locales. PR#658 153 | * Fix - Display Smart Payment Buttons on Product pages built from a shortcode. PR#665 154 | * Fix - Send the product SKU to PayPal so it's displayed in the order/transaction details and reports on PayPal. PR#664 155 | * Fix - Show an error when saving incomplete/missing API credentials. PR#712 156 | * Fix - Remove PHP warnings in later versions of PHP when a PayPal Session doesn't exist. PR#727 157 | * Fix - Error when processing refunds (Already Refunded. No Amount to Refund). PR#710 158 | * Fix - Required state field errors on the "Confirm your PayPal Order" page when returning from PayPal. PR#725 159 | * Fix - Display WC Add To Cart validation errors on the product page when clicking the PayPal Smart Payment Buttons. PR#707 160 | * Update - Smart Payment Buttons are enabled by default and settings to toggle these on/off have been removed and replaced with a filter. PR#660 161 | * Update - Deprecate unused/incomplete function `WC_Gateway_PPEC_Client::update_billing_agreement()`. PR#602 162 | * Update - Move inline javascript found in `settings-ppec.php` to `ppec-settings.js`. PR#676 163 | * Update - Move Support and Documentation links from the plugin actions to plugin meta section on the Plugin activation/deactivation page. PR#735 164 | * Update - WooCommerce 4.1 and WordPress 5.4 compatibility. PR#732 165 | 166 | = 1.6.21 - 2020-04-14 = 167 | * Fix - Ensure Puerto Rico and supported Locales are eligible for PayPal Credit. PR#693 168 | * Fix - Support purchasing subscriptions with $0 initial payment - free trials, synced etc. PR#698 169 | * Fix - Only make the billing fields optional during an active PayPal Checkout session. PR#697 170 | * Fix - Uncaught JS errors on product page when viewing and out-of-stock product. PR#704 171 | * Fix - Loading API certificates and improves managing certificate settings. PR#696 172 | * Fix - Displaying PayPal Smart Payment buttons on pages with single product shortcode. PR#665 173 | * Fix - Do not add discounts to total item amount and cause line item amount offset. PR#677 174 | * Fix - Redirect to Confirm your PayPal Order page for subscriptions initial purchases using PayPal Smart Buttons. PR#702 175 | * Fix - Display missing checkout notice when email format is incorrect. PR#708 176 | * Add - Filter product form validity via a new `wc_ppec_validate_product_form` event. PR#695 177 | * Add - Translation tables for states of more countries. PR#659 178 | * Update - WooCommerce 4.0 compatibility 179 | 180 | = 1.6.20 - 2020-02-18 = 181 | * Fix - Upgrade the plugin on plugins loaded rather than on plugin init. PR#682 182 | 183 | = 1.6.19 - 2020-02-06 = 184 | * Fix - Check if order exists before adding order actions. PR #653 185 | * Fix - Global attributes stripped before sent to PayPal if unicode characters. PR#470 186 | * Fix - Handle subscription payment change. PR#640 187 | * Fix - Fixes error "Trying to get property of non-object" found during onboarding wizard. PR#654 188 | * Fix - Hide smart payment buttons on mini cart when cart is empty. PR#450 189 | * Fix - Only display smart buttons on product page if product is in stock. PR#662 190 | * Fix - Do not display smart buttons for external products and grouped products. PR#663 191 | * Update - Display a WooCommerce pre 3.0 admin notice warning. In an upcoming release PayPal Checkout will drop support for WC 2.6 and below. PR#671 192 | 193 | = 1.6.18 - 2019-12-05 = 194 | * Fix - Send fees to PayPal as line items 195 | * Fix - Fix error 10426 when coupons are used 196 | * Fix - Call to a member function has_session() on null 197 | * Add - Notice about legacy payment buttons deprecation 198 | * Fix - Use order currency when renewing subscription instead of store currency 199 | * Update - WooCommerce 3.8 compatibility 200 | * Update - WordPress 5.3 compatibility 201 | 202 | = 1.6.17 - 2019-08-08 = 203 | * Update - WooCommerce 3.7 compatibility 204 | * Add - Filter to require display of billing agreement during checkout 205 | * Add - Add CURRENCYCODE to capture_payment 206 | * Add - Add filter for buttons on products 207 | * Fix - Skip wasteful render on initial Checkout page load 208 | * Fix - Appearance tweaks on Checkout screen 209 | 210 | = 1.6.16 - 2019-07-18 = 211 | * Fix - Don't require address for renewal of virtual subscriptions 212 | * Fix - Avoid broken confirmation screen edge case after 10486 redirect 213 | 214 | = 1.6.15 - 2019-06-19 = 215 | * Fix - Prevent PHP errors when no billing details are present in PP response 216 | * Fix - Require billing address for virtual products when enabled 217 | * Add - Hook when a payment error occurs 218 | 219 | = 1.6.14 - 2019-05-08 = 220 | * Fix - Failing checkout when no addons are used 221 | 222 | = 1.6.12 - 2019-05-08 = 223 | * Fix - Better handling of virtual subscriptions when billing address is not required 224 | * Fix - Prevent errors showing when purchasing a virtual product with WP_DEBUG enabled 225 | 226 | = 1.6.11 - 2019-04-17 = 227 | * Fix/Performance - Prevent db option updates during bootstrap on each page load 228 | * Tweak = WC 3.6 compatibiliy. 229 | 230 | = 1.6.10 - 2019-03-05 = 231 | * Fix - Use only product attributes when adding to cart 232 | 233 | = 1.6.9 - 2019-02-03 = 234 | * Fix - Avoid SPB render error by tweaking 'allowed' funding methods' empty value 235 | 236 | = 1.6.8 - 2019-01-25 = 237 | * Fix - Guard against themes applying filter with too few params 238 | 239 | = 1.6.7 - 2019-01-25 = 240 | * Fix - Error 10413 when using coupons 241 | * Fix: All variation details when using buttons on product pages are kept 242 | * Fix: Always render the PayPal buttons in the mini cart 243 | 244 | = 1.6.6 - 2019-01-09 = 245 | * Fix - Discount items were not being included 246 | * Add - Filter for order details to accept decimal quantities of products 247 | * Fix - Unable to buy variation from product page 248 | * Fix - Can use PayPal from product page without inputting required fields 249 | * Add - Display PayPal fees under the totals on the order admin page 250 | * Add - Prefill name, phone, and email info in PayPal Guest Checkout from checkout screen 251 | 252 | = 1.6.5 - 2018-10-31 = 253 | * Fix - Truncate the line item descriptions to avoid exceeding PayPal character limits. 254 | * Update - WC 3.5 compatibility. 255 | * Fix - checkout.js script loading when not needed. 256 | * Fix - Missing shipping total and address when starting from checkout page. 257 | 258 | = 1.6.4 - 2018-09-27 = 259 | * Fix - Billing address from Checkout form not being passed to PayPal via Smart Payment Button. 260 | * Fix - Checkout form not being validated until after Smart Payment Button payment flow. 261 | 262 | = 1.6.3 - 2018-08-15 = 263 | * Fix - Fatal error caused by a fix for Smart Payment Buttons. 264 | 265 | = 1.6.2 - 2018-08-15 = 266 | * Fix - Tax not applied on the (Confirm your PayPal order) page at the checkout. 267 | 268 | = 1.6.1 - 2018-07-04 = 269 | * Fix - GDPR Fatal error exporting user data when they have PPEC subscriptions. 270 | * Fix - PayPal Credit still being disabled by default. 271 | * Update - Rename 'PayPal Express Checkout' to 'PayPal Checkout'. 272 | * Fix - Missing PayPal branding in "Buy Now" Smart Payment Button. 273 | * Fix - PHP warning when PayPal Credit not supported and no funding methods hidden. 274 | * Fix - Smart Payment Buttons gateway not inheriting IPN and subscription handling. 275 | * Fix - Single product Smart Payment Button failing without existing session. 276 | * Fix - When cart is empty, JS error on cart page and mini-cart payment buttons showing. 277 | * Add - Locale filter. 278 | 279 | = 1.6.0 - 2018-06-27 = 280 | * Add - Smart Payment Buttons mode as alternative to directly embedded image links for all instances of PayPal button. 281 | * Fix - Help tip alignment for image settings. 282 | * Update - Enable PayPal Credit by default, and restrict its support by currency. 283 | * Update - Omit 'Express Checkout' portion of default payment method title. 284 | * Update - Enable Express Checkout on regular checkout page by default. 285 | * Update - Enable Express Checkout on single product page by default. 286 | 287 | = 1.5.6 - 2018-06-06 = 288 | * Fix - Virtual products cause issues with billing details validation. 289 | 290 | = 1.5.5 - 2018-05-23 = 291 | * Update - WC 3.4 compatibility 292 | * Update - Privacy policy notification. 293 | * Update - Export/erasure hooks added. 294 | 295 | = 1.5.4 - 2018-05-08 = 296 | * Add - Hook to make billing address not required `woocommerce_paypal_express_checkout_address_not_required` (bool). 297 | * Fix - Duplicate checkout settings when PP Credit option is enabled. 298 | * Fix - Impossible to open API credentials after saving Settings. 299 | * Fix - Prevent filtering if PPEC is not enabled. 300 | * Fix - Single Product checkout: Quantity being duplicated due to multiple AJAX calls. 301 | * Fix - When returning from PayPal, place order buttons says "proceed to payment". 302 | * Tweak - Default billing address to be required. 303 | 304 | = 1.5.3 - 2018-03-28 = 305 | * Fix - wp_enqueue_media was not correctly loaded causing weird behavior with other parts of system wanting to use it. 306 | * Fix - Typo in activation hook. 307 | 308 | = 1.5.2 - 2018-02-20 = 309 | * Tweak - Express checkout shouldn't display "Review your order before the payment". 310 | * Fix - Compatibility with Subscriptions and Checkout from Single Product page. 311 | * Fix - Make sure session object exists before use to prevent fatal error. 312 | 313 | [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-paypal-express-checkout/trunk/changelog.txt). 314 | -------------------------------------------------------------------------------- /templates/paypal-payments-upgrade-notice.php: -------------------------------------------------------------------------------- 1 | 'install-plugin', 15 | 'plugin' => dirname( $paypal_payments_path ), 16 | ), 17 | admin_url( 'update.php' ) 18 | ), 19 | 'install-plugin_' . dirname( $paypal_payments_path ) 20 | ); 21 | 22 | $paypal_payments_activate_link = wp_nonce_url( 23 | add_query_arg( 24 | array( 25 | 'action' => 'activate', 26 | 'plugin' => $paypal_payments_path, 27 | ), 28 | admin_url( 'plugins.php' ) 29 | ), 30 | 'activate-plugin_' . $paypal_payments_path 31 | ); 32 | ?> 33 | 34 | 35 | 36 |
    37 |
    38 |

    Action Required: Switch to WooCommerce PayPal Payments

    39 |
    40 |
    41 |

    As of 1 Sept 2021, PayPal Checkout is officially retired from WooCommerce.com, and support for this product will end as of 1 March 2022.

    42 |

    We highly recommend upgrading to PayPal Payments, the latest, fully supported extension that includes all of the features of PayPal Checkout and more.

    43 |
    44 | 51 |
    52 | 53 | 54 | -------------------------------------------------------------------------------- /woocommerce-gateway-paypal-express-checkout.php: -------------------------------------------------------------------------------- 1 | maybe_run(); 49 | 50 | /** 51 | * Adds the WooCommerce Inbox option on plugin activation 52 | * 53 | * @since 2.1.2 54 | */ 55 | if ( ! function_exists( 'add_woocommerce_inbox_variant' ) ) { 56 | function add_woocommerce_inbox_variant() { 57 | $option = 'woocommerce_inbox_variant_assignment'; 58 | 59 | if ( false === get_option( $option, false ) ) { 60 | update_option( $option, wp_rand( 1, 12 ) ); 61 | } 62 | } 63 | } 64 | register_activation_hook( __FILE__, 'add_woocommerce_inbox_variant' ); 65 | --------------------------------------------------------------------------------