diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 65e96c0c8..f879f4a1b 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -45,7 +45,9 @@ class Renderer { } } - this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); + if (this.creditCardRenderer) { + this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); + } for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) { this.renderButtons( diff --git a/modules/ppcp-onboarding/resources/js/settings.js b/modules/ppcp-onboarding/resources/js/settings.js index 9440319ce..00e35b1e6 100644 --- a/modules/ppcp-onboarding/resources/js/settings.js +++ b/modules/ppcp-onboarding/resources/js/settings.js @@ -233,6 +233,7 @@ document.addEventListener( '#field-button_label', '#field-button_color', '#field-button_shape', + '#field-button_preview', ] ); @@ -256,6 +257,7 @@ document.addEventListener( '#field-button_product_label', '#field-button_product_color', '#field-button_product_shape', + '#field-button_product_preview', ] ); @@ -280,6 +282,7 @@ document.addEventListener( '#field-button_mini-cart_color', '#field-button_mini-cart_shape', '#field-button_mini-cart_height', + '#field-button_mini-cart_preview', ] ); @@ -291,6 +294,7 @@ document.addEventListener( '#field-button_cart_label', '#field-button_cart_color', '#field-button_cart_shape', + '#field-button_cart_preview', ] ); groupToggle( diff --git a/modules/ppcp-wc-gateway/package.json b/modules/ppcp-wc-gateway/package.json index 36265e6f1..b37863041 100644 --- a/modules/ppcp-wc-gateway/package.json +++ b/modules/ppcp-wc-gateway/package.json @@ -11,6 +11,7 @@ "Edge >= 14" ], "dependencies": { + "@paypal/paypal-js": "^5.1.1", "core-js": "^3.25.0" }, "devDependencies": { diff --git a/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss new file mode 100644 index 000000000..832642eb0 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss @@ -0,0 +1,17 @@ +.ppcp-button-preview { + width: 350px; + padding: 15px; + border: 1px solid lightgray; + border-radius: 15px; + box-shadow: 0 2px 10px 1px #ddd; + + h4 { + margin-top: 0; + } + + @media (min-width: 1200px) { + position: absolute; + left: 800px; + margin-top: -400px; + } +} diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index cf9ee45bd..7f502628c 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -1,3 +1,7 @@ +import { loadScript } from "@paypal/paypal-js"; +import {debounce} from "./helper/debounce"; +import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer' + ;document.addEventListener( 'DOMContentLoaded', () => { @@ -65,6 +69,70 @@ atLeastOneChecked(vaultingCheckboxes) ? disablePayLater() : enablePayLater() } + const form = jQuery('#mainform'); + + function createButtonPreview(settingsCallback) { + const render = (settings) => { + const wrapper = document.querySelector(settings.button.wrapper); + wrapper.innerHTML = ''; + + const renderer = new Renderer(null, settings, (data, actions) => actions.reject(), null); + + try { + renderer.render({}); + } catch (err) { + console.error(err); + } + }; + + let oldSettings = settingsCallback(); + + form.on('change', ':input', debounce(() => { + const newSettings = settingsCallback(); + if (JSON.stringify(oldSettings) === JSON.stringify(newSettings)) { + return; + } + + render(newSettings); + + oldSettings = newSettings; + }, 300)); + + jQuery(document).on('ppcp_paypal_script_loaded', () => { + oldSettings = settingsCallback(); + + render(oldSettings); + }); + + render(oldSettings); + } + + function getPaypalScriptSettings() { + const disabledSources = jQuery('[name="ppcp[disable_funding][]"]').val(); + const settings = { + 'client-id': PayPalCommerceGatewaySettings.client_id, + 'currency': PayPalCommerceGatewaySettings.currency, + 'integration-date': PayPalCommerceGatewaySettings.integration_date, + 'components': ['buttons', 'funding-eligibility', 'messages'], + 'enable-funding': ['venmo'], + 'buyer-country': PayPalCommerceGatewaySettings.country, + }; + if (disabledSources.length) { + settings['disable-funding'] = disabledSources; + } + return settings; + } + + function loadPaypalScript(settings, onLoaded = () => {}) { + loadScript(JSON.parse(JSON.stringify(settings))) // clone the object to prevent modification + .then(paypal => { + document.dispatchEvent(new CustomEvent('ppcp_paypal_script_loaded')); + + onLoaded(paypal); + }) + .catch((error) => console.error('failed to load the PayPal JS SDK script', error)); + } + disableAll( disabledCheckboxes ) togglePayLater() @@ -73,5 +141,71 @@ if(PayPalCommerceGatewaySettings.is_subscriptions_plugin_active !== '1') { document.getElementById('field-subscription_behavior_when_vault_fails').style.display = 'none'; } + + let oldScriptSettings = getPaypalScriptSettings(); + + form.on('change', ':input', debounce(() => { + const newSettings = getPaypalScriptSettings(); + if (JSON.stringify(oldScriptSettings) === JSON.stringify(newSettings)) { + return; + } + + loadPaypalScript(newSettings); + + oldScriptSettings = newSettings; + }, 1000)); + + function getButtonSettings(wrapperSelector, fields) { + const layout = jQuery(fields['layout']).val(); + const style = { + 'color': jQuery(fields['color']).val(), + 'shape': jQuery(fields['shape']).val(), + 'label': jQuery(fields['label']).val(), + 'tagline': layout === 'horizontal' && jQuery(fields['tagline']).is(':checked'), + 'layout': layout, + }; + if ('height' in fields) { + style['height'] = parseInt(jQuery(fields['height']).val()); + } + return { + 'button': { + 'wrapper': wrapperSelector, + 'style': style, + }, + 'separate_buttons': {}, + }; + } + + loadPaypalScript(oldScriptSettings, () => { + createButtonPreview(() => getButtonSettings('#ppcpCheckoutButtonPreview', { + 'color': '#ppcp-button_color', + 'shape': '#ppcp-button_shape', + 'label': '#ppcp-button_label', + 'tagline': '#ppcp-button_tagline', + 'layout': '#ppcp-button_layout', + })); + createButtonPreview(() => getButtonSettings('#ppcpProductButtonPreview', { + 'color': '#ppcp-button_product_color', + 'shape': '#ppcp-button_product_shape', + 'label': '#ppcp-button_product_label', + 'tagline': '#ppcp-button_product_tagline', + 'layout': '#ppcp-button_product_layout', + })); + createButtonPreview(() => getButtonSettings('#ppcpCartButtonPreview', { + 'color': '#ppcp-button_cart_color', + 'shape': '#ppcp-button_cart_shape', + 'label': '#ppcp-button_cart_label', + 'tagline': '#ppcp-button_cart_tagline', + 'layout': '#ppcp-button_cart_layout', + })); + createButtonPreview(() => getButtonSettings('#ppcpMiniCartButtonPreview', { + 'color': '#ppcp-button_mini-cart_color', + 'shape': '#ppcp-button_mini-cart_shape', + 'label': '#ppcp-button_mini-cart_label', + 'tagline': '#ppcp-button_mini-cart_tagline', + 'layout': '#ppcp-button_mini-cart_layout', + 'height': '#ppcp-button_mini-cart_height', + })); + }); } ); diff --git a/modules/ppcp-wc-gateway/resources/js/helper/debounce.js b/modules/ppcp-wc-gateway/resources/js/helper/debounce.js new file mode 100644 index 000000000..68a34e771 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/helper/debounce.js @@ -0,0 +1,9 @@ +export const debounce = (callback, delayMs) => { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, delayMs); + }; +}; diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 2df151099..386d3b613 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -401,6 +401,14 @@ return array( $onboarding_options_renderer = $container->get( 'onboarding.render-options' ); assert( $onboarding_options_renderer instanceof OnboardingOptionsRenderer ); + $render_preview_element = function ( string $id ): string { + return ' +
+

' . __( 'Preview', 'woocommerce-paypal-payments' ) . '

+
+
'; + }; + $fields = array( 'checkout_settings_heading' => array( 'heading' => __( 'Standard Payments Settings', 'woocommerce-paypal-payments' ), @@ -802,6 +810,15 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'button_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpCheckoutButtonPreview' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), 'message_heading' => array( 'heading' => __( 'Pay Later on Checkout', 'woocommerce-paypal-payments' ), 'type' => 'ppcp-heading', @@ -1107,6 +1124,15 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'button_product_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpProductButtonPreview' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), 'message_product_heading' => array( 'heading' => __( 'Pay Later on Single Product Page', 'woocommerce-paypal-payments' ), @@ -1413,6 +1439,15 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'button_cart_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpCartButtonPreview' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), 'message_cart_heading' => array( 'heading' => __( 'Pay Later on Cart', 'woocommerce-paypal-payments' ), @@ -1732,6 +1767,15 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'button_mini-cart_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpMiniCartButtonPreview' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), 'disable_cards' => array( 'title' => __( 'Disable specific credit cards', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 5926580ff..2fd00b4a5 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -37,17 +37,51 @@ class SettingsPageAssets { */ protected $subscription_helper; + /** + * The PayPal SDK client ID. + * + * @var string + */ + private $client_id; + + /** + * 3-letter currency code of the shop. + * + * @var string + */ + private $currency; + + /** + * 2-letter country code of the shop. + * + * @var string + */ + private $country; + /** * Assets constructor. * * @param string $module_url The url of this module. * @param string $version The assets version. * @param SubscriptionHelper $subscription_helper The subscription helper. + * @param string $client_id The PayPal SDK client ID. + * @param string $currency 3-letter currency code of the shop. + * @param string $country 2-letter country code of the shop. */ - public function __construct( string $module_url, string $version, SubscriptionHelper $subscription_helper ) { + public function __construct( + string $module_url, + string $version, + SubscriptionHelper $subscription_helper, + string $client_id, + string $currency, + string $country + ) { $this->module_url = $module_url; $this->version = $version; $this->subscription_helper = $subscription_helper; + $this->client_id = $client_id; + $this->currency = $currency; + $this->country = $country; } /** @@ -98,6 +132,13 @@ class SettingsPageAssets { * Register assets for admin pages. */ private function register_admin_assets() { + wp_enqueue_style( + 'ppcp-gateway-settings', + trailingslashit( $this->module_url ) . 'assets/css/gateway-settings.css', + array(), + $this->version + ); + wp_enqueue_script( 'ppcp-gateway-settings', trailingslashit( $this->module_url ) . 'assets/js/gateway-settings.js', @@ -106,12 +147,20 @@ class SettingsPageAssets { true ); - // Intent is configured with Authorize and Capture Virtual-Only Orders is not set. + /** + * Psalm cannot find it for some reason. + * + * @psalm-suppress UndefinedConstant + */ wp_localize_script( 'ppcp-gateway-settings', 'PayPalCommerceGatewaySettings', array( 'is_subscriptions_plugin_active' => $this->subscription_helper->plugin_is_active(), + 'client_id' => $this->client_id, + 'currency' => $this->currency, + 'country' => $this->country, + 'integration_date' => PAYPAL_INTEGRATION_DATE, ) ); } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 7fb9c0b3c..18a39b57e 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -157,7 +157,10 @@ class WCGatewayModule implements ModuleInterface { $assets = new SettingsPageAssets( $c->get( 'wcgateway.url' ), $c->get( 'ppcp.asset-version' ), - $c->get( 'subscription.helper' ) + $c->get( 'subscription.helper' ), + $c->get( 'button.client_id' ), + $c->get( 'api.shop.currency' ), + $c->get( 'api.shop.country' ) ); $assets->register_assets(); } diff --git a/modules/ppcp-wc-gateway/webpack.config.js b/modules/ppcp-wc-gateway/webpack.config.js index 91a99abb7..009952fa6 100644 --- a/modules/ppcp-wc-gateway/webpack.config.js +++ b/modules/ppcp-wc-gateway/webpack.config.js @@ -9,6 +9,7 @@ module.exports = { 'gateway-settings': path.resolve('./resources/js/gateway-settings.js'), 'pay-upon-invoice': path.resolve('./resources/js/pay-upon-invoice.js'), 'oxxo': path.resolve('./resources/js/oxxo.js'), + 'gateway-settings-style': path.resolve('./resources/css/gateway-settings.scss'), }, output: { path: path.resolve(__dirname, 'assets/'), @@ -19,6 +20,19 @@ module.exports = { test: /\.js?$/, exclude: /node_modules/, loader: 'babel-loader', + }, + { + test: /\.scss$/, + exclude: /node_modules/, + use: [ + { + loader: 'file-loader', + options: { + name: 'css/[name].css', + } + }, + {loader:'sass-loader'} + ] }] } }; diff --git a/modules/ppcp-wc-gateway/yarn.lock b/modules/ppcp-wc-gateway/yarn.lock index db1f0b716..9c070466e 100644 --- a/modules/ppcp-wc-gateway/yarn.lock +++ b/modules/ppcp-wc-gateway/yarn.lock @@ -1005,6 +1005,13 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@paypal/paypal-js@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-5.1.1.tgz#81ab1f78dd2001061a2472f561d20df687a1d295" + integrity sha512-MMQ8TA048gTB43pzEOMzod8WY8hfzy+ahd7w29LtMvXduqzp7/29WxrTlsy4k6ARG6WGJ/uGqpc4+la4UZEQgw== + dependencies: + promise-polyfill "^8.2.3" + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -1933,6 +1940,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +promise-polyfill@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" + integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b07da92da..000000000 --- a/package-lock.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "woocommerce-paypal-payments", - "version": "1.7.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "woocommerce-paypal-payments", - "version": "1.7.0", - "license": "GPL-2.0", - "devDependencies": {} - } - } -} diff --git a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php index 23f32d100..3f94739bf 100644 --- a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php +++ b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php @@ -16,7 +16,7 @@ class SettingsPagesAssetsTest extends TestCase $modulePath = '/var/www/html/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-wc-gateway'; $subscriptionsHelper = Mockery::mock(SubscriptionHelper::class); - $testee = new SettingsPageAssets($moduleUrl, $modulePath, $subscriptionsHelper); + $testee = new SettingsPageAssets($moduleUrl, $modulePath, $subscriptionsHelper, '123', 'EUR', 'DE'); when('is_admin') ->justReturn(true);