From cf13d57f591d5be6b4fcf0f77b22bfd2d4bbbe7a Mon Sep 17 00:00:00 2001 From: Salvatore Pruiti Date: Wed, 22 Apr 2026 16:20:44 +0200 Subject: [PATCH 1/2] BCSAT-88 - Improve checkout flow, admin configuration handling and plugin security --- LICENSE | 20 +- .../js/blocks/satispay-badge/index.asset.php | 1 + assets/js/blocks/satispay-badge/index.js | 1 + assets/js/frontend/blocks.asset.php | 2 +- assets/js/frontend/blocks.js | 2 +- blocks/satispay-badge/block.json | 58 ++ blocks/satispay-badge/render.php | 48 ++ changelog.txt | 42 ++ includes/blocks/wc-satispay-blocks.php | 21 +- readme.txt | 35 +- resources/js/blocks/satispay-badge/index.js | 135 ++++ resources/js/frontend/index.js | 15 +- wc-satispay.php | 707 ++++++++++++++++-- woo-satispay.php | 50 +- woo-satispay.pot | 255 ++++++- 15 files changed, 1263 insertions(+), 129 deletions(-) create mode 100644 assets/js/blocks/satispay-badge/index.asset.php create mode 100644 assets/js/blocks/satispay-badge/index.js create mode 100644 blocks/satispay-badge/block.json create mode 100644 blocks/satispay-badge/render.php create mode 100644 changelog.txt create mode 100644 resources/js/blocks/satispay-badge/index.js diff --git a/LICENSE b/LICENSE index 200cb6e..ed9cf1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,9 @@ -MIT License +Satispay for WooCommerce is licensed under the GNU General Public License v2.0 or later. Copyright (c) 2018-present Satispay -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/assets/js/blocks/satispay-badge/index.asset.php b/assets/js/blocks/satispay-badge/index.asset.php new file mode 100644 index 0000000..2a5c388 --- /dev/null +++ b/assets/js/blocks/satispay-badge/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n'), 'version' => '6da0e404000f0a37c335'); diff --git a/assets/js/blocks/satispay-badge/index.js b/assets/js/blocks/satispay-badge/index.js new file mode 100644 index 0000000..c2bf26b --- /dev/null +++ b/assets/js/blocks/satispay-badge/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const e=window.wp.i18n,t=window.wp.blocks,o=window.wp.blockEditor,l=window.wp.components,a=JSON.parse('{"UU":"woo-satispay/payment-badge"}'),i=window.ReactJSXRuntime,n=window.wooSatispayBadge?.logoUrl||"",s=()=>(0,i.jsx)("svg",{width:"24",height:"24",viewBox:"0 0 25 25",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",children:(0,i.jsx)("path",{fill:"#FF3D00",d:"M20.320312 0.574219L17.855469 2.839844L14.824219 0H20.097656C20.398438 0 20.550781 0.367188 20.320312 0.574219ZM14.832031 25H20.136719C20.441406 25 20.589844 24.625 20.359375 24.425781L17.871094 22.113281L14.832031 24.992188ZM1.324219 14.703125C0.695312 14.089844 0.355469 13.261719 0.355469 12.40625C0.355469 11.546875 0.71875 10.71875 1.347656 10.128906L7.136719 4.644531L11.484375 8.683594L7.710938 12.140625C7.636719 12.214844 7.589844 12.316406 7.589844 12.421875C7.589844 12.523438 7.636719 12.628906 7.71875 12.699219L11.535156 16.230469L7.183594 20.269531L1.316406 14.6875ZM4.941406 0H12.871094L23.652344 10.152344C24.28125 10.742188 24.644531 11.570312 24.644531 12.429688C24.652344 13.269531 24.304688 14.105469 23.699219 14.703125L12.902344 24.976562H4.917969C4.617188 24.976562 4.464844 24.609375 4.695312 24.402344L17.289062 12.738281C17.363281 12.667969 17.410156 12.5625 17.410156 12.460938C17.410156 12.355469 17.363281 12.253906 17.28125 12.183594L4.71875 0.574219C4.496094 0.375 4.648438 0 4.941406 0Z"})});(0,t.registerBlockType)(a.UU,{icon:(0,i.jsx)(s,{}),edit:function({attributes:t,setAttributes:a}){const{label:s,showLabel:r,inlineContent:g,textAlign:w,logoWidth:p}=t,d=(0,o.useBlockProps)({style:{textAlign:w}}),c=g?{display:"inline-flex",alignItems:"center",gap:"8px",textAlign:"left"}:{display:"inline-block"},h={display:g?"inline-block":"block",width:p+"px",height:"auto",marginLeft:g||"center"!==w&&"right"!==w?void 0:"auto",marginRight:g||"center"!==w?void 0:"auto"},x=g?{margin:0}:{marginTop:"8px"};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(o.InspectorControls,{children:(0,i.jsxs)(l.PanelBody,{title:(0,e.__)("Satispay Badge Settings","woo-satispay"),children:[(0,i.jsx)(l.ToggleControl,{label:(0,e.__)("Show label","woo-satispay"),checked:r,onChange:e=>a({showLabel:e})}),(0,i.jsx)(l.ToggleControl,{label:(0,e.__)("Show logo and text on the same row","woo-satispay"),checked:g,onChange:e=>a({inlineContent:e})}),r&&(0,i.jsx)(l.TextControl,{label:(0,e.__)("Label text","woo-satispay"),value:s,onChange:e=>a({label:e})}),(0,i.jsx)(l.RangeControl,{label:(0,e.__)("Logo width (px)","woo-satispay"),value:p,onChange:e=>a({logoWidth:e}),min:40,max:400}),(0,i.jsx)(l.SelectControl,{label:(0,e.__)("Alignment","woo-satispay"),value:w,options:[{label:(0,e.__)("Left","woo-satispay"),value:"left"},{label:(0,e.__)("Center","woo-satispay"),value:"center"},{label:(0,e.__)("Right","woo-satispay"),value:"right"}],onChange:e=>a({textAlign:e})})]})}),(0,i.jsx)("div",{...d,children:(0,i.jsxs)("div",{style:c,children:[n?(0,i.jsx)("img",{src:n,alt:(0,e.__)("Satispay","woo-satispay"),style:h}):(0,i.jsx)("span",{style:{fontStyle:"italic",color:"#999"},children:(0,e.__)("Satispay logo","woo-satispay")}),r&&s&&(0,i.jsx)("p",{style:x,children:s})]})})]})},save:()=>null})})(); \ No newline at end of file diff --git a/assets/js/frontend/blocks.asset.php b/assets/js/frontend/blocks.asset.php index a5b0a3d..0571d5a 100644 --- a/assets/js/frontend/blocks.asset.php +++ b/assets/js/frontend/blocks.asset.php @@ -1 +1 @@ - array('react', 'wc-blocks-registry', 'wc-settings', 'wp-html-entities', 'wp-i18n'), 'version' => 'd5aae4311c3954d25225'); + array('react-jsx-runtime', 'wp-html-entities', 'wp-i18n'), 'version' => '0b893e738227415030c3'); diff --git a/assets/js/frontend/blocks.js b/assets/js/frontend/blocks.js index 53e2458..aa69d17 100644 --- a/assets/js/frontend/blocks.js +++ b/assets/js/frontend/blocks.js @@ -1 +1 @@ -(()=>{"use strict";const t=window.React,e=window.wp.i18n,n=window.wc.wcBlocksRegistry,a=window.wp.htmlEntities,i=(0,window.wc.wcSettings.getSetting)("satispay_data",{}),s=(0,e.__)("Satispay"),c=(0,e.__)("Do it smart. Choose Satispay and pay with a tap!"),o=i.icon,l=(0,a.decodeEntities)(i.title)||s,r=()=>(0,a.decodeEntities)(i.description||c),w={name:"satispay",label:(0,t.createElement)((e=>{const{PaymentMethodLabel:n}=e.components,a=(0,t.createElement)("img",{src:o,alt:l,name:l});return(0,t.createElement)(n,{text:l,icon:a})}),null),content:(0,t.createElement)(r,null),edit:(0,t.createElement)(r,null),canMakePayment:()=>!0,ariaLabel:l,supports:{features:i.supports},icon:i.icon};(0,n.registerPaymentMethod)(w)})(); \ No newline at end of file +(()=>{"use strict";const t=window.wp.i18n,e=wc.wcBlocksRegistry,s=window.wp.htmlEntities,n=wc.wcSettings,i=window.ReactJSXRuntime,a=(0,n.getSetting)("satispay_data",{}),o=(0,t.__)("Satispay","woo-satispay"),c=(0,t.__)("Do it smart. Choose Satispay and pay with a tap!","woo-satispay"),r=a.icon,p=!1!==a.isSupportedCurrency,d=a.unsupportedCurrencyMessage||"",w=(0,s.decodeEntities)(a.title)||o,l=()=>(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("p",{children:(0,s.decodeEntities)(a.description||c)}),!p&&d?(0,i.jsx)("p",{className:"wc-block-components-notice-banner is-error wc-block-components-satispay-currency-notice",children:(0,s.decodeEntities)(d)}):null]}),y=t=>{const{PaymentMethodLabel:e}=t.components,s=(0,i.jsx)("img",{src:r,alt:w,name:w});return(0,i.jsx)(e,{text:w,icon:s})},m={name:"satispay",label:(0,i.jsx)(y,{}),content:(0,i.jsx)(l,{}),edit:(0,i.jsx)(l,{}),canMakePayment:()=>!0,ariaLabel:w,supports:{features:a.supports},icon:a.icon};(0,e.registerPaymentMethod)(m)})(); \ No newline at end of file diff --git a/blocks/satispay-badge/block.json b/blocks/satispay-badge/block.json new file mode 100644 index 0000000..904545e --- /dev/null +++ b/blocks/satispay-badge/block.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "woo-satispay/payment-badge", + "title": "Satispay Payment Methods", + "category": "woocommerce", + "description": "Display Satispay as an accepted payment method, with logo and optional label.", + "keywords": [ "satispay", "payment", "badge", "logo", "accepted" ], + "textdomain": "woo-satispay", + "editorScript": "woo-satispay-payment-badge-editor", + "render": "file:./render.php", + "example": { + "attributes": { + "label": "We accept Satispay", + "showLabel": true, + "inlineContent": false, + "textAlign": "left", + "logoWidth": 60 + } + }, + "attributes": { + "label": { + "type": "string", + "default": "We accept Satispay" + }, + "showLabel": { + "type": "boolean", + "default": true + }, + "inlineContent": { + "type": "boolean", + "default": false + }, + "textAlign": { + "type": "string", + "default": "left" + }, + "logoWidth": { + "type": "number", + "default": 60 + } + }, + "supports": { + "html": false, + "color": { + "background": true, + "text": true + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true + } + } +} + diff --git a/blocks/satispay-badge/render.php b/blocks/satispay-badge/render.php new file mode 100644 index 0000000..445db68 --- /dev/null +++ b/blocks/satispay-badge/render.php @@ -0,0 +1,48 @@ + 'woo-satispay-payment-badge', + 'style' => $inline_style, +] ); +?> +
> +
+ + +

+ +
+
+ + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..8258de2 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,42 @@ +*** Satispay for WooCommerce Changelog *** + +2026-04-21 - version 3.0.0 +* Added compatibility with WooCommerce 10.6.1 +* Updated deprecated functions and properties (WC_Order, WC_Cart, etc.) +* Fixed hook registration for available payment gateways +* Improved security with wp_safe_redirect +* Defined WC_SATISPAY_VERSION constant for better version management +* Updated license to GPLv2 or later for Marketplace compliance +* Implemented Store API security hardening for guest checkout +* Added visible EUR-only warnings in checkout and Blocks, plus authoritative server-side validation, without hiding Satispay +* Added input sanitization for all S2S callback parameters +* Removed direct usage of $_GET for improved security and WordPress compliance +* Added a dedicated late capture gateway setting +* Added clickable Satispay transaction links in WooCommerce order details +* Added staging/live dashboard transaction URL support for new Satispay orders +* Persisted the Satispay order environment to keep transaction links stable across configuration changes +* Fixed the sandbox account URL in gateway settings +* Detected and stored Satispay BNPL payments from payment API callbacks +* Detected and stored Satispay fringe benefits payments, including the fringe amount used +* Displayed Satispay BNPL and fringe benefits details in WooCommerce order pages and admin orders +* Added "Satispay Payment Methods" Gutenberg block for displaying Satispay logo and label in block-based areas (footers, pages) +* Restored cart contents when customer cancels payment on Satispay redirect (classic and block checkout) +* Cleared cart snapshot session data after successful payment +* Fixed admin error notices using WC_Admin_Settings::add_error() instead of raw echo +* Prevented invalid activation code from being saved on authentication failure +* Fixed PHP warning on missing checkbox POST data in process_admin_options +* Skipped unnecessary API call in admin_options when no credentials are configured +* Separated admin method_description from customer-facing checkout description +* Declared cart_checkout_blocks feature compatibility + +2026-03-31 - version 2.2.9 +* Updated compatibility to WP-6.9 + +2026-03-25 - version 2.2.8 +* Guidelines improvement + +2026-03-15 - version 2.2.7 +* Updated compatibility to WP-6.8.1 + +2026-02-28 - version 2.2.6 +* Updated compatibility to WP-6.6.0 diff --git a/includes/blocks/wc-satispay-blocks.php b/includes/blocks/wc-satispay-blocks.php index cc9a568..cf6ac9d 100644 --- a/includes/blocks/wc-satispay-blocks.php +++ b/includes/blocks/wc-satispay-blocks.php @@ -1,7 +1,5 @@ array(), - 'version' => '1.2.0' + 'version' => WC_SATISPAY_VERSION ); $script_url = WC_Satispay::plugin_url() . $script_path; @@ -63,7 +61,7 @@ public function get_payment_method_script_handles() { ); if ( function_exists( 'wp_set_script_translations' ) ) { - wp_set_script_translations( 'wc-satispay-payments-blocks', 'woo-satispay', WC_Satispay::plugin_abspath()); + wp_set_script_translations( 'wc-satispay-payments-blocks', 'woo-satispay', WC_Satispay::plugin_abspath() . 'languages'); } return [ 'wc-satispay-payments-blocks' ]; @@ -75,11 +73,22 @@ public function get_payment_method_script_handles() { * @return array */ public function get_payment_method_data() { + $currency = strtoupper( (string) get_woocommerce_currency() ); + $is_supported_currency = WC_Satispay::SUPPORTED_CURRENCY === $currency; + return [ 'title' => __('Satispay', 'woo-satispay'), - 'description' => __('Do it smart. Choose Satispay and pay with a tap!', 'woo-satispay'), + 'description' => __('After clicking "Pay now", you will be redirected to Satispay to complete your purchase securely', 'woo-satispay'), 'icon' => WC_Satispay::plugin_url() . '/logo.svg', - 'supports' => WC_Satispay::SUPPORTS + 'supports' => WC_Satispay::SUPPORTS, + 'currency' => $currency, + 'supportedCurrency' => WC_Satispay::SUPPORTED_CURRENCY, + 'isSupportedCurrency' => $is_supported_currency, + 'unsupportedCurrencyMessage' => sprintf( + __('Satispay is available only for orders in %1$s. Your current order currency is %2$s. Please choose a different payment method to complete your purchase.', 'woo-satispay'), + WC_Satispay::SUPPORTED_CURRENCY, + $currency + ), ]; } } \ No newline at end of file diff --git a/readme.txt b/readme.txt index 4b66659..e64bc27 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: Satispay Tags: woocommerce, satispay, payment method Requires at least: 5.0 Tested up to: 6.9 -Stable tag: 2.2.9 -License: MIT -License URI: https://opensource.org/licenses/MIT +Stable tag: 3.0.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html Save time and money by accepting payments from your customers with Satispay. Free, simple, secure! #doitsmart @@ -45,6 +45,35 @@ You can view Satispay's terms of service and privacy policy here: * Privacy Policy: https://www.satispay.com/en-it/legal-hub/privacy-policy/ == Changelog == += 3.0.0 = +* Added compatibility with WooCommerce 10.6.1 +* Updated deprecated functions and properties (WC_Order, WC_Cart, etc.) +* Fixed hook registration for available payment gateways +* Improved security with wp_safe_redirect +* Defined WC_SATISPAY_VERSION constant for better version management +* Updated license to GPLv2 or later for Marketplace compliance +* Implemented Store API security hardening for guest checkout +* Added visible EUR-only warnings in checkout and Blocks, plus authoritative server-side validation, without hiding Satispay +* Added input sanitization for all S2S callback parameters +* Removed direct usage of $_GET for improved security and WordPress compliance +* Added a dedicated late capture gateway setting +* Added clickable Satispay transaction links in WooCommerce orders +* Added staging/live dashboard transaction URL support for new Satispay orders +* Persisted the Satispay order environment to keep transaction links stable across configuration changes +* Fixed the sandbox account URL in gateway settings +* Detected and stored Satispay BNPL payments from payment API callbacks +* Detected and stored Satispay fringe benefits payments, including the fringe amount used +* Displayed Satispay BNPL and fringe benefits details in WooCommerce order pages and admin orders +* Added "Satispay Payment Methods" Gutenberg block for displaying Satispay logo and label in block-based areas (footers, pages) +* Restored cart contents when customer cancels payment on Satispay redirect (classic and block checkout) +* Cleared cart snapshot session data after successful payment +* Fixed admin error notices using WC_Admin_Settings::add_error() instead of raw echo +* Prevented invalid activation code from being saved on authentication failure +* Fixed PHP warning on missing checkbox POST data in process_admin_options +* Skipped unnecessary API call in admin_options when no credentials are configured +* Separated admin method_description from customer-facing checkout description +* Declared cart_checkout_blocks feature compatibility + = 2.2.9 = * Updated compatibility to WP-6.9 diff --git a/resources/js/blocks/satispay-badge/index.js b/resources/js/blocks/satispay-badge/index.js new file mode 100644 index 0000000..7a769fd --- /dev/null +++ b/resources/js/blocks/satispay-badge/index.js @@ -0,0 +1,135 @@ +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; +import { + InspectorControls, + useBlockProps, +} from '@wordpress/block-editor'; +import { + PanelBody, + TextControl, + ToggleControl, + RangeControl, + SelectControl, +} from '@wordpress/components'; +import metadata from '../../../../blocks/satispay-badge/block.json'; + +const logoUrl = window.wooSatispayBadge?.logoUrl || ''; + +const SatispayBlockIcon = () => ( + +); + +function Edit( { attributes, setAttributes } ) { + const { label, showLabel, inlineContent, textAlign, logoWidth } = attributes; + + const blockProps = useBlockProps( { + style: { textAlign }, + } ); + + const previewContentStyle = inlineContent + ? { + display: 'inline-flex', + alignItems: 'center', + gap: '8px', + textAlign: 'left', + } + : { + display: 'inline-block', + }; + + const imgStyle = { + display: inlineContent ? 'inline-block' : 'block', + width: logoWidth + 'px', + height: 'auto', + marginLeft: ! inlineContent && ( textAlign === 'center' || textAlign === 'right' ) ? 'auto' : undefined, + marginRight: ! inlineContent && textAlign === 'center' ? 'auto' : undefined, + }; + + const labelStyle = inlineContent ? { margin: 0 } : { marginTop: '8px' }; + + return ( + <> + + + setAttributes( { showLabel: val } ) } + /> + setAttributes( { inlineContent: val } ) } + /> + { showLabel && ( + setAttributes( { label: val } ) } + /> + ) } + setAttributes( { logoWidth: val } ) } + min={ 40 } + max={ 400 } + /> + setAttributes( { textAlign: val } ) } + /> + + + +
+
+ { logoUrl ? ( + { + ) : ( + + { __( 'Satispay logo', 'woo-satispay' ) } + + ) } + { showLabel && label && ( +

{ label }

+ ) } +
+
+ + ); +} + +registerBlockType( metadata.name, { + icon: , + edit: Edit, + save: () => null, // dynamic block — rendered server-side via render.php +} ); + + + + + diff --git a/resources/js/frontend/index.js b/resources/js/frontend/index.js index fe7d1ad..3b0c845 100644 --- a/resources/js/frontend/index.js +++ b/resources/js/frontend/index.js @@ -1,5 +1,5 @@ -import { sprintf, __ } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { registerPaymentMethod } from '@woocommerce/blocks-registry'; import { decodeEntities } from '@wordpress/html-entities'; import { getSetting } from '@woocommerce/settings'; @@ -11,13 +11,24 @@ const defaultLabel = __('Satispay', 'woo-satispay'); const defaultDescription = __('Do it smart. Choose Satispay and pay with a tap!','woo-satispay'); const iconUrl = settings.icon; +const isSupportedCurrency = settings.isSupportedCurrency !== false; +const unsupportedCurrencyMessage = settings.unsupportedCurrencyMessage || ''; const label = decodeEntities( settings.title ) || defaultLabel; /** * Content component */ const Content = () => { - return decodeEntities( settings.description || defaultDescription ); + return ( + <> +

{ decodeEntities( settings.description || defaultDescription ) }

+ { ! isSupportedCurrency && unsupportedCurrencyMessage ? ( +

+ { decodeEntities( unsupportedCurrencyMessage ) } +

+ ) : null } + + ); }; /** * Label component diff --git a/wc-satispay.php b/wc-satispay.php index ec6cde2..7f5c6ad 100644 --- a/wc-satispay.php +++ b/wc-satispay.php @@ -7,24 +7,36 @@ class WC_Satispay extends WC_Payment_Gateway { + const SESSION_PAYMENT_ID_KEY = 'satispay_payment_id'; + + const SESSION_CART_SNAPSHOT_KEY = 'satispay_cart_snapshot'; + const SUPPORTS = array( 'products', 'refunds' ); + const SUPPORTED_CURRENCY = 'EUR'; + + const ORDER_ENVIRONMENT_META_KEY = '_satispay_environment'; + + const TRANSACTION_URL_LIVE = 'https://dashboard.satispay.com/dashboard/transactions/%s'; + + const TRANSACTION_URL_STAGING = 'https://staging.dashboard.satispay.com/dashboard/transactions/%s'; + public function __construct() { - if ((!empty($_GET['section'])) && ($_GET['section'] == 'satispay')) { + if ('satispay' === filter_input(INPUT_GET, 'section', FILTER_SANITIZE_SPECIAL_CHARS)) { $GLOBALS['hide_save_button'] = false; } $this->id = 'satispay'; $this->method_title = __('Satispay', 'woo-satispay'); - $this->order_button_text = __('Pay with Satispay', 'woo-satispay'); + $this->order_button_text = __('Pay now', 'woo-satispay'); $this->method_description = __('Do it smart. Choose Satispay and pay with a tap!', 'woo-satispay'); $this->has_fields = false; $this->supports = self::SUPPORTS; $this->title = $this->method_title; - $this->description = $this->method_description; + $this->description = __('After clicking "Pay now", you will be redirected to Satispay to complete your purchase securely', 'woo-satispay'); $this->icon = plugins_url('/logo.svg', __FILE__); $this->init_form_fields(); @@ -32,6 +44,12 @@ public function __construct() { add_action('woocommerce_update_options_payment_gateways_'.$this->id, array($this, 'process_admin_options')); add_action('woocommerce_api_wc_gateway_'.$this->id, array($this, 'gateway_api')); + add_filter('woocommerce_order_actions', array($this, 'add_order_actions'), 10, 2); + add_action('woocommerce_order_action_satispay_capture_authorized_payment', array($this, 'capture_authorized_payment')); + add_action('woocommerce_order_action_satispay_cancel_authorized_payment', array($this, 'cancel_authorized_payment')); + add_action('woocommerce_thankyou_'.$this->id, array($this, 'render_thankyou_payment_features')); + add_action('woocommerce_order_details_after_order_table', array($this, 'render_order_payment_features')); + add_action('woocommerce_admin_order_data_after_order_details', array($this, 'render_admin_order_payment_features')); if ($this->get_option('sandbox') == 'yes') { \SatispayGBusiness\Api::setSandbox(true); @@ -40,17 +58,35 @@ public function __construct() { \SatispayGBusiness\Api::setPublicKey($this->get_option('publicKey')); \SatispayGBusiness\Api::setPrivateKey($this->get_option('privateKey')); \SatispayGBusiness\Api::setKeyId($this->get_option('keyId')); - add_action('woocommerce_available_payment_gateways', array($this, 'check_gateway'), 15); + add_filter('woocommerce_available_payment_gateways', array($this, 'check_gateway'), 15); } + const ORDER_FLOW_META_KEY = '_satispay_flow'; + + const ORDER_REMOTE_STATUS_META_KEY = '_satispay_remote_status'; + + const ORDER_AUTHORIZED_AT_META_KEY = '_satispay_authorized_at'; + + const ORDER_LAST_SYNC_AT_META_KEY = '_satispay_last_sync_at'; + + const ORDER_PAYMENT_TYPE_META_KEY = '_satispay_payment_type'; + + const ORDER_PAYMENT_TYPE_LABEL_META_KEY = '_satispay_payment_type_label'; + + const ORDER_IS_BNPL_META_KEY = '_satispay_is_bnpl'; + + const ORDER_IS_FRINGE_BENEFITS_META_KEY = '_satispay_is_fringe_benefits'; + + const ORDER_FRINGE_AMOUNT_UNIT_META_KEY = '_satispay_fringe_amount_unit'; + public function process_refund($order_id, $amount = null, $reason = '') { - $order = new WC_Order($order_id); + $order = wc_get_order($order_id); try { $response = \SatispayGBusiness\Payment::create(array( 'flow' => 'REFUND', 'amount_unit' => round($amount * 100), - 'currency' => (method_exists($order, 'get_currency')) ? $order->get_currency() : $order->order_currency, + 'currency' => $order->get_currency(), 'parent_payment_uid' => $order->get_transaction_id() )); @@ -82,19 +118,10 @@ public function finalize_orders() { } //callback logic $payment = \SatispayGBusiness\Payment::get($transactionId); - if ($order->has_status(wc_get_is_paid_statuses())) { + if ($order->has_status(wc_get_is_paid_statuses()) && $payment->status !== 'ACCEPTED') { continue; } - if ($payment->status === 'ACCEPTED') { - $order->payment_complete($payment->id); - $order->add_order_note('The Satispay Payment has been finalized by custom cron action'); - $order->save(); - } - if ($payment->status === 'CANCELED') { - $order->update_status("wc-cancelled"); - $order->add_order_note('The Satispay Payment has been cancelled by custom cron action'); - $order->save(); - } + $this->sync_order_with_payment($order, $payment, 'custom cron action'); } } catch (\Exception $e) { if (function_exists('wc_get_logger')) { @@ -108,6 +135,54 @@ public function finalize_orders() { } } + public function add_order_actions($actions, $order) { + if (!$order instanceof WC_Order || $order->get_payment_method() !== $this->id || !$this->is_late_capture_order($order)) { + return $actions; + } + + $remoteStatus = $order->get_meta(self::ORDER_REMOTE_STATUS_META_KEY, true); + + if ($remoteStatus === 'AUTHORIZED' && !$order->has_status(wc_get_is_paid_statuses())) { + $actions['satispay_capture_authorized_payment'] = __('Capture authorized Satispay payment', 'woo-satispay'); + $actions['satispay_cancel_authorized_payment'] = __('Cancel Satispay authorization', 'woo-satispay'); + } + + return $actions; + } + + public function capture_authorized_payment($order) { + if (!$order instanceof WC_Order || !$this->is_late_capture_order($order)) { + return; + } + + try { + $payment = \SatispayGBusiness\Payment::update($order->get_transaction_id(), array( + 'action' => 'ACCEPT', + 'amount_unit' => round($order->get_total() * 100) + )); + + $this->sync_order_with_payment($order, $payment, 'manual capture'); + } catch (\Exception $e) { + $order->add_order_note(sprintf(__('Satispay capture failed: %s', 'woo-satispay'), $e->getMessage())); + } + } + + public function cancel_authorized_payment($order) { + if (!$order instanceof WC_Order || !$this->is_late_capture_order($order)) { + return; + } + + try { + $payment = \SatispayGBusiness\Payment::update($order->get_transaction_id(), array( + 'action' => 'CANCEL' + )); + + $this->sync_order_with_payment($order, $payment, 'manual cancellation'); + } catch (\Exception $e) { + $order->add_order_note(sprintf(__('Satispay cancellation failed: %s', 'woo-satispay'), $e->getMessage())); + } + } + public function init_form_fields() { $this->form_fields = array( 'enabled' => array( @@ -126,75 +201,82 @@ public function init_form_fields() { 'label' => __('Sandbox Mode', 'woo-satispay'), 'type' => 'checkbox', 'default' => 'no', - 'description' => wp_kses_post(sprintf(__('Sandbox Mode can be used to test payments. Request a Sandbox Account.', 'woo-satispay'), 'https://developers.satispay.com/docs/sandbox-account')) + 'description' => wp_kses_post(sprintf(__('Sandbox Mode can be used to test payments. Request a Sandbox Account.', 'woo-satispay'), 'https://satispay-sandbox.paperform.co/')) + ), + 'lateCapture' => array( + 'title' => __('Late capture', 'woo-satispay'), + 'label' => __('Enable late capture', 'woo-satispay'), + 'type' => 'checkbox', + 'default' => 'no', + 'description' => __('When enabled, new Satispay payments will use the late capture flow.', 'woo-satispay') ), 'finalizeUnhandledTransactions' => array( 'title' => __('Finalize unhandled payments', 'woo-satispay'), 'label' => __('Enable cron', 'woo-satispay'), 'type' => 'checkbox', 'default' => 'no', - 'description' => sprintf(__('Finalize unhandled Satispay payments with a cron.', 'woo-satispay')) + 'description' => __('Finalize unhandled Satispay payments with a cron.', 'woo-satispay') ), 'finalizeMaxHours' => array( 'title' => __('Finalize pending payments up to', 'woo-satispay'), 'label' => __('Finalize pending payments up to', 'woo-satispay'), 'type' => 'integer', 'default' => 4, - 'description' => sprintf(__('Choose a number of hours, default is four and minimum is two.', 'woo-satispay')) + 'description' => __('Choose a number of hours, default is four and minimum is two.', 'woo-satispay') ) ); } public function gateway_api() { - switch($_GET['action']) { + $action = (string) filter_input(INPUT_GET, 'action', FILTER_SANITIZE_SPECIAL_CHARS); + switch($action) { case 'redirect': - $paymentId = WC()->session->get('satispay_payment_id'); + $paymentId = (WC()->session) ? WC()->session->get(self::SESSION_PAYMENT_ID_KEY) : ''; if (!$paymentId) { - header('Location: '.$this->get_return_url('')); - break; + wp_safe_redirect($this->get_return_url()); + exit; } $payment = \SatispayGBusiness\Payment::get($paymentId); - $order = new WC_Order($payment->metadata->order_id); - - if ($payment->status === 'ACCEPTED') { - header('Location: '.$this->get_return_url($order)); + $order = wc_get_order($payment->metadata->order_id); + if (in_array($payment->status, array('ACCEPTED', 'AUTHORIZED'), true)) { + $this->sync_order_with_payment($order, $payment, 'redirect'); + $this->clear_checkout_cart_snapshot(); + wp_safe_redirect($this->get_return_url($order)); + exit; } else { \SatispayGBusiness\Payment::update($payment->id, array( 'action' => 'CANCEL' )); - header('Location: '. WC()->cart->get_checkout_url()); + + $this->restore_checkout_cart_snapshot($order); + wp_safe_redirect(wc_get_checkout_url()); + exit; } - break; case 'callback': - $payment = \SatispayGBusiness\Payment::get($_GET['payment_id']); - $order = new WC_Order($payment->metadata->order_id); + $paymentId = (string) filter_input(INPUT_GET, 'payment_id', FILTER_SANITIZE_SPECIAL_CHARS); + $payment = \SatispayGBusiness\Payment::get($paymentId); + $order = wc_get_order($payment->metadata->order_id); - if ($order->has_status(wc_get_is_paid_statuses())) { + if (!$order instanceof WC_Order) { exit; } - if ($payment->status === 'ACCEPTED') { - $order->payment_complete($payment->id); - } - if ($payment->status === 'CANCELED') { - $order->update_status("wc-cancelled"); - } + $this->sync_order_with_payment($order, $payment, 'callback'); break; } } public function process_admin_options() { $activationCode = $this->get_option('activationCode'); - $sandbox = $this->get_option('sandbox'); - $finalizeMaxHours = $this->get_option('finalizeMaxHours'); - $finalizeUnhandledTransactions = $this->get_option('finalizeUnhandledTransactions'); $postData = $this->get_post_data(); - $newActivationCode = $postData['woocommerce_satispay_activationCode']; - $newSandbox = $postData['woocommerce_satispay_sandbox']; + $newActivationCode = isset($postData['woocommerce_satispay_activationCode']) + ? sanitize_text_field($postData['woocommerce_satispay_activationCode']) + : ''; + $newSandbox = !empty($postData['woocommerce_satispay_sandbox']); - if (!empty($newActivationCode) && $newActivationCode != $activationCode) { - if ($newSandbox == '1') { + if (!empty($newActivationCode) && $newActivationCode !== $activationCode) { + if ($newSandbox) { \SatispayGBusiness\Api::setSandbox(true); } @@ -210,9 +292,12 @@ public function process_admin_options() { \SatispayGBusiness\Api::setPrivateKey($authentication->privateKey); \SatispayGBusiness\Api::setPublicKey($authentication->publicKey); } catch(\Exception $ex) { - echo '
'; - echo '

'. esc_html(sprintf(__('The Activation Code "%s" is invalid', 'woo-satispay'), $newActivationCode)).'

'; - echo '
'; + WC_Admin_Settings::add_error( + sprintf(__('The Activation Code "%s" is invalid. Please check the code and try again.', 'woo-satispay'), $newActivationCode) + ); + + // Prevent the invalid activation code from being saved by restoring the previous value. + $_POST['woocommerce_satispay_activationCode'] = $activationCode; } } else if (empty($newActivationCode)) { $this->update_option('keyId', ''); @@ -225,15 +310,23 @@ public function process_admin_options() { } public function admin_options() { - try { - \SatispayGBusiness\Payment::all(); - } catch (\Exception $ex) { - echo '
'; - echo '

' . wp_kses_post(sprintf(__('Satispay is not correctly configured, get an Activation Code from Online Shop section on Satispay Dashboard', 'woo-satispay'), 'https://dashboard.satispay.com')) .'

'; - echo '
'; + $keyId = $this->get_option('keyId'); + + if (!empty($keyId)) { + try { + \SatispayGBusiness\Payment::all(); + } catch (\Exception $ex) { + WC_Admin_Settings::add_error( + sprintf( + __('Satispay is not correctly configured, get an Activation Code from Online Shop section on %sSatispay Dashboard%s', 'woo-satispay'), + '', + '' + ) + ); + } } - - return parent::admin_options(); + + parent::admin_options(); } public function is_available() { @@ -243,22 +336,66 @@ public function is_available() { return true; } + public function payment_fields() { + if (!empty($this->description)) { + echo wpautop(wp_kses_post($this->description)); + } + + if ($this->is_checkout_currency_supported()) { + return; + } + + echo ''; + } + + public function validate_fields() { + if ($this->is_checkout_currency_supported()) { + return true; + } + + wc_add_notice($this->get_unsupported_currency_message(), 'error'); + + return false; + } + + public function is_late_capture_enabled() { + return $this->get_option('lateCapture') === 'yes'; + } + + public function get_transaction_url($order) { + $this->view_transaction_url = $this->get_transaction_url_base($order); + + return parent::get_transaction_url($order); + } + public function process_payment($order_id) { $order = wc_get_order($order_id); - $apiUrl = WC()->api_request_url('WC_Gateway_Satispay'); - if (strpos($apiUrl, '?') !== FALSE) { - $callbackUrl = $apiUrl.'&action=callback&payment_id={uuid}'; - $redirectUrl = $apiUrl.'&action=redirect'; - } else { - $callbackUrl = $apiUrl.'?action=callback&payment_id={uuid}'; - $redirectUrl = $apiUrl.'?action=redirect'; + if (!$this->is_checkout_currency_supported($order)) { + $message = $this->get_unsupported_currency_message($order instanceof WC_Order ? $order->get_currency() : null); + + wc_add_notice($message, 'error'); + + return array( + 'result' => 'failure', + 'message' => $message, + ); } + $flow = $this->is_late_capture_enabled() ? 'FUND_LOCK' : 'MATCH_CODE'; + + $apiUrl = WC()->api_request_url('WC_Gateway_Satispay'); + $callbackUrl = add_query_arg( + ['action' => 'callback', 'payment_id' => '{uuid}'], + $apiUrl + ); + + $redirectUrl = add_query_arg(['action' => 'redirect'], $apiUrl); + $payment = \SatispayGBusiness\Payment::create(array( - 'flow' => 'MATCH_CODE', + 'flow' => $flow, 'amount_unit' => round($order->get_total() * 100), - 'currency' => (method_exists($order, 'get_currency')) ? $order->get_currency() : $order->order_currency, + 'currency' => $order->get_currency(), 'callback_url' => $callbackUrl, 'external_code' => $order->get_id(), 'redirect_url' => $redirectUrl, @@ -270,7 +407,17 @@ public function process_payment($order_id) { try { $order->update_status('wc-on-hold'); $order->set_transaction_id($payment->id); - WC()->session->set('satispay_payment_id', $payment->id); + $order->update_meta_data(self::ORDER_ENVIRONMENT_META_KEY, $this->get_environment()); + $order->update_meta_data(self::ORDER_FLOW_META_KEY, $flow); + $order->update_meta_data(self::ORDER_REMOTE_STATUS_META_KEY, $payment->status); + $order->update_meta_data(self::ORDER_LAST_SYNC_AT_META_KEY, current_time('mysql', true)); + if (WC()->session) { + if (!WC()->session->has_session()) { + WC()->session->set_customer_session_cookie(true); + } + WC()->session->set(self::SESSION_PAYMENT_ID_KEY, $payment->id); + $this->store_checkout_cart_snapshot($order, $payment->id); + } $order->save(); } catch (\Exception $e) { if (function_exists('wc_get_logger')) { @@ -288,6 +435,395 @@ public function process_payment($order_id) { ); } + public function render_thankyou_payment_features($order_id) { + $order = wc_get_order($order_id); + + if (!$order instanceof WC_Order) { + return; + } + + $this->render_order_payment_features_section($order); + } + + public function render_order_payment_features($order) { + if (!$order instanceof WC_Order) { + return; + } + + $this->render_order_payment_features_section($order); + } + + public function render_admin_order_payment_features($order) { + if (!$order instanceof WC_Order || $order->get_payment_method() !== $this->id) { + return; + } + + $paymentFeatures = $this->get_order_payment_features($order); + + if (!$paymentFeatures['has_special_payment']) { + return; + } + + echo '
'; + echo '

' . esc_html__('Satispay payment details', 'woo-satispay') . '

'; + + if (!empty($paymentFeatures['label'])) { + echo '

' . esc_html__('Payment solution', 'woo-satispay') . ': ' . esc_html($paymentFeatures['label']) . '

'; + } + + if ($paymentFeatures['is_fringe_benefits'] && null !== $paymentFeatures['fringe_amount_unit']) { + echo '

' . esc_html__('Fringe benefits used', 'woo-satispay') . ': ' . wp_kses_post($this->format_amount_from_unit($paymentFeatures['fringe_amount_unit'], $order->get_currency())) . '

'; + } + + echo '
'; + } + + private function is_late_capture_order($order) { + return $order instanceof WC_Order && $order->get_meta(self::ORDER_FLOW_META_KEY, true) === 'FUND_LOCK'; + } + + private function sync_order_with_payment($order, $payment, $context = 'sync') { + if (!$order instanceof WC_Order || !isset($payment->status)) { + return; + } + + $previousRemoteStatus = $order->get_meta(self::ORDER_REMOTE_STATUS_META_KEY, true); + + $order->update_meta_data(self::ORDER_REMOTE_STATUS_META_KEY, $payment->status); + $order->update_meta_data(self::ORDER_LAST_SYNC_AT_META_KEY, current_time('mysql', true)); + + if (!$order->get_transaction_id()) { + $order->set_transaction_id($payment->id); + } + + $this->sync_order_payment_features($order, $payment, $context); + + if ($payment->status === 'AUTHORIZED') { + if (!$order->get_meta(self::ORDER_AUTHORIZED_AT_META_KEY, true)) { + $order->update_meta_data(self::ORDER_AUTHORIZED_AT_META_KEY, current_time('mysql', true)); + } + + if ($order->get_status() !== 'on-hold' && !$order->has_status(wc_get_is_paid_statuses())) { + $order->update_status('on-hold'); + } + + if ($previousRemoteStatus !== 'AUTHORIZED') { + $order->add_order_note(sprintf(__('Satispay payment authorized and waiting for capture (%s).', 'woo-satispay'), $context)); + } + } + + if ($payment->status === 'ACCEPTED') { + if (!$order->has_status(wc_get_is_paid_statuses())) { + $order->payment_complete($payment->id); + $capturedAmount = isset($payment->amount_unit) ? wc_price($payment->amount_unit / 100, array('currency' => $order->get_currency())) : ''; + $order->add_order_note( + $capturedAmount + ? sprintf(__('Satispay payment captured for %1$s (%2$s).', 'woo-satispay'), $capturedAmount, $context) + : sprintf(__('Satispay payment captured (%s).', 'woo-satispay'), $context) + ); + } + } + + if ($payment->status === 'CANCELED') { + if (!$order->has_status('cancelled')) { + $order->update_status('cancelled'); + $order->add_order_note(sprintf(__('Satispay payment canceled (%s).', 'woo-satispay'), $context)); + } + } + + $order->save(); + } + + private function sync_order_payment_features($order, $payment, $context) { + $paymentFeatures = $this->extract_payment_features($payment); + + if (!$paymentFeatures['has_special_payment']) { + return; + } + + $previousLabel = (string) $order->get_meta(self::ORDER_PAYMENT_TYPE_LABEL_META_KEY, true); + $previousFringeAmountUnit = $order->get_meta(self::ORDER_FRINGE_AMOUNT_UNIT_META_KEY, true); + + $order->update_meta_data(self::ORDER_PAYMENT_TYPE_META_KEY, $paymentFeatures['type']); + $order->update_meta_data(self::ORDER_PAYMENT_TYPE_LABEL_META_KEY, $paymentFeatures['label']); + $order->update_meta_data(self::ORDER_IS_BNPL_META_KEY, $paymentFeatures['is_bnpl'] ? 'yes' : 'no'); + $order->update_meta_data(self::ORDER_IS_FRINGE_BENEFITS_META_KEY, $paymentFeatures['is_fringe_benefits'] ? 'yes' : 'no'); + + if (null !== $paymentFeatures['fringe_amount_unit']) { + $order->update_meta_data(self::ORDER_FRINGE_AMOUNT_UNIT_META_KEY, $paymentFeatures['fringe_amount_unit']); + } + + if ($previousLabel !== $paymentFeatures['label'] || (string) $previousFringeAmountUnit !== (string) $paymentFeatures['fringe_amount_unit']) { + $note = sprintf( + __('Satispay special payment details detected: %1$s (%2$s).', 'woo-satispay'), + $paymentFeatures['label'], + $context + ); + + if ($paymentFeatures['is_fringe_benefits'] && null !== $paymentFeatures['fringe_amount_unit']) { + $note = sprintf( + __('Satispay special payment details detected: %1$s. Fringe benefits used: %2$s (%3$s).', 'woo-satispay'), + $paymentFeatures['label'], + wp_strip_all_tags($this->format_amount_from_unit($paymentFeatures['fringe_amount_unit'], $order->get_currency())), + $context + ); + } + + $order->add_order_note($note); + } + } + + private function extract_payment_features($payment) { + $isBnpl = true === $this->get_nested_value($payment, array('payment_options', 'buy_now_pay_later', 'is_buy_now_pay_later')); + $fringeAmountUnit = $this->get_nested_value($payment, array('payment_method', 'welfare_fringe', 'amount_unit')); + $hasFringeBenefits = null !== $fringeAmountUnit && '' !== $fringeAmountUnit; + + if ($hasFringeBenefits) { + $fringeAmountUnit = (int) $fringeAmountUnit; + } else { + $fringeAmountUnit = null; + } + + $type = ''; + if ($hasFringeBenefits) { + $type = 'fringe_benefits'; + } elseif ($isBnpl) { + $type = 'bnpl'; + } + + return array( + 'type' => $type, + 'label' => $this->get_payment_features_label($isBnpl, $hasFringeBenefits), + 'is_bnpl' => $isBnpl, + 'is_fringe_benefits' => $hasFringeBenefits, + 'fringe_amount_unit' => $fringeAmountUnit, + 'has_special_payment' => $isBnpl || $hasFringeBenefits, + ); + } + + private function get_payment_features_label($isBnpl, $hasFringeBenefits) { + if ($isBnpl && $hasFringeBenefits) { + return __('Satispay BNPL + fringe benefits', 'woo-satispay'); + } + + if ($hasFringeBenefits) { + return __('Satispay fringe benefits', 'woo-satispay'); + } + + if ($isBnpl) { + return __('Satispay BNPL', 'woo-satispay'); + } + + return ''; + } + + private function get_nested_value($source, $path) { + $value = $source; + + foreach ($path as $segment) { + if (is_array($value) && array_key_exists($segment, $value)) { + $value = $value[$segment]; + continue; + } + + if (is_object($value) && isset($value->{$segment})) { + $value = $value->{$segment}; + continue; + } + + return null; + } + + return $value; + } + + private function get_order_payment_features($order): array + { + $isBnpl = $order->get_meta(self::ORDER_IS_BNPL_META_KEY, true) === 'yes'; + $isFringeBenefits = $order->get_meta(self::ORDER_IS_FRINGE_BENEFITS_META_KEY, true) === 'yes'; + $fringeAmountUnit = $order->get_meta(self::ORDER_FRINGE_AMOUNT_UNIT_META_KEY, true); + + if ($fringeAmountUnit === '') { + $fringeAmountUnit = null; + } elseif (null !== $fringeAmountUnit) { + $fringeAmountUnit = (int) $fringeAmountUnit; + } + + return array( + 'type' => (string) $order->get_meta(self::ORDER_PAYMENT_TYPE_META_KEY, true), + 'label' => (string) $order->get_meta(self::ORDER_PAYMENT_TYPE_LABEL_META_KEY, true), + 'is_bnpl' => $isBnpl, + 'is_fringe_benefits' => $isFringeBenefits, + 'fringe_amount_unit' => $fringeAmountUnit, + 'has_special_payment' => $isBnpl || $isFringeBenefits, + ); + } + + private function render_order_payment_features_section($order) { + if (!$order instanceof WC_Order || $order->get_payment_method() !== $this->id) { + return; + } + + $paymentFeatures = $this->get_order_payment_features($order); + + if (!$paymentFeatures['has_special_payment']) { + return; + } + + echo '
'; + echo '

' . esc_html__('Satispay payment details', 'woo-satispay') . '

'; + echo '
    '; + + if (!empty($paymentFeatures['label'])) { + echo '
  • '; + echo esc_html__('Payment solution:', 'woo-satispay') . ' '; + echo '' . esc_html($paymentFeatures['label']) . ''; + echo '
  • '; + } + + if ($paymentFeatures['is_fringe_benefits'] && null !== $paymentFeatures['fringe_amount_unit']) { + echo '
  • '; + echo esc_html__('Fringe benefits used:', 'woo-satispay') . ' '; + echo '' . wp_kses_post($this->format_amount_from_unit($paymentFeatures['fringe_amount_unit'], $order->get_currency())) . ''; + echo '
  • '; + } + + echo '
'; + echo '
'; + } + + private function format_amount_from_unit($amountUnit, $currency) { + return wc_price(((int) $amountUnit) / 100, array('currency' => $currency)); + } + + private function is_checkout_currency_supported($order = null) { + return $this->get_checkout_currency($order) === self::SUPPORTED_CURRENCY; + } + + private function get_checkout_currency($order = null) { + if ($order instanceof WC_Order) { + return strtoupper((string) $order->get_currency()); + } + + $contextOrder = $this->get_current_checkout_order(); + + if ($contextOrder instanceof WC_Order) { + return strtoupper((string) $contextOrder->get_currency()); + } + + return strtoupper((string) get_woocommerce_currency()); + } + + private function get_current_checkout_order() { + $orderId = (int) filter_input(INPUT_GET, 'order-pay', FILTER_SANITIZE_NUMBER_INT); + + if ($orderId <= 0) { + return null; + } + + $order = wc_get_order($orderId); + + return $order instanceof WC_Order ? $order : null; + } + + private function store_checkout_cart_snapshot($order, $paymentId = '') { + if (!WC()->session || !WC()->cart || !$order instanceof WC_Order) { + return; + } + + WC()->session->set( + self::SESSION_CART_SNAPSHOT_KEY, + array( + 'order_id' => $order->get_id(), + 'payment_id' => (string) $paymentId, + 'cart' => WC()->cart->get_cart_for_session(), + 'applied_coupons' => WC()->cart->get_applied_coupons(), + ) + ); + } + + private function restore_checkout_cart_snapshot($order = null) { + if (!WC()->session || !WC()->cart) { + return false; + } + + $snapshot = WC()->session->get(self::SESSION_CART_SNAPSHOT_KEY); + + if (empty($snapshot['cart']) || !is_array($snapshot['cart'])) { + return false; + } + + if ($order instanceof WC_Order && !empty($snapshot['order_id']) && (int) $snapshot['order_id'] !== (int) $order->get_id()) { + return false; + } + + WC()->cart->empty_cart(); + + foreach ($snapshot['cart'] as $cartItem) { + if (!is_array($cartItem)) { + continue; + } + + $productId = isset($cartItem['product_id']) ? (int) $cartItem['product_id'] : 0; + $quantity = isset($cartItem['quantity']) ? (int) $cartItem['quantity'] : 0; + $variationId = isset($cartItem['variation_id']) ? (int) $cartItem['variation_id'] : 0; + $variation = isset($cartItem['variation']) && is_array($cartItem['variation']) ? $cartItem['variation'] : array(); + + if ($productId <= 0 || $quantity <= 0) { + continue; + } + + $cartItemData = $cartItem; + unset( + $cartItemData['key'], + $cartItemData['product_id'], + $cartItemData['variation_id'], + $cartItemData['variation'], + $cartItemData['quantity'], + $cartItemData['data'], + $cartItemData['data_hash'], + $cartItemData['line_total'], + $cartItemData['line_tax'], + $cartItemData['line_subtotal'], + $cartItemData['line_subtotal_tax'], + $cartItemData['line_tax_data'] + ); + + WC()->cart->add_to_cart($productId, $quantity, $variationId, $variation, $cartItemData); + } + + if (!empty($snapshot['applied_coupons']) && is_array($snapshot['applied_coupons'])) { + foreach ($snapshot['applied_coupons'] as $couponCode) { + WC()->cart->apply_coupon($couponCode); + } + } + + WC()->cart->calculate_totals(); + $this->clear_checkout_cart_snapshot(); + + return true; + } + + private function clear_checkout_cart_snapshot() { + if (!WC()->session) { + return; + } + + WC()->session->set(self::SESSION_PAYMENT_ID_KEY, null); + WC()->session->set(self::SESSION_CART_SNAPSHOT_KEY, null); + } + + private function get_unsupported_currency_message($currency = null): string + { + $currency = strtoupper((string) ($currency ?: $this->get_checkout_currency())); + + return sprintf( + __('Satispay is available only for orders in %1$s. Your current order currency is %2$s. Please choose a different payment method to complete your purchase.', 'woo-satispay'), + self::SUPPORTED_CURRENCY, + $currency + ); + } + /** * Get the start criteria for the scheduled datetime */ @@ -314,6 +850,43 @@ private function get_end_date_scheduled_time() return strtotime($now->sub($tosub)->format('Y-m-d H:i:s')); } + /** + * Get current gateway environment. + * + * @return string + */ + private function get_environment() { + return $this->get_option('sandbox') === 'yes' ? 'staging' : 'live'; + } + + /** + * Get the Satispay environment for an order. + * + * @param WC_Order $order Order object. + * @return string + */ + private function get_order_environment($order) { + $environment = $order->get_meta(self::ORDER_ENVIRONMENT_META_KEY, true); + + if (!empty($environment)) { + return $environment; + } + + return $this->get_environment(); + } + + /** + * Get transaction dashboard URL base for an order. + * + * @param WC_Order $order Order object. + * @return string + */ + private function get_transaction_url_base($order) { + return $this->get_order_environment($order) === 'staging' + ? self::TRANSACTION_URL_STAGING + : self::TRANSACTION_URL_LIVE; + } + /** * Plugin url. * diff --git a/woo-satispay.php b/woo-satispay.php index efc9ab2..781f1ab 100644 --- a/woo-satispay.php +++ b/woo-satispay.php @@ -5,12 +5,12 @@ * Description: Save time and money by accepting payments from your customers with Satispay. Free, simple, secure! #doitsmart * Author: Satispay * Author URI: https://www.satispay.com/ - * License: MIT - * License URI: https://opensource.org/licenses/MIT - * Version: 2.2.9 + * License: GPLv2 or later + * License URI: https://www.gnu.org/licenses/gpl-2.0.html + * Version: 3.0.0 * Requires at least: 5.0 * Requires Plugins: woocommerce - * WC tested up to: 8.9 + * WC tested up to: 10.6 */ // Exit if accessed directly. @@ -18,13 +18,55 @@ exit; } +define( 'WC_SATISPAY_VERSION', '2.3.0' ); +define( 'WC_SATISPAY_MAIN_FILE', __FILE__ ); + +add_action( 'init', 'wc_satispay_register_blocks' ); +function wc_satispay_register_blocks() { + $block_dir = plugin_dir_path( __FILE__ ) . 'blocks/satispay-badge'; + $asset_file = plugin_dir_path( __FILE__ ) . 'assets/js/blocks/satispay-badge/index.asset.php'; + + if ( ! file_exists( $asset_file ) ) { + return; + } + + $asset = require $asset_file; + + wp_register_script( + 'woo-satispay-payment-badge-editor', + plugins_url( 'assets/js/blocks/satispay-badge/index.js', __FILE__ ), + $asset['dependencies'], + $asset['version'], + true + ); + +// if ( function_exists( 'wp_set_script_translations' ) ) { +// wp_set_script_translations( 'woo-satispay-payment-badge-editor', 'woo-satispay', plugin_dir_path( __FILE__ ) . 'languages' ); +// } + + wp_add_inline_script( + 'woo-satispay-payment-badge-editor', + 'window.wooSatispayBadge = ' . wp_json_encode( [ 'logoUrl' => plugins_url( 'logo.svg', __FILE__ ) ] ) . ';', + 'before' + ); + + register_block_type( $block_dir ); +} + add_action( 'before_woocommerce_init', function() { if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); } } ); add_action('plugins_loaded', 'wc_satispay_init', 0); +//add_action('plugins_loaded', 'wc_satispay_load_textdomain'); add_filter('cron_schedules', 'wc_satispay_cron_schedule'); + +//function wc_satispay_load_textdomain() { +// load_plugin_textdomain('woo-satispay', false, dirname(plugin_basename(__FILE__)) . '/languages'); +//} + function wc_satispay_init() { if (!class_exists('WC_Payment_Gateway')) return; diff --git a/woo-satispay.pot b/woo-satispay.pot index 04dea02..d20b7af 100644 --- a/woo-satispay.pot +++ b/woo-satispay.pot @@ -1,15 +1,15 @@ -# Copyright (C) 2025 Satispay -# This file is distributed under the MIT. +# Copyright (C) 2026 Satispay +# This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: Satispay for WooCommerce 2.2.9\n" +"Project-Id-Version: Satispay for WooCommerce 2.3.0\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/woo-satispay\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2025-12-01T16:16:06+00:00\n" +"POT-Creation-Date: 2026-04-21T17:37:38+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: woo-satispay\n" @@ -31,8 +31,12 @@ msgstr "" #. Author of the plugin #: woo-satispay.php -#: includes/blocks/wc-satispay-blocks.php:79 -#: wc-satispay.php:20 +#: blocks/satispay-badge/render.php:35 +#: includes/blocks/wc-satispay-blocks.php:80 +#: wc-satispay.php:32 +#: assets/js/blocks/satispay-badge/index.js:1 +#: assets/js/frontend/blocks.js:1 +#: resources/js/blocks/satispay-badge/index.js:109 #: resources/js/frontend/index.js:9 msgid "Satispay" msgstr "" @@ -42,81 +46,274 @@ msgstr "" msgid "https://www.satispay.com/" msgstr "" -#: includes/blocks/wc-satispay-blocks.php:80 -#: wc-satispay.php:22 +#: blocks/satispay-badge/render.php:10 +msgid "We accept Satispay" +msgstr "" + +#: includes/blocks/wc-satispay-blocks.php:81 +#: wc-satispay.php:39 +msgid "After clicking \"Pay now\", you will be redirected to Satispay to complete your purchase securely" +msgstr "" + +#: includes/blocks/wc-satispay-blocks.php:88 +#: wc-satispay.php:821 +#, php-format +msgid "Satispay is available only for orders in %1$s. Your current order currency is %2$s. Please choose a different payment method to complete your purchase." +msgstr "" + +#: wc-satispay.php:33 +msgid "Pay now" +msgstr "" + +#: wc-satispay.php:34 +#: assets/js/frontend/blocks.js:1 #: resources/js/frontend/index.js:11 msgid "Do it smart. Choose Satispay and pay with a tap!" msgstr "" -#: wc-satispay.php:21 -msgid "Pay with Satispay" +#: wc-satispay.php:146 +msgid "Capture authorized Satispay payment" +msgstr "" + +#: wc-satispay.php:147 +msgid "Cancel Satispay authorization" +msgstr "" + +#: wc-satispay.php:166 +#, php-format +msgid "Satispay capture failed: %s" +msgstr "" + +#: wc-satispay.php:182 +#, php-format +msgid "Satispay cancellation failed: %s" msgstr "" -#: wc-satispay.php:114 +#: wc-satispay.php:189 msgid "Enable/Disable" msgstr "" -#: wc-satispay.php:115 +#: wc-satispay.php:190 msgid "Enable Satispay" msgstr "" -#: wc-satispay.php:120 +#: wc-satispay.php:195 msgid "Activation Code" msgstr "" -#: wc-satispay.php:122 +#: wc-satispay.php:197 #, php-format msgid "Get a six characters Activation Code from Online Shop section on Satispay Dashboard." msgstr "" -#: wc-satispay.php:125 +#: wc-satispay.php:200 msgid "Sandbox" msgstr "" -#: wc-satispay.php:126 +#: wc-satispay.php:201 msgid "Sandbox Mode" msgstr "" -#: wc-satispay.php:129 +#: wc-satispay.php:204 #, php-format msgid "Sandbox Mode can be used to test payments. Request a Sandbox Account." msgstr "" -#: wc-satispay.php:132 +#: wc-satispay.php:207 +msgid "Late capture" +msgstr "" + +#: wc-satispay.php:208 +msgid "Enable late capture" +msgstr "" + +#: wc-satispay.php:211 +msgid "When enabled, new Satispay payments will use the late capture flow." +msgstr "" + +#: wc-satispay.php:214 msgid "Finalize unhandled payments" msgstr "" -#: wc-satispay.php:133 +#: wc-satispay.php:215 msgid "Enable cron" msgstr "" -#: wc-satispay.php:136 +#: wc-satispay.php:218 msgid "Finalize unhandled Satispay payments with a cron." msgstr "" -#: wc-satispay.php:139 -#: wc-satispay.php:140 +#: wc-satispay.php:221 +#: wc-satispay.php:222 msgid "Finalize pending payments up to" msgstr "" -#: wc-satispay.php:143 +#: wc-satispay.php:225 msgid "Choose a number of hours, default is four and minimum is two." msgstr "" -#: wc-satispay.php:214 +#: wc-satispay.php:296 +#, php-format +msgid "The Activation Code \"%s\" is invalid. Please check the code and try again." +msgstr "" + +#: wc-satispay.php:321 +#, php-format +msgid "Satispay is not correctly configured, get an Activation Code from Online Shop section on %sSatispay Dashboard%s" +msgstr "" + +#: wc-satispay.php:468 +#: wc-satispay.php:674 +msgid "Satispay payment details" +msgstr "" + +#: wc-satispay.php:471 +msgid "Payment solution" +msgstr "" + +#: wc-satispay.php:475 +msgid "Fringe benefits used" +msgstr "" + +#: wc-satispay.php:511 +#, php-format +msgid "Satispay payment authorized and waiting for capture (%s)." +msgstr "" + +#: wc-satispay.php:521 +#, php-format +msgid "Satispay payment captured for %1$s (%2$s)." +msgstr "" + +#: wc-satispay.php:522 +#, php-format +msgid "Satispay payment captured (%s)." +msgstr "" + +#: wc-satispay.php:530 #, php-format -msgid "The Activation Code \"%s\" is invalid" +msgid "Satispay payment canceled (%s)." msgstr "" -#: wc-satispay.php:232 +#: wc-satispay.php:558 #, php-format -msgid "Satispay is not correctly configured, get an Activation Code from Online Shop section on Satispay Dashboard" +msgid "Satispay special payment details detected: %1$s (%2$s)." msgstr "" -#: woo-satispay.php:57 +#: wc-satispay.php:565 +#, php-format +msgid "Satispay special payment details detected: %1$s. Fringe benefits used: %2$s (%3$s)." +msgstr "" + +#: wc-satispay.php:606 +msgid "Satispay BNPL + fringe benefits" +msgstr "" + +#: wc-satispay.php:610 +msgid "Satispay fringe benefits" +msgstr "" + +#: wc-satispay.php:614 +msgid "Satispay BNPL" +msgstr "" + +#: wc-satispay.php:679 +msgid "Payment solution:" +msgstr "" + +#: wc-satispay.php:686 +msgid "Fringe benefits used:" +msgstr "" + +#: woo-satispay.php:93 msgid "Settings" msgstr "" -#: woo-satispay.php:74 +#: woo-satispay.php:110 msgid "Every 4 hours" msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:66 +msgid "Satispay Badge Settings" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:68 +msgid "Show label" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:73 +msgid "Show logo and text on the same row" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:79 +msgid "Label text" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:85 +msgid "Logo width (px)" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:92 +msgid "Alignment" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:95 +msgid "Left" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:96 +msgid "Center" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:97 +msgid "Right" +msgstr "" + +#: assets/js/blocks/satispay-badge/index.js:1 +#: resources/js/blocks/satispay-badge/index.js:114 +msgid "Satispay logo" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block title" +msgid "Satispay Payment Methods" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block description" +msgid "Display Satispay as an accepted payment method, with logo and optional label." +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block keyword" +msgid "satispay" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block keyword" +msgid "payment" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block keyword" +msgid "badge" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block keyword" +msgid "logo" +msgstr "" + +#: blocks/satispay-badge/block.json +msgctxt "block keyword" +msgid "accepted" +msgstr "" From 47775ec502d470acf1ba71570b4ea09d138ab4f7 Mon Sep 17 00:00:00 2001 From: Salvatore Pruiti Date: Wed, 22 Apr 2026 16:37:26 +0200 Subject: [PATCH 2/2] Add 'Check Satispay payment status' order action for manual payment sync --- changelog.txt | 1 + readme.txt | 1 + wc-satispay.php | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8258de2..6628960 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ * Skipped unnecessary API call in admin_options when no credentials are configured * Separated admin method_description from customer-facing checkout description * Declared cart_checkout_blocks feature compatibility +* Added "Check Satispay payment status" order action for manual payment sync from admin 2026-03-31 - version 2.2.9 * Updated compatibility to WP-6.9 diff --git a/readme.txt b/readme.txt index e64bc27..7824101 100644 --- a/readme.txt +++ b/readme.txt @@ -73,6 +73,7 @@ You can view Satispay's terms of service and privacy policy here: * Skipped unnecessary API call in admin_options when no credentials are configured * Separated admin method_description from customer-facing checkout description * Declared cart_checkout_blocks feature compatibility +* Added "Check Satispay payment status" order action for manual payment sync from admin = 2.2.9 = * Updated compatibility to WP-6.9 diff --git a/wc-satispay.php b/wc-satispay.php index 7f5c6ad..e57c901 100644 --- a/wc-satispay.php +++ b/wc-satispay.php @@ -47,6 +47,7 @@ public function __construct() { add_filter('woocommerce_order_actions', array($this, 'add_order_actions'), 10, 2); add_action('woocommerce_order_action_satispay_capture_authorized_payment', array($this, 'capture_authorized_payment')); add_action('woocommerce_order_action_satispay_cancel_authorized_payment', array($this, 'cancel_authorized_payment')); + add_action('woocommerce_order_action_satispay_check_payment_status', array($this, 'check_payment_status')); add_action('woocommerce_thankyou_'.$this->id, array($this, 'render_thankyou_payment_features')); add_action('woocommerce_order_details_after_order_table', array($this, 'render_order_payment_features')); add_action('woocommerce_admin_order_data_after_order_details', array($this, 'render_admin_order_payment_features')); @@ -136,15 +137,21 @@ public function finalize_orders() { } public function add_order_actions($actions, $order) { - if (!$order instanceof WC_Order || $order->get_payment_method() !== $this->id || !$this->is_late_capture_order($order)) { + if (!$order instanceof WC_Order || $order->get_payment_method() !== $this->id) { return $actions; } - $remoteStatus = $order->get_meta(self::ORDER_REMOTE_STATUS_META_KEY, true); + if ($order->get_transaction_id()) { + $actions['satispay_check_payment_status'] = __('Check Satispay payment status', 'woo-satispay'); + } + + if ($this->is_late_capture_order($order)) { + $remoteStatus = $order->get_meta(self::ORDER_REMOTE_STATUS_META_KEY, true); - if ($remoteStatus === 'AUTHORIZED' && !$order->has_status(wc_get_is_paid_statuses())) { - $actions['satispay_capture_authorized_payment'] = __('Capture authorized Satispay payment', 'woo-satispay'); - $actions['satispay_cancel_authorized_payment'] = __('Cancel Satispay authorization', 'woo-satispay'); + if ($remoteStatus === 'AUTHORIZED' && !$order->has_status(wc_get_is_paid_statuses())) { + $actions['satispay_capture_authorized_payment'] = __('Capture authorized Satispay payment', 'woo-satispay'); + $actions['satispay_cancel_authorized_payment'] = __('Cancel Satispay authorization', 'woo-satispay'); + } } return $actions; @@ -183,6 +190,26 @@ public function cancel_authorized_payment($order) { } } + public function check_payment_status($order) { + if (!$order instanceof WC_Order) { + return; + } + + $transactionId = $order->get_transaction_id(); + + if (empty($transactionId)) { + $order->add_order_note(__('Satispay payment check skipped: no transaction ID found.', 'woo-satispay')); + return; + } + + try { + $payment = \SatispayGBusiness\Payment::get($transactionId); + $this->sync_order_with_payment($order, $payment, 'manual check'); + } catch (\Exception $e) { + $order->add_order_note(sprintf(__('Satispay payment check failed: %s', 'woo-satispay'), $e->getMessage())); + } + } + public function init_form_fields() { $this->form_fields = array( 'enabled' => array(